模块化演进
早期 JavaScript 因缺乏官方模块化规范,催生了 CommonJS(CJS)(Node.js 原生)、AMD(RequireJS)等社区方案。2015 年 ES6 推出 ES 模块(ESM),以 import/export
静态语法实现标准化,逐渐成为前端主流。如今,ESM 已成为浏览器(原生支持)和 Node.js(14+ 版本主推)的首选方案,而 CommonJS 仍在旧项目中广泛存在,两者的兼容性问题成为现代开发的核心挑战。
ESM 与 CommonJS 的核心差异对比
特性
ESM
CommonJS
语法本质
静态声明(编译阶段确定依赖)
动态赋值(运行时解析依赖)
依赖加载
并行加载(浏览器优化)
同步阻塞(适合服务端)
作用域
严格隔离(无全局污染)
可通过 global
污染全局
导出能力
支持命名导出与默认导出
仅支持对象导出(需统一封装)
优化潜力
原生支持 Tree Shaking(体积优化)
不支持静态分析(优化受限)
循环引用
支持「部分加载」(按引用状态取值)
可能获取未初始化值(风险较高)
关键差异总结:ESM 通过静态结构实现性能与工具链优化,CommonJS 则因动态特性更适合传统服务端场景。
UMD
UMD 能够兼容多钟模块,包括 AMD 、CommonJS 以及浏览器环境的立即执行函数
四大典型兼容性场景与解决方案
场景 1:Node.js 中的模块类型冲突
问题:未配置 package.json
的 type
时,.js
文件默认作为 CommonJS 解析,直接使用 import
会报错(Node 14+ 仅警告)。
解决方案:
- 明确模块类型:
type: "module"
声明 ESM(.js
视为 ESM),或使用.mjs
(强制 ESM)、.cjs
(强制 CommonJS)后缀。 - 混合使用规则:ESM 可导入 CommonJS(自动转换),但 CommonJS 中禁止静态
import
(允许动态import()
)。
场景 2:浏览器中运行 CommonJS 模块
问题:浏览器原生支持 ESM(<script type="module">
),但不识别 require()
语法。
解决方案:
- 依赖打包工具(Webpack/Vite)将 CommonJS 转换为 ESM。
- 动态导入:
import('./cjs-module.cjs').then(module => { ... })
。
场景 3:第三方库的模块格式冲突
问题 1:ESM 中使用 require()
调用 CommonJS,导致静态分析失败(如 Tree Shaking 失效)。
案例:
// ESM 中动态加载 CommonJS(正确做法)
const module = await import('./cjs-library.cjs');
// 错误:静态 require 无法被打包工具追踪依赖
const module = require('./cjs-library');
问题 2:UMD 库环境检测错误导致默认导出丢失。
原理:UMD 通过 typeof exports
判断环境,若在 ESM 中未正确处理,需显式访问 .default
:
import umdLib from 'umd-library';
umdLib.default.foo(); // 因 UMD 未设置 default 导出,需手动处理
场景 4:打包工具配置错误
问题:Webpack 误将 ESM 打包为 CommonJS(output.type: 'commonjs'
),导致浏览器加载失败。
解决方案:
- 浏览器环境配置
output.type: 'module'
; - Node.js 环境使用
output.type: 'commonjs'
。
package.json 核心字段与模块解析规则
1. type 字段:声明项目模块类型
type: "module"
:.js
视为 ESM,.cjs
视为 CommonJS- 当前项目使用 ESM,导入 npm 包时优先使用其
module
字段(ESM 入口)
type: "commonjs"
(默认):.js
视为 CommonJS,需通过 Babel 转换 ESM 语法- 当前项目使用 CommonJS
2. 第三方库的双模块支持:module vs main
-
main
:CommonJS 入口(传统项目默认) -
module
:ESM 入口(现代项目优先) -
browser
:浏览器使用包的入口文件 -
exports
:条件导出(如exports import: './esm/index.js', require: './cjs/index.js'
)
案例:Vant 库的 .mjs 设计
Vant 将 ESM 的 CSS 文件以 .mjs
格式导入,核心目的是:
- 显式标识 ESM:即使项目未配置
type: module
,.mjs
也会被 Node.js 视为 ESM,避免与 CommonJS 混淆; - 兼容多环境:浏览器直接支持 ESM 导入 CSS,Node.js 无需额外配置即可运行;
- 优化工具链:静态分析更精准,确保 Tree Shaking 剔除无效样式。
现代开发最佳实践
1. 新项目初始化策略
- 默认使用 ESM:配置
package.json
的type: "module"
,.js
文件统一使用import/export
; - 避免混合语法:CommonJS 文件使用
.cjs
后缀,逐步迁移至 ESM。
2. 处理第三方库兼容
-
优先选择双模块库(如 React、Vant),通过
module
字段加载 ESM 版本; -
兼容单模块库:
javascript
// ESM 中导入 CommonJS 并处理默认导出 import cjsLib from 'cjs-library'; // 等价于 const cjsLib = require('cjs-library').default;
3. 打包工具配置建议
-
Webpack:
module.exports = { mode: 'development', output: { type: 'module', // 浏览器环境输出 ESM }, resolve: { fallback: { 'path': require.resolve('path-browserify') } // 兼容 Node.js 内置模块 } };
-
Vite:自动兼容 ESM 与 CommonJS,无需额外配置。
4. 性能优化关键点
- Tree Shaking:仅 ESM 支持完整静态分析,建议移除项目中的 CommonJS 依赖;
- 动态导入:使用
import()
延迟加载非关键模块,提升首屏性能
未来趋势:ESM 的全面主导
- 浏览器兼容性:现代浏览器(Chrome 61+、Firefox 60+)已全面支持 ESM,Vue 3、React 18 等框架默认采用 ESM 打包;
- Node.js 演进:Node 20+ 计划默认启用 ESM,逐步淘汰 CommonJS 兼容性模式;
- 工具链升级:Deno、Bun 等新兴运行时仅支持 ESM,推动生态向标准化发展。
建议:新项目应果断采用 ESM,旧项目可通过 .mjs
/.cjs
后缀过渡,逐步迁移至统一模块规范。模块化的终极目标是「一次编写,多端运行」,而 ESM 正是实现这一目标的核心基础设施。
关键收获
- 在 package.json 中指定 type,只是对 node 运行时的限制,对 Webpack 构建没有影响,对 ESLint 语法检测工具也没有影响
- CommonJS 不存在默认导出的概念,需要通过 .default 显示访问
QA
1. 怎么确定项目是使用的什么模块呢,比如 ES6、CommonJS、UMD 等,包括 npm 包与普通前端项目
-
普通项目:
-
package.json 的 type 字段,
-
代码中的语法
-
UMD 同时支持 CommonJS、AMD 与全局变量,浏览器中如果 script 标签上添加
type="module"
,可以直接支持 import 与 export 语法,且是异步加载;传统脚本不支持这种语法; -
npm 包:
-
package.json 的导入入口字段:
-
ESM: module
-
CommonJS: main
-
UMD: main
-
如果同时提供 ESM 和 CommonJS 版本,打包工具会根据项目自行选择
-
浏览器中的引入方式:在浏览器中如果能够通过常规 script 标签引入,则为 UMD 或者全局库,如果只能通过
type="module"
引入,则为 ESM 库
2. 为什么在 ES6 模块中可以导入 CommonJS,但静态分析限制可能出问题
- 虽然 ES6 模块中能够导入 CommonJS,Node 在运行时也能自动转换,但是静态分析工具如 ESLint TypeScript 等无法完全模拟 Node.js 的动态转换逻辑
3. 为什么 ES Module 能够被现代打包工具优先识别呢,机制是什么
- ES Module 拥有更高的性能,能够方便进行 tree-shaking
4. 为什么在现代前端项目中也能使用 UMD 包呢,构建工具要如何打包,比如一个项目同时使用 CommonJS、ES6、UMD 的包它会怎么处理呢
- 历史过渡需求,核心都是统一转换为目标环境的格式
5. AMD 模块可以直接在浏览器中使用吗
- 不能,引入 AMD 实现库,如 RequireJS
6. 为什么建议新的项目使用 type: "module" 呢,有哪些好处?
- **标准化模块语法,消除兼容性割裂:**彻底统一了 Node.js 和浏览器的模块语法,减少跨环境开发的语法成本,尤其是对于全栈项目,可以让服务器端和浏览器端共享前端逻辑;无需兼容层
- **静态分析能力:**优化工具链和性能;原生支持 tree-shaking, CommonJS 是动态的,在运行时确定路径,无法实现完全静态分析,优化效果有限;更高效的模块解析,CJS 因 require 动态解析会导致性能损耗
- **支持现代语法特性:**顶层 await 与 默认严格语法
- **生态系统与长期兼容性:**Node.js 官方主推方向与更好的库支持
- **开发体验提升:**更简洁的文件后缀与更清晰的错误提示
- **性能与内存优化:**更快的模块加载,减少运行时动态分析的开销与内存占用更低
7. 哪些浏览器原生支持 ESM, 我看现在 vue3 打包后的项目默认就是 type=module,为什么呢,如果要兼容老浏览器是不是有相关的工具
- 现代浏览器原生都支持 ESM,Vue3 只支持现代浏览器,兼容老浏览器是有相关工具
8. 为什么 vant 库 ESM 的 css 文件要以 mjs 的方式导入呢,有什么好处吗?
- ESM 能够方便 TreeShaking,对于 CSS 文件也是如此
9. 为什么模块依赖中不要出现循环引用的问题?
- 循环引用会导致模块依赖分析出错,可能导致打包后的文件与预期不符