—— 打通规范、工具链与现代架构的任督二脉
0 引言
模块化的本质是“分而治之 + 明确边界 + 可组合复用”。
JavaScript 从最初嵌入的“全局脚本”时代一路演进:
- 早期:全局变量横飞、依赖顺序需手动维护;
- 社区百花:CommonJS、AMD、UMD 各自维优;
- ESM 标准化:语言层面原生支持、构建工具深度优化;
- 远程/边端/微前端场景:揭示对加载时机、体积、隔离性的更高要求。
本文按照时间线 + 工程落地双主线,既讲原理也给实战示例,帮助你把碎片化知识串成系统。
1 原始阶段:Script 标签 + 命名空间
1.1 全局脚本模式
<script src="jquery.js"></script>
<script src="app.js"></script>
缺点:全局污染、依赖顺序、人肉维护。
1.2 IIFE / 揭示式模块模式
const utils = (function () {
function add(a, b) { return a + b }
return { add }
})()
console.log(utils.add(1, 2))
- 通过立即执行函数隔离私有变量。
- 仍依赖全局单例名
utils,易冲突。
1.3 命名冲突与依赖地狱
- 多人协作时难以保证唯一前缀。
- 依赖链一长就出现“谁先加载谁后加载”的 脚本加载地狱。
2 社区方案:CommonJS、AMD、UMD
| 维度 | CommonJS | AMD | UMD |
|---|---|---|---|
| 加载方式 | 同步(阻塞) | 异步(回调) | 同步 / 异步 |
| 运行环境 | Node | 浏览器 | 通吃 |
| 核心语法 |
require() / module.exports
|
define([], fn) |
内部判断 module.exports 是否存在 |
| 优缺点 | 简单直观,但浏览器需打包 | 网络并发好,回调繁琐 | 只是一层 |
2.1 CommonJS 示例
// math.js
exports.add = (a, b) => a + b
// app.js
const { add } = require('./math')
console.log(add(3, 4))
-
模块执行一次后即缓存;再次
require直接取缓存。 - 在浏览器需 Browserify/Webpack 打成 bundle。
2.2 AMD 示例
define(['jquery'], function ($) {
return function mount() {
$('#root').text('Hello AMD')
}
})
- 以 依赖数组 声明,自带异步加载。
- 为了解决浏览器中多个脚本并行加载的问题而诞生,但存在大量回调与配置繁琐的问题。
2.3 UMD
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory()
} else if (typeof define === 'function' && define.amd) {
define(factory)
} else {
root.myLib = factory()
}
})(this, function () { /* ... */ })
- 典型“同时支持 Node、AMD、全局”的包装。
- 本质还是“打补丁”,渐渐被 ESM 取代。
3 标准化新纪元:ESM
3.1 语法速览
// math.js
export function add(a, b) { return a + b }
export const PI = 3.14
// app.mjs
import { add, PI } from './math.js'
console.log(add(PI, 1))
-
静态结构:
import/export需位于顶层,编译期即可解析依赖图。
3.2 静态优势
- Tree‑Shaking:未被引用的导出在打包阶段可被删除;
- 声明提升:循环依赖时获取到是“引用”而非值拷贝。
3.3 浏览器原生加载
<script type="module" src="/main.js"></script>
- 默认 延迟执行,等同
defer; - 模块脚本默认启用 严格模式 + CORS。
3.4 Node.js ESM
-
package.json置"type":"module"或使用后缀.mjs。 -
import.meta.url、import.meta.resolve给出运行时信息。
3.5 CommonJS ↔ ESM 互操作
-
ESM → CJS:
import pkg from 'cjs-lib'实际拿到module.exports; -
CJS → ESM:
require('./esm-file.mjs')会返回一个 Promise。
⚠️注意“默认导出”差异:CJS 只有module.exports一个出口。
3.6 高级用法
动态导入
button.onclick = async () => {
const { lazy } = await import('./heavy.js')
lazy()
}
支持 代码分割 + 按需加载。
Top‑Level Await
// fetch-data.mjs
const data = await fetch('/api').then(r => r.json())
export default data
使“模块依赖异步资源”变得自然,但会 阻塞整个依赖图的评估。
Import Maps(浏览器端)
<script type="importmap">
{
"imports": {
"vue": "https://cdn.skypack.dev/vue@3.5.0"
}
}
</script>
允许在浏览器端指定模块路径映射,简化依赖管理。目前仍处于实验阶段,需浏览器支持。
3.7 循环依赖解析
- ESM 先创建“模块记录”再执行;相互引用时拿到 暂时未赋值的绑定。
4 构建工具与打包生态
4.1 Bundler 进化
| 年份 | 工具 | 特征 |
|---|---|---|
| 2011 | Browserify | CommonJS to IIFE |
| 2012 | Webpack | Loader & Plugin 生态、HMR |
| 2015 | Rollup | ESM 优化、极致 tree‑shaking |
| 2020 | Vite / ESBuild | Dev Server + ESM 原生 + 极快构建 |
| 2023 | Bun | JS Runtime + Bundler 一体 |
4.2 Tree‑Shaking & Scope‑Hoisting
- Rollup 静态分析导出使用情况;
-
Webpack
sideEffects:false+TerserPlugin; - Scope‑Hoisting 把多模块函数折叠成一个 IIFE,减少闭包开销。
4.3 输出格式
export default {
format: ['esm', 'cjs', 'umd'],
dts: true
}
- Library 场景通常 多格式发布;
- App 场景仅需浏览器友好的
esm+ polyfill 方案。
4.4 源映射与 Chunk 策略
-
//# sourceMappingURL=帮助线上错误定位到源码行; -
webpackChunkName注释或rollup.output.manualChunks手工拆包。
4.5 Monorepo / 多包管理
-
pnpm workspaces+nx/rush实现 Hoist 依赖 + 原子发布。
5 跑在云端与边缘的模块化
| 平台 | 特征 | 模块格式 |
|---|---|---|
| Deno | 原生 ESM、URL 导入 | ESM |
| Cloudflare Workers | V8 Isolate + ESM-only | ESM |
| Vercel Edge | 基于 V8 的无服务器 | ESM |
- Service Worker 支持ESM:
new Worker('./worker.js', { type:'module' })。 - HTTP/2 Push、Early Hints 搭配 ESM preload 减少白屏。
6 微前端与模块联邦
6.1 需求背景
- 多团队并行交付;
- 渐进式重构旧系统。
6.2 Webpack Module Federation
// host webpack.config.js
plugins:[
new ModuleFederationPlugin({
remotes: { shop: 'shop@https://cdn.xxx.com/remoteEntry.js' }
})
]
// remote app
module.exports = {
name:'shop', exposes:{ './Header':'./src/Header.tsx' }
}
运行时拉取远程 bundle 并注入共享依赖版本。
6.3 CDN + import() 方案
const Header = await import('https://unpkg.com/shop/Header.js')
无打包器耦合,更贴近浏览器原生。
6.4 共享依赖治理
-
singleton: true指定依赖只加载一次; - 版本不匹配降级到 fiber 沙箱或 iframe 隔离。
7 安全、性能与最佳实践
-
供应链安全
- Subresource Integrity:
<script src="..." integrity="sha384‑..."> - 锁文件审计:npm‑audit / snyk。
- Subresource Integrity:
-
Side Effects 字段
-
package.json里"sideEffects": false协助 tree‑shaking。
-
-
条件导入(Node ≥ 20)
import pkg from './lib/index.js' with { type:'json' } -
类型 & Lint
- TypeScript + JSDoc 双保险;
- ESLint
import/no‑cycle检测循环依赖。
-
性能指标
- 首屏 JS ≤ 100 kB、Chunk ≤ 50 kB;
- 长任务切分、动态 import 保持主线程流畅。
8 未来展望
| 提案 | 目标 | 进度 |
|---|---|---|
| Package Exports/Imports | 更安全的包入口映射 | Stage 4, 已被 Node 采纳 |
| Module Fragments | 内嵌 HTML 模块化 | Stage 2 |
| WASM Modules | JS ↔︎ Wasm 无缝导入 | 试验中 |
- 零打包时代:ESBuild/Bun 利用极快编译 + 浏览器原生 ESM。
9 结语
模块化是一条“控制复杂度”的主线:
从命名空间到 CommonJS,再到 ESM 与 Module Federation,每一次演进都在拉高抽象层,降低协作成本。
理解背后的 依赖解析、作用域隔离、加载策略,你才能在任何规模的项目中做出最优权衡。
至此,你已拥有 JavaScript 模块化全景地图。
未来无论构建多包库、落地微前端,抑或在 Edge Runtime 编写函数,你都能以最合适的模块方案驾轻就熟。
希望本文能帮助你更好地理解和应用 JavaScript 模块化的相关知识。
如有任何问题或建议,欢迎在评论区交流讨论!