在前端工程里,模块化是把复杂逻辑拆成独立文件并互相引用的一套约定。CommonJS最早在服务端Node.js中落地,用require和module.exports沟通;ES6模块是语言标准,用import和export语法,被浏览器与原生环境共同支持。两者表面都是导出一个值、引入一个值,但底层加载机制、作用域处理、工具识别方式完全不同。

CommonJS规范基础
CommonJS的设计目标是让JavaScript拥有类似Python或Java的模块能力。每个文件就是一个模块,内部变量不会污染全局。模块通过module.exports向外暴露内容,外部用require同步读取。因为Node早期是本地文件系统,同步加载不会阻塞网络,体验很自然。
下面是一段典型的CommonJS代码,展示了如何导出函数与引入使用:
// math.js
function add(a, b) {
return a + b;
}
module.exports = { add };
// main.js
const { add } = require('./math.js');
console.log(add(1, 2));
这种写法在运行时才决定依赖关系。也就是说,代码执行到require那一行,才会去读文件、执行模块体、拿到exports对象。如果模块之间存在循环引用,CommonJS会拿到“未完成”的exports副本,需要开发者自己规避。它的优势是简单直观,配合Node生态海量包,几乎零学习成本。
ES6模块规范基础
ES6模块是ECMAScript 2015写入语言标准的官方方案。它使用import声明依赖,用export暴露接口。与CommonJS最大区别是:依赖关系在代码解析阶段就被静态确定,引擎不需要运行代码就知道谁引入了谁。
同样的逻辑用ES6模块书写如下:
// math.js
export function add(a, b) {
return a + b;
}
// main.js
import { add } from './math.js';
console.log(add(1, 2));
静态结构带来两个好处。一是浏览器可原生支持,用<script type="module">直接跑,不需打包;二是打包工具能精确知道哪些导出没有被使用,从而删除死代码。ES6模块默认是严格模式,导出的是绑定而非值副本,一处修改外部也能看到,更适合大型协作项目。
现代前端工具链中的差异
Webpack、Rollup、Vite等工具对两种规范处理路径不同。Webpack借助loader把ES6模块转成类似CommonJS的中间形式再分析依赖;Rollup原生偏好ES6,利用静态性做高效Tree Shaking;Vite开发时直接用浏览器原生ES6模块,生产环境用Rollup打包。
当项目中混用两种规范,工具必须做互操作。比如Webpack用@babel/preset-env把import转require,或用插件把CommonJS包模拟成ES6形状。这种转换有成本,也可能导致Tree Shaking失效,让最终包变大。
// 工具配置示例:Vite中排除某些CommonJS包
export default {
optimizeDeps: {
exclude: ['some-commonjs-lib']
}
};
从构建体积看,纯ES6模块项目更容易控制在几十KB内;而大量依赖CommonJS的古老包,常让构建产物多出几KB到几百KB不等。开发者在选库时应优先看是否提供ES6构建版本。
优势对比与选型建议
把两者放在一张表里更直观:
| 维度 | CommonJS | ES6模块 |
|---|---|---|
| 加载时机 | 运行时同步 | 编译时静态 |
| 循环依赖 | 拿到部分副本 | 绑定实时更新 |
| Tree Shaking | 难支持 | 原生支持 |
| 运行环境 | Node为主 | 浏览器与Node |
新项目建议默认使用ES6模块,享受静态分析与小体积。维护老Node服务或调用只发CommonJS的包时,再用require兜底。理解规范差异,能让你在配置Webpack或Vite时少做无用功,也更容易定位打包异常的根本原因。
简言之,CommonJS胜在历史和兼容,ES6模块胜在标准和效率,工具链的差异大多源于静态与动态这一根本分歧。
掌握这两套规范,不只是会写import和require,更是看清现代前端为什么能从几百KB优化到几十KB,以及为什么有些bug只在特定打包模式下出现。写模块时顺手选对规范,后期重构会轻松很多。
CommonJSES6_module前端工具链修改时间:2026-08-07 18:21:25