JavaScript模块化发展过程中,CommonJS和ESM是两种应用最广泛的模块规范,前者是Node.js早期默认的模块方案,后者是ECMAScript官方标准模块规范。很多存量项目基于CommonJS开发,新功能又倾向于使用ESM,因此两者共存成为开发中的常见需求。

CommonJS与ESM的核心差异
要实现两者共存,首先需要明确两种规范的核心区别,避免因为特性差异导致兼容问题。
- 加载方式:CommonJS是运行时同步加载,ESM是编译时静态分析异步加载
- 导出方式:CommonJS通过
module.exports导出,ESM通过export关键字导出 - 导入方式:CommonJS使用
require导入,ESM使用import关键字导入 - 作用域:CommonJS模块有独立的
module、exports、require全局变量,ESM模块默认严格模式,没有这些顶层变量
Node.js环境下的共存方案
1. 通过package.json的type字段区分模块类型
Node.js从12.17.0版本开始正式支持ESM,默认情况下.js文件会被识别为CommonJS模块,若要在项目中同时使用两种规范,可以通过package.json的type字段设置默认模块类型,再通过文件扩展名区分不同规范的模块。
当type设置为module时,默认.js文件为ESM模块,CommonJS模块需要使用.cjs扩展名;当type未设置或设置为commonjs时,默认.js文件为CommonJS模块,ESM模块需要使用.mjs扩展名。
示例package.json配置:
{
"name": "module-demo",
"type": "module",
"version": "1.0.0"
}
此时项目中的utils.mjs是CommonJS模块,index.js是ESM模块,两者可以互相导入:
// utils.cjs CommonJS模块
const add = (a, b) => a + b;
module.exports = { add };
// index.js ESM模块
import { add } from './utils.cjs';
console.log(add(1, 2)); // 输出3
2. 使用动态import加载CommonJS模块
在ESM模块中,无法直接用静态import导入CommonJS模块,需要使用动态import()方法,该方法返回Promise,可以异步获取CommonJS模块的导出内容。
// ESM模块中导入CommonJS模块
const loadCommonJSModule = async () => {
const commonjsModule = await import('./utils.cjs');
console.log(commonjsModule.add(2, 3)); // 输出5
};
loadCommonJSModule();
3. 包导出字段配置兼容两种规范
如果是开发npm包,需要在package.json中配置exports字段,分别指定CommonJS和ESM的导出路径,让使用不同规范的项目都能正确导入。
{
"name": "my-utils",
"type": "module",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.cjs"
}
},
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.js"
}
上述配置中,import字段指定ESM项目的导入路径,require字段指定CommonJS项目的导入路径,main和module是兼容旧版工具的配置。
前端构建工具中的共存方案
1. Webpack中的处理
Webpack默认支持两种模块的解析,只需要在配置中明确模块解析规则即可。如果项目中同时存在两种规范的模块,Webpack会自动将ESM转换为CommonJS,或者根据target配置输出对应规范的代码。
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: require('path').resolve(__dirname, 'dist')
},
resolve: {
extensions: ['.js', '.cjs', '.mjs']
}
};
2. Vite中的处理
Vite默认优先使用ESM,对于CommonJS模块会在预构建阶段将其转换为ESM。如果项目中需要同时支持两种规范,只需要正常导入即可,Vite会自动处理兼容问题。
// Vite项目中导入CommonJS模块
const commonjsLib = require('some-commonjs-lib');
// 也可以混合使用ESM导入
import { esmFunc } from './esm-module.js';
共存时的注意事项
- CommonJS的
__dirname、__filename在ESM中不存在,需要使用import.meta.url配合path模块获取等效路径 - CommonJS的循环依赖处理方式与ESM不同,共存时尽量避免复杂的循环依赖场景
- 部分旧版工具不支持
.mjs和.cjs扩展名,需要提前确认工具链的兼容性 - 动态导入返回的是模块命名空间对象,CommonJS的默认导出会被挂载到
default属性上,需要注意取值方式
合理处理CommonJS和ESM的共存,既能复用存量CommonJS代码,又能享受ESM静态分析、 tree-shaking等优势,是过渡阶段项目的最优选择。
CommonJSESMJavaScript_模块化模块互操作修改时间:2026-07-06 00:21:29