前端模块化:CommonJS 和 ES Module 到底有什么区别?
前端模块化:CommonJS 和 ES Module 到底有什么区别?
- 前言
- 一、为什么前端需要模块化?
- 二、四种模块规范速览
- 三、CommonJS
- 四、ES Module
- 五、CommonJS vs ES Module:四个核心区别
- 5.1 同步 vs 异步
- 5.2 值的拷贝 vs 值的引用(最容易踩坑)
- 5.3 动态加载能力
- 5.4 Tree Shaking 支持
- 六、AMD 与 UMD
- 6.1 AMD
- 6.2 UMD
- 七、总结
- 参考资源
前言
模块化是 JavaScript 绕不开的话题。从require到import,从值的拷贝到值的引用,这篇文章把模块化最核心的内容梳理了一遍。
先看两段代码:
// 代码 Aconstfs=require('fs')module.exports={name:'app'}// 代码 Bimportfsfrom'fs'exportconstname='app'这两段代码都在做同一件事——导入和导出模块,但写法完全不同。
工作中你可能两种都见过,甚至在一个项目里同时出现过。它们有什么区别?为什么会有两种写法?什么时候用哪个?
如果你正在被这些问题困扰,这篇文章或许能给你一些答案。
一、为什么前端需要模块化?
在模块化出现之前,JavaScript 代码是这样引入的:
<scriptsrc="jquery.js"></script><scriptsrc="utils.js"></script><scriptsrc="main.js"></script>这种方式的 3 个致命问题:
| 问题 | 说明 | 真实场景 |
|---|---|---|
| 🌍全局污染 | 所有变量都挂在window上 | 两个库都定义了version变量,后者覆盖前者 |
| 🔗依赖混乱 | 必须手动保证加载顺序 | main.js必须在jquery.js之后加载,否则报错 |
| 📦无法复用 | 代码难以在不同项目中共享 | 复制粘贴是常态,维护成本极高 |
模块化就是为解决这些问题的:每个文件有独立作用域、明确声明依赖、可以导出和导入。
二、四种模块规范速览
在开始之前,先对四种模块规范有个整体印象:
| 规范 | 全称 | 主要运行环境 | 加载方式 |
|---|---|---|---|
| CommonJS | CommonJS | Node.js | 同步 |
| ES Module | ECMAScript Module | 浏览器 / Node.js | 异步 |
| AMD | Asynchronous Module Definition | 浏览器(RequireJS) | 异步 |
| UMD | Universal Module Definition | 浏览器 / Node.js | 同步 |
日常开发中,CommonJS 和 ES Module 占据了绝大多数场景,AMD 和 UMD 只需要有个基本概念就行。
三、CommonJS
诞生背景:2009 年,Mozilla 工程师 Kevin Dangoor 发起了一个叫 ServerJS 的项目(后更名为 CommonJS),希望让 JavaScript 也能在服务器端开发。Node.js 选择了 CommonJS 作为其模块系统,从此require()和module.exports成为了服务端 JavaScript 的事实标准。
语法示例:
// 导入constfs=require('fs')constpath=require('path')// 导出方式一:module.exportsmodule.exports={name:'John',sayHello(){console.log('Hello')}}// 导出方式二:exportsexports.age=18核心特点:
| 特点 | 说明 |
|---|---|
| 加载方式 | 同步加载(阻塞式) |
| 执行时机 | 代码运行时执行 |
| 缓存机制 | 模块加载后会被缓存,多次 require() 返回同一对象 |
| 值传递 | 导出的是值的拷贝(浅拷贝) |
| 动态加载 | ✅ 支持条件 require() |
| Tree Shaking | ❌ 不支持 |
四、ES Module
诞生背景:2015 年,ECMA International 在 ES2015(ES6)规范中正式将模块系统纳入 JavaScript 语言标准。这是 JavaScript 语言层面的官方模块方案,也是未来的统一方向。
语法示例:
// 导入方式importfsfrom'fs'// 默认导入import{readFile}from'fs'// 命名导入import*asfsfrom'fs'// 全部导入// 导出方式exportconstname='John'// 命名导出exportfunctionsayHello(){}// 命名导出exportdefault{name:'John'}// 默认导出核心特点:
| 特点 | 说明 |
|---|---|
| 加载方式 | 浏览器异步 / Node.js 同步 |
| 执行时机 | 代码解析时确定依赖(静态分析) |
| 缓存机制 | 模块只执行一次,但 import 是引用 |
| 值传递 | 导出的是值的引用(动态绑定) |
| 动态加载 | ✅ 支持 import() |
| Tree Shaking | ✅ 支持(得益于静态结构) |
💡 快速分辨:看到require()、module.exports、exports.xxx就是 CommonJS;看到import、export、export default就是 ES Module。
五、CommonJS vs ES Module:四个核心区别
| 对比维度 | CommonJS | ES Module |
|---|---|---|
| 加载方式 | 同步(阻塞) | 异步(非阻塞) |
| 值传递 | 值的拷贝 | 值的引用 |
| 动态加载 | ✅ 支持条件 require() | ✅ 支持 import() |
| Tree Shaking | ❌ 不支持 | ✅ 支持 |
5.1 同步 vs 异步
// CommonJS:同步加载,会阻塞后续代码constdata=require('./data.json')console.log('等上面加载完才执行')// ES Module:异步加载,不阻塞渲染importdatafrom'./data.json'console.log('可能比数据先执行')5.2 值的拷贝 vs 值的引用(最容易踩坑)
// ========== CommonJS:值的拷贝 ==========// counter.jsletcount=0module.exports={count,increment:()=>count++}// main.jsconstcounter=require('./counter')counter.increment()console.log(counter.count)// 0 ❌ 不变(值的拷贝)// ========== ES Module:值的引用 ==========// counter.jsexportletcount=0exportconstincrement=()=>count++// main.jsimport{count,increment}from'./counter'increment()console.log(count)// 1 ✅ 变了(值的引用)5.3 动态加载能力
// CommonJS:支持条件 requireif(env==='prod'){constlogger=require('./logger-prod')}else{constlogger=require('./logger-dev')}// ES Module:顶层 import 必须静态importloggerfrom'./logger'// 只能写在顶层// ES Module:动态加载用 import()(返回 Promise)if(env==='prod'){constlogger=awaitimport('./logger-prod')}5.4 Tree Shaking 支持
// utils.jsexportfunctionused(){console.log('used')}exportfunctionunused(){console.log('unused')}// app.jsimport{used}from'./utils'// ES Module 打包后:unused() 被移除(Tree Shaking)// CommonJS:无法做到 Tree Shaking,因为无法静态分析哪些函数被使用了六、AMD 与 UMD
6.1 AMD
诞生背景:CommonJS 的 require() 是同步加载的,在 Node.js 服务器环境读取本地文件没有问题。但在浏览器里,模块文件需要通过网络请求获取,同步加载会卡住页面渲染。AMD(Asynchronous Module Definition)专门为浏览器设计,支持异步加载。
核心思路:先把所有依赖并行下载,全部下载完成后,再执行回调函数。
语法示例:
// 定义模块define(['jquery','lodash'],function($,_){return{doSomething:function(){// 使用 $ 和 _}}})// 使用模块require(['math'],function(math){console.log(math.add(2,3))})核心特点:
| 特点 | 说明 |
|---|---|
| 加载方式 | 异步加载(不阻塞渲染) |
| 依赖处理 | 先并行下载所有依赖,再执行回调 |
| 代表实现 | RequireJS |
| 使用场景 | 浏览器端 |
现状:新项目几乎不用了。只有在维护 5-10 年前用 RequireJS 搭建的老项目时才会遇到。
6.2 UMD
诞生背景:一个库的作者希望自己写的代码不管在什么环境下都能运行——可能是 Node.js(CommonJS),可能是浏览器(AMD),也可能就是直接加载一个<script>标签(全局变量)。UMD(Universal Module Definition)就是为了解决这个问题。
核心思路:UMD没有创造新的模块规范,它就是一个"环境检测器":上场后先环顾四周,判断当前是CommonJS环境、AMD环境、还是啥也没有的全局环境,然后自动选择最合适的加载方式。
语法示例(简化版):
// UMD 的核心思想:环境判断 + 适配if(typeofdefine==='function'&&define.amd){// AMD 环境define(['jquery'],factory)}elseif(typeofmodule==='object'&&module.exports){// CommonJS 环境module.exports=factory(require('jquery'))}else{// 浏览器全局环境window.MyLib=factory(window.jQuery)}核心特点:
| 特点 | 说明 |
|---|---|
| 运行环境 | 浏览器 + Node.js |
| 加载方式 | 同步 |
| 兼容性 | 自动判断环境,兼容 AMD、CommonJS、全局变量 |
| 使用场景 | 需要跨环境运行的第三方库 |
现状:新项目不会直接写UMD代码了。但你会在node_modules里看到很多第三方库的打包文件(比如lodash.min.js),它们为了兼容各种环境,最终被编译成了UMD格式——你正常用就行,构建工具会自动处理。
小结:AMD 和 UMD 在日常开发中基本用不到,但了解它们能帮你理解两件事:为什么浏览器需要异步加载模块(AMD),以及一个库为什么能在不同环境下都能运行(UMD)。
七、总结
新项目用ES Module,维护老项目用CommonJS;遇到AMD/UMD知道它们是干什么的就行。
核心要点
CommonJS:Node.js 的传统方案,
require()同步加载,导出的是值的拷贝ES Module:官方标准,
import异步加载,导出的是值的引用,支持Tree ShakingAMD:为了解决浏览器异步加载而生,现在新项目不推荐
UMD:万能适配器,让一个库能在各种环境下运行
参考资源
- MDN: JavaScript 模块 — ES Module 官方指南
- Node.js CommonJS 模块文档 — CommonJS 权威实现(英文)
- 阮一峰: CommonJS 规范 — CommonJS 中文解读
- AMD 规范仓库 (核心文档) — AMD 规范原文
- 阮一峰: ES6 Module 语法 — ES Module 中文教程
