ESM & CommonJS

6 阅读1分钟

模块化演进

早期 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.jsontype 时,.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.jsontype: "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. 为什么模块依赖中不要出现循环引用的问题?

  • 循环引用会导致模块依赖分析出错,可能导致打包后的文件与预期不符