前端阅读 315月28日 03:34
前端模块规范有哪些?模块如何异步加载?
JavaScript 模块化经历了从全局变量污染到标准化模块系统的漫长演进,不同规范解决了不同阶段的问题。IIFE:最早的模块化尝试在规范出现之前,开发者用立即执行函数表达式创建独立作用域:var MyModule = (function() { var privateVar = 'hidden'; function privateMethod() { return privateVar; } return { publicMethod: function() { return privateMethod(); } };})();IIFE 通过闭包隔离内部变量,只暴露全局接口。缺点是依赖关系靠全局变量传递,script 标签顺序一旦出错就全局崩溃。CommonJS:Node.js 的选择// math.jsmodule.exports = { add: (a, b) => a + b };// main.jsconst { add } = require('./math');console.log(add(1, 2));CommonJS 用 require 同步加载模块,module.exports 导出。核心特征:运行时加载,require 执行时才确定依赖;输出值的拷贝,模块内部变化不会影响已导入的值;this 指向当前模块。同步加载在服务端不是问题——文件在本地磁盘,读取极快。但在浏览器中,模块要从网络下载,同步阻塞会让页面卡死。AMD:为浏览器而生define(['jquery', './utils'], function($, utils) { return { init: function() { $('body').append(utils.format()); } };});AMD(Asynchronous Module Definition)用 define 声明模块和依赖,依赖在回调执行前全部加载完成。RequireJS 是最知名的实现。依赖必须前置声明,不管是否马上用到都会先加载。CMD:依赖就近define(function(require, exports, module) { var $ = require('jquery'); // 用到时才加载 exports.init = function() { $('body').append('hello'); };});CMD 由 SeaJS 推广,和 AMD 的核心区别是依赖就近声明——只有执行到 require 时才加载对应模块。两者在浏览器端都已退出主流,被 ESModule 取代。UMD:兼容方案(function(root, factory) { if (typeof exports === 'object') module.exports = factory(); else if (typeof define === 'function') define(factory); else root.MyModule = factory();})(this, function() { return { version: '1.0' };});UMD 判断运行环境,兼容 CommonJS、AMD 和全局变量三种方式。库开发者打包时常用,确保代码在任何环境都能正常加载。ESModule:统一标准// math.jsexport const add = (a, b) => a + b;// main.jsimport { add } from './math.js';ESModule 是 JavaScript 语言层面的模块标准。与 CommonJS 的关键区别:编译时静态分析,import/export 必须在顶层,引擎在执行前就确定依赖关系;输出值的引用,模块内部变化会同步反映到导入方;顶层 this 为 undefined;天然支持 Tree-Shaking。模块异步加载异步加载的核心场景是按需加载——首屏不需要的代码延迟到使用时再请求,减少初始包体积。ESModule 动态导入:import() 返回 Promise,可在任意位置调用,是实现代码分割和路由懒加载的标准方式:button.addEventListener('click', async () => { const { openDialog } = await import('./dialog.js'); openDialog();});AMD 异步加载:require([deps], callback) 本身就是异步的,依赖列表中的模块并行下载后再执行回调。CommonJS:require 本身是同步的,不支持浏览器原生的异步加载。但打包工具(Webpack 等)可以将 import() 语法编译为 CommonJS 环境下的异步加载 chunk。追问import() 和顶层 import 有什么区别?顶层 import 是静态声明,必须在模块顶层,编译时确定依赖关系,引擎可以做静态分析和 Tree-Shaking。import() 是动态函数调用,返回 Promise,可以在任何位置调用,运行时才加载模块。后者用于代码分割、路由懒加载等按需加载场景。静态 import 在严格模式下还会被提升到模块顶部执行。为什么浏览器不支持 CommonJS 的 require?require 是同步调用——读取文件、编译、执行,然后返回结果。在服务端文件在本地磁盘上,同步读取耗时可以忽略;但在浏览器里模块要从网络下载,网络延迟不可控,同步阻塞意味着页面卡死直到所有依赖下载完成。AMD 和 ESModule 都采用异步加载模型,不阻塞主线程。库作者应该发布什么格式?ESM + CJS 双格式——package.json 的 exports 字段同时声明两种格式的入口,CJS 兼容老工具和老版本 Node.js,ESM 支持 Tree-Shaking 和静态分析。典型配置:{ "exports": { ".": { "import": "./dist/index.mjs", "require": "./dist/index.cjs" } }}单独只发 CJS 会丢失 Tree-Shaking 能力,单独只发 ESM 会排除不支持 ESM 的旧环境。双格式是当前最稳妥的方案。