15、前端模块化发展历史,CommonJS / AMD / ESM 的区别?

张开发
2026/4/19 0:22:32 15 分钟阅读

分享文章

15、前端模块化发展历史,CommonJS / AMD / ESM 的区别?
目录一、先建立认知为什么需要模块化二、模块化的演进历史阶段一原始时代——IIFE立即执行函数阶段二Node.js 带来了 CommonJS2009 年CommonJS 的特点为什么不适合浏览器阶段三浏览器端的异步方案——AMD2011 年AMD 的特点缺点阶段四另一条路——CMD2012 年AMD 和 CMD 的核心区别阶段五统一时代——UMD阶段六官方标准——ES Module2015 年三、CommonJS 和 ESM 的核心区别1. 静态 vs 动态CommonJS动态ESM静态2. 值的拷贝 vs 实时绑定CommonJS值的拷贝ESM实时绑定Live Binding3. 同步 vs 异步CommonJS同步ESM异步天然支持4. 运行时 vs 编译时四、各规范对比速查五、面试回答精彩模板版本一标准版1 分钟版本二进阶版更有深度六、整体知识结构梳理七、高分总结这道题考察的是你对前端工程化演进的整体认知。能把这段历史讲清楚说明你不只是会用工具还理解工具为什么存在。一、先建立认知为什么需要模块化早期的前端代码是这样的script srcjquery.js/script script srcutils.js/script script srcapp.js/script所有代码都在全局作用域里运行带来了三个核心问题全局污染任何文件都能修改全局变量变量冲突难以排查依赖混乱script 的加载顺序必须手动维护漏了或者顺序错了就报错无法复用代码组织全靠人工约定没有真正意义上的封装模块化要解决的就是这三个问题作用域隔离、依赖管理、代码复用。二、模块化的演进历史阶段一原始时代——IIFE立即执行函数在没有任何模块规范之前开发者用 IIFE 模拟模块var MyModule (function() { var privateVar 私有变量 // 外部访问不到 function privateMethod() {} return { publicMethod: function() { return privateVar } } })()解决了作用域隔离但没有解决依赖管理问题。你还是得手动保证 script 的加载顺序。阶段二Node.js 带来了 CommonJS2009 年Node.js 诞生服务端 JS 需要一套正式的模块规范CommonJS 应运而生。// 导出 module.exports { fn: function() {} } // 引入 const utils require(./utils)CommonJS 的特点同步加载require是同步的执行到这行代码时立刻加载文件动态导入路径可以是变量可以写在条件语句里值的拷贝导入的是值的拷贝模块内部修改不影响外部// 可以这样写 if (condition) { const mod require(someVariable) }为什么不适合浏览器同步加载在 Node.js 里没问题因为文件在本地磁盘读取很快。但在浏览器里文件需要通过网络请求获取同步加载意味着页面会卡死等文件下载完才能继续执行。阶段三浏览器端的异步方案——AMD2011 年为了解决浏览器里的模块化问题AMDAsynchronous Module Definition规范诞生代表实现是RequireJS。// 定义模块 define(myModule, [jquery, utils], function($, utils) { return { fn: function() {} } }) // 使用模块 require([myModule], function(myModule) { myModule.fn() })AMD 的特点异步加载依赖提前声明并行下载不阻塞页面依赖前置必须在define的数组里提前声明所有依赖专为浏览器设计缺点语法繁琐回调嵌套可读性差。依赖必须前置声明即使某些依赖只在特定条件下才需要也得一开始就全部加载。阶段四另一条路——CMD2012 年Sea.js 提出的 CMDCommon Module Definition规范是国内玉伯主导的方案。define(function(require, exports, module) { var $ require(jquery) // 就近引入用到才 require exports.fn function() {} })AMD 和 CMD 的核心区别AMDCMD代表RequireJSSea.js依赖声明提前声明提前执行就近声明延迟执行风格更像异步编程更像 CommonJSCMD 的理念更接近 CommonJS依赖可以在用到的地方再require写法更自然。但随着 Webpack 的崛起AMD 和 CMD 都逐渐退出了历史舞台。阶段五统一时代——UMDAMD 和 CommonJS 并存期间库的作者很头疼写 AMD 格式 Node.js 用不了写 CommonJS 格式 RequireJS 用不了。UMDUniversal Module Definition是一种兼容所有格式的写法(function(root, factory) { if (typeof module ! undefined module.exports) { // CommonJS module.exports factory() } else if (typeof define function define.amd) { // AMD define(factory) } else { // 全局变量 root.myLib factory() } }(this, function() { return {} }))判断当前运行环境自动选择对应格式。很多老牌库jQuery、Lodash 的旧版本都支持 UMD。阶段六官方标准——ES Module2015 年ES2015 终于把模块化纳入语言标准这就是 ES ModuleESM。// 导出 export function fn() {} export const name hello export default class MyClass {} // 导入 import { fn, name } from ./utils import MyClass from ./myClass import * as utils from ./utils // 命名空间导入三、CommonJS 和 ESM 的核心区别这是面试最高频的考点必须讲清楚。1. 静态 vs 动态CommonJS动态// 路径可以是变量 const path condition ? ./a : ./b const mod require(path) // 可以在函数里、条件里 require if (needModule) { const mod require(./mod) }ESM静态// 必须在顶层路径必须是字符串字面量 import { fn } from ./utils // ❌ 这样会报错 if (condition) { import { fn } from ./utils }这个区别至关重要正是因为 ESM 是静态的Webpack 才能在编译阶段分析依赖、实现 Tree Shaking。2. 值的拷贝 vs 实时绑定CommonJS值的拷贝// counter.js let count 0 module.exports { count, increment() { count } } // main.js const { count, increment } require(./counter) increment() console.log(count) // 还是 0拿到的是拷贝不是原始变量导入的是模块导出对象的快照后续模块内部的修改不会同步过来。ESM实时绑定Live Binding// counter.js export let count 0 export function increment() { count } // main.js import { count, increment } from ./counter increment() console.log(count) // 1实时反映模块内部的变化ESM 导入的是绑定不是值本身始终指向模块内部的最新值。3. 同步 vs 异步CommonJS同步// 执行到这行立即加载阻塞后续代码 const mod require(./mod)ESM异步天然支持ESM 支持顶层 await和异步加载不会阻塞// 动态 import返回 Promise const mod await import(./mod)4. 运行时 vs 编译时CommonJSESM依赖解析时机运行时编译时是否支持 Tree Shaking不支持支持循环依赖处理部分支持可能得到未完成的对象更完善实时绑定四、各规范对比速查规范年份环境加载方式代表工具IIFE早期浏览器同步无CommonJS2009Node.js同步Node.jsAMD2011浏览器异步RequireJSCMD2012浏览器异步/延迟Sea.jsUMD2014通用兼容以上各大库ESM2015通用静态/异步现代浏览器、Node.js五、面试回答精彩模板版本一标准版1 分钟前端模块化的演进是由问题驱动的。最早没有模块化全局变量污染和依赖管理混乱是核心痛点。Node.js 带来了 CommonJS解决了服务端的模块化问题但它是同步加载的不适合浏览器网络环境。为了浏览器端的异步加载AMD 和 CMD 先后出现AMD 依赖前置、CMD 就近引入但语法都比较繁琐最终随着 Webpack 的崛起退出了历史舞台。2015 年 ES2015 把模块化纳入语言标准ESM 成为官方规范。CommonJS 和 ESM 最核心的区别有两点一是 CommonJS 动态、ESM 静态ESM 的静态结构让 Webpack 能做 Tree Shaking二是 CommonJS 导出的是值的拷贝ESM 导出的是实时绑定内部变量修改后外部能感知到。版本二进阶版更有深度前端模块化的演进本质上是在不同阶段的技术约束下不断解决依赖管理和作用域隔离问题的过程。最早期靠 IIFE 实现作用域隔离但没有解决依赖顺序问题。2009 年 Node.js 带来了 CommonJS语法简洁require/module.exports到现在还在大量使用。但 CommonJS 是同步的浏览器通过网络加载文件时同步等待会导致页面卡死所以不适合浏览器端。为了解决这个问题AMDRequireJS采用异步加载 依赖前置的方案CMDSea.js则采用就近引入的方式更接近 CommonJS 风格。这两者在 Webpack 崛起后逐渐式微。ESM 是 2015 年纳入语言标准的官方方案和 CommonJS 有两个本质区别第一ESM 是静态的import/export必须在顶层、路径必须是字面量这让打包工具在编译阶段就能分析依赖图实现 Tree ShakingCommonJS 是动态的require可以在任何地方执行只有运行时才能确定依赖关系。第二CommonJS 导出的是值的拷贝模块内部修改不影响外部ESM 导出的是实时绑定始终反映模块内部的最新状态这在处理循环依赖时表现也更好。现在的最佳实践是Node.js 项目逐渐向 ESM 迁移浏览器端项目用 ESM 编写交给 Webpack 或 Vite 打包处理兼容性。六、整体知识结构梳理前端模块化演进 │ ├── 远古时代 │ └── IIFE → 解决作用域不解决依赖 │ ├── 服务端方案 │ └── CommonJS2009→ 同步、动态、值拷贝 │ ├── 浏览器端方案 │ ├── AMD2011→ 异步、依赖前置RequireJS │ └── CMD2012→ 异步、就近引入Sea.js │ ├── 兼容方案 │ └── UMD → 自动判断环境兼容以上所有 │ └── 官方标准 └── ESM2015→ 静态、实时绑定、支持 Tree Shaking CommonJS vs ESM 核心区别 ├── 动态 vs 静态能否 Tree Shaking 的根本 ├── 值拷贝 vs 实时绑定 └── 同步 vs 天然支持异步七、高分总结前端模块化的演进是从无规范靠约定到有标准靠语言的过程每一个规范都是在解决上一个阶段遗留的问题。面试里想答得精彩不要只背规范名字要能讲清楚为什么 CommonJS 不能用在浏览器同步加载的本质问题AMD 和 CMD 的核心区别依赖前置 vs 就近引入CommonJS 和 ESM 最本质的两个区别静态 vs 动态、值拷贝 vs 实时绑定ESM 的静态结构为什么对 Tree Shaking 至关重要把这道题和上一道题串起来把模块化演进和 Tree Shaking、Vite 原理串联起来讲就能体现出你对整个前端工程化体系的系统理解。

更多文章