前端模块化
前言
javascript
┌─────────────────────────────────────────────────────────────────────────────┐
│ 知识体系递进关系 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 第一章:为什么需要模块化? │
│ └── 问题背景、历史演进 │
│ │ │
│ ▼ │
│ 第二章:模块化规范有哪些? │
│ └── CommonJS、AMD/UMD、ESM 语法与特性对比 │
│ │ │
│ ▼ │
│ 第三章:ESM 如何工作?(官方标准深入) │
│ └── 三阶段加载、静态特性、实时绑定、循环依赖 │
│ │ │
│ ▼ │
│ 第四章:为什么需要构建工具? │
│ └── ESM 的局限性、Webpack 的价值定位 │
│ │ │
│ ▼ │
│ 第五章:Webpack 构建原理 │
│ └── 构建流程、Module/Chunk/Bundle、模块包裹机制 │
│ │ │
│ ▼ │
│ 第六章:Webpack 运行时机制 │
│ └── __webpack_require__、异步加载、JSONP 回调 │
│ │ │
│ ▼ │
│ 第七章:优化策略与最佳实践 │
│ └── Tree Shaking、代码分割、模块输出格式 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
第一章:模块化的历史演进与问题背景
本章解决的问题:为什么 JavaScript 需要模块化?模块化是如何一步步发展的?
1.1 无模块时代的痛点(1995-2009)
JavaScript 诞生之初并没有模块系统,所有代码共享全局作用域:
javascript
// a.js
var name = "moduleA";
function helper() { /*...*/ }
// b.js
var name = "moduleB"; // 💥 覆盖了 a.js 的 name!
function helper() { /*...*/ } // 💥 覆盖了 a.js 的 helper!
// index.html - 必须手动管理加载顺序
<script src="a.js"></script>
<script src="b.js"></script>
核心问题:
- 命名冲突:全局变量相互覆盖
- 依赖管理:手动维护 script 标签顺序
- 按需加载:无法实现,所有代码一次性加载
早期解决方案:IIFE + 命名空间
javascript
var MyApp = MyApp || {};
MyApp.moduleA = (function () {
var privateVar = "private";
return {
publicMethod: function () { /*...*/ },
};
})();
1.2 模块化演进时间线
yaml
┌─────────────────────────────────────────────────────────────────────────────┐
│ JavaScript 模块化演进史 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1995 2009 2011 2015 2017 现在 │
│ │ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ ▼ │
│ 无模块 → CommonJS → AMD/UMD → ES Modules → Node支持ESM → ESM成为主流 │
│ │ │ │ │ │ │ │
│ 全局变量 Node.js 浏览器异步 语言标准 生态统一 构建工具 │
│ 命名冲突 服务端模块 加载需求 静态分析 双格式并存 深度优化 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| 阶段 | 时间 | 规范 | 背景 | 特点 |
|---|---|---|---|---|
| 1 | 2009 | CommonJS | Node.js 诞生,服务端需要模块系统 | 同步加载,运行时解析 |
| 2 | 2011 | AMD/UMD | 浏览器需要异步加载(网络延迟) | 异步加载,兼容多环境 |
| 3 | 2015 | ES Modules | JavaScript 语言层面的官方标准 | 静态分析,编译时确定 |
| 4 | 2017+ | 生态统一 | Node.js 12+ 原生支持 ESM | ESM + CJS 双格式并存 |
第二章:模块化规范详解
本章解决的问题:三大模块规范(CommonJS、AMD/UMD、ESM)各有什么语法和特性?如何选择?
2.1 规范对比总览
| 特性 | CommonJS | AMD | UMD | ESM |
|---|---|---|---|---|
| 设计目标 | 服务端 | 浏览器异步 | 通用兼容 | 语言标准 |
| 加载时机 | 运行时 | 运行时 | 运行时 | 编译时 |
| 加载方式 | 同步 | 异步 | 依环境 | 异步(可同步) |
| 导出类型 | 值拷贝 | 值拷贝 | 值拷贝 | 引用绑定 |
| 静态分析 | ❌ | ❌ | ❌ | ✅ |
| Tree Shaking | ❌ | ❌ | ❌ | ✅ |
2.2 CommonJS 详解
语法:
javascript
// 导出
module.exports = { a: 1, b: 2 };
// 或
exports.a = 1;
exports.b = 2;
// 导入
const lib = require('./lib');
const { a, b } = require('./lib');
运行时特性(动态):
javascript
// ✅ 条件导入
if (process.env.NODE_ENV === 'production') {
module.exports = require('./prod.js');
} else {
module.exports = require('./dev.js');
}
// ✅ 动态路径
const name = 'utils';
const utils = require(`./${name}.js`);
// ✅ 循环中导入
['a', 'b', 'c'].forEach(name => {
modules[name] = require(`./${name}.js`);
});
值拷贝特性:
javascript
// counter.js
let count = 0;
module.exports = {
count,
increment: () => count++,
};
// index.js
const counter = require('./counter');
console.log(counter.count); // 0
counter.increment();
console.log(counter.count); // 0 ← 还是 0!值拷贝!
2.3 UMD 详解(通用模块定义)
UMD 的目标是一份代码,多环境运行:
javascript
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD 环境 (RequireJS)
define(['dependency'], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS 环境 (Node.js)
module.exports = factory(require('dependency'));
} else {
// 浏览器全局变量
root.MyLibrary = factory(root.Dependency);
}
})(typeof self !== 'undefined' ? self : this, function (dependency) {
return {
doSomething: function () {
console.log('Hello from UMD!');
},
};
});
环境判断流程:
objectivec
┌──────────────────────┐
│ typeof exports === │
│ "object" && │─── YES ──→ CommonJS2: module.exports = ...
│ typeof module === │
│ "object" │
└──────────┬───────────┘
│ NO
▼
┌──────────────────────┐
│ typeof define === │
│ "function" && │─── YES ──→ AMD: define([], factory)
│ define.amd │
└──────────┬───────────┘
│ NO
▼
┌──────────────────────┐
│ typeof exports === │─── YES ──→ CommonJS: exports["name"] = ...
│ "object" │
└──────────┬───────────┘
│ NO
▼
浏览器全局变量: root["name"] = ...
2.4 ESM 详解(官方标准)
导出语法:
javascript
// ─────────── 命名导出 ───────────
export const name = 'ESM';
export function greet() { /*...*/ }
export class Person { /*...*/ }
// 批量导出
const a = 1, b = 2;
export { a, b };
// 重命名导出
export { a as aliasA, b as aliasB };
// ─────────── 默认导出 ───────────
export default function() { /*...*/ }
export default class { /*...*/ }
// ─────────── 聚合导出(re-export)───────────
export { foo, bar } from './other.js';
export * from './utils.js';
export * as utils from './utils.js'; // 命名空间聚合
导入语法:
javascript
// ─────────── 命名导入 ───────────
import { name, greet } from './module.js';
import { name as aliasName } from './module.js';
// ─────────── 默认导入 ───────────
import MyDefault from './module.js';
// ─────────── 混合导入 ───────────
import MyDefault, { name, greet } from './module.js';
// ─────────── 命名空间导入 ───────────
import * as Module from './module.js';
// ─────────── 副作用导入 ───────────
import './polyfill.js'; // 只执行,不导入任何值
// ─────────── 动态导入 ───────────
const module = await import('./module.js');
静态结构限制(与 CommonJS 的关键区别):
javascript
// ❌ ESM 不允许 - 动态导入路径
import { foo } from getModulePath(); // SyntaxError
// ❌ ESM 不允许 - 条件导入
if (condition) {
import { bar } from './bar.js'; // SyntaxError
}
// ✅ CommonJS 允许 - 完全动态
const mod = require(getModulePath());
if (condition) {
const bar = require('./bar.js');
}
第三章:ESM 工作原理深入
本章解决的问题:ESM 作为官方标准,它的加载机制是什么?静态特性和实时绑定是如何实现的?
3.1 ESM 三阶段加载过程
ESM 的加载过程分为三个完全独立的阶段:
scss
┌─────────────────────────────────────────────────────────────────┐
│ 时机划分 │
├──────────────────┬──────────────────────────────────────────────┤
│ │ │
│ 编译时/加载时 │ ① 构建 (Construction) - 解析、发现依赖 │
│ (静态分析) │ ② 实例化 (Instantiation) - 分配内存、连接绑定│
│ │ │
├──────────────────┼──────────────────────────────────────────────┤
│ │ │
│ 运行时 │ ③ 求值 (Evaluation) - 执行代码、填充导出值 │
│ (代码执行) │ │
│ │ │
└──────────────────┴──────────────────────────────────────────────┘
3.1.1 构建阶段 (Construction)
sql
┌─────────────────────────────────────────────────────────────┐
│ 构建阶段 │
├─────────────────────────────────────────────────────────────┤
│ 1. 解析模块说明符 (Module Specifier) │
│ './utils.js' → 'file:///project/utils.js' │
│ │
│ 2. 获取模块文件 (Fetch) │
│ - 浏览器: HTTP 请求 │
│ - Node.js: 文件系统读取 │
│ │
│ 3. 解析为模块记录 (Module Record) │
│ - 静态分析 import/export 语句 │
│ - 不执行任何代码 │
│ - 构建模块依赖图 │
└─────────────────────────────────────────────────────────────┘
关键点:
- 每个模块只会被解析一次,结果缓存在 Module Map 中
- 所有依赖通过深度优先遍历被发现和加载
- 此阶段完全是静态分析,不执行任何 JavaScript 代码
javascript
// 模块记录 (Module Record) 的简化结构
{
[[RequestedModules]]: ['./dep1.js', './dep2.js'], // 依赖列表
[[ImportEntries]]: [...], // 导入条目
[[ExportEntries]]: [...], // 导出条目
[[Status]]: 'unlinked', // 模块状态
[[EvaluationError]]: null // 执行错误
}
3.1.2 实例化阶段 (Instantiation / Linking)
arduino
┌─────────────────────────────────────────────────────────────┐
│ 实例化阶段 │
├─────────────────────────────────────────────────────────────┤
│ 1. 为每个模块分配内存空间 (Module Environment Record) │
│ │
│ 2. 创建导出绑定 (Export Bindings) │
│ - 在内存中为所有 export 创建"槽位" │
│ - 此时槽位未初始化 (uninitialized) │
│ │
│ 3. 连接导入导出 (Linking) │
│ - import 直接指向 export 的内存地址 │
│ - 这就是"实时绑定" (Live Binding) │
└─────────────────────────────────────────────────────────────┘
3.1.3 求值阶段 (Evaluation)
scss
┌─────────────────────────────────────────────────────────────┐
│ 求值阶段 │
├─────────────────────────────────────────────────────────────┤
│ 1. 按照深度优先、后序遍历的顺序执行模块代码 │
│ (先执行依赖模块,再执行当前模块) │
│ │
│ 2. 填充导出槽位的实际值 │
│ │
│ 3. 每个模块只执行一次,结果被缓存 │
└─────────────────────────────────────────────────────────────┘
执行顺序示例:
javascript
// main.js
import { b } from './b.js';
import { a } from './a.js';
console.log('main');
// a.js
console.log('a');
export const a = 'A';
// b.js
import { a } from './a.js';
console.log('b');
export const b = 'B';
// 执行顺序: a → b → main
// 输出: 'a', 'b', 'main'
3.2 静态结构特性
静态分析 :在代码执行前(编译阶段),仅通过分析代码文本结构,就能确定所有模块依赖关系。
javascript
┌─────────────────────────────────────────────────────────────────┐
│ 静态 vs 动态 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ESM 静态分析(编译时) │
│ ───────────────────── │
│ import { add } from './math.js'; │
│ ↓ │
│ 编译器读取代码文本 → 看到 import 语句 → 知道依赖 math.js │
│ ↓ │
│ 无需执行代码,100% 确定依赖关系 │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ CommonJS 动态分析(运行时) │
│ ───────────────────────── │
│ const math = require(getPath()); │
│ ↓ │
│ 必须执行 getPath() 才知道路径是什么 │
│ ↓ │
│ 编译时无法确定依赖! │
│ │
└─────────────────────────────────────────────────────────────────┘
静态结构的优势:
scss
┌─────────────────────────────────────────────────────────────┐
│ 静态结构的优势 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Tree Shaking (摇树优化) │
│ ├── 在构建时分析哪些导出被使用 │
│ └── 移除未使用的代码 (Dead Code Elimination) │
│ │
│ 2. 更快的查找 │
│ ├── 变量查找在编译时确定 │
│ └── 无需运行时的动态查找 │
│ │
│ 3. 类型检查 │
│ ├── TypeScript 可在编译时验证导入 │
│ └── IDE 提供准确的自动补全 │
│ │
│ 4. 循环依赖处理更可靠 │
│ └── 静态分析可以提前发现问题 │
│ │
└─────────────────────────────────────────────────────────────┘
3.3 实时绑定 (Live Binding)
ESM 最重要的特性之一,与 CommonJS 的值拷贝形成鲜明对比:
javascript
// ============ ESM: 实时绑定 ============
// counter.mjs
export let count = 0;
export function increment() { count++; }
// main.mjs
import { count, increment } from './counter.mjs';
console.log(count); // 0
increment();
console.log(count); // 1 ← 看到了变化!
// ============ CommonJS: 值拷贝 ============
// counter.cjs
let count = 0;
module.exports = {
count,
increment() { count++; }
};
// main.cjs
const { count, increment } = require('./counter.cjs');
console.log(count); // 0
increment();
console.log(count); // 0 ← 还是 0!(拷贝的值)
绑定原理图解:
yaml
┌────────────────── ESM 实时绑定 ──────────────────┐
│ │
│ 导出模块内存 导入模块 │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ count: [ref]─┼───────────┼─► count │ │
│ │ │ │ │ │ │
│ │ ▼ │ │ │ │
│ │ ┌───┐ │ │ │ │
│ │ │ 0 │ │ 同一内存 │ │ │
│ │ └───┘ │ │ │ │
│ └──────────────┘ └──────────────┘ │
│ │
└──────────────────────────────────────────────────┘
┌────────────────── CJS 值拷贝 ───────────────────┐
│ │
│ 导出模块 导入模块 │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ count: ───┐ │ │ count: ───┐ │ │
│ │ │ │ 拷贝操作 │ │ │ │
│ │ ▼ │ ═══════► │ ▼ │ │
│ │ ┌───┐ │ │ ┌───┐ │ │
│ │ │ 0 │ │ │ │ 0 │ │ │
│ │ └───┘ │ │ └───┘ │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ 独立内存 独立内存 │
└──────────────────────────────────────────────────┘
导入是只读的:
javascript
// module.js
export let value = 1;
// main.js
import { value } from './module.js';
value = 2; // TypeError: Assignment to constant variable
// 导入的绑定是只读的!
// 但可以修改导入对象的属性
import { obj } from './module.js';
obj.prop = 'new'; // ✅ 这是允许的
3.4 循环依赖处理
使用 const/let 声明 ------ 抛出 ReferenceError
javascript
// a.mjs
console.log('a.mjs 开始执行');
import { b } from './b.mjs';
console.log('在 a.mjs 中, b =', b);
export const a = 'a 的值';
// b.mjs
console.log('b.mjs 开始执行');
import { a } from './a.mjs';
console.log('在 b.mjs 中, a =', a); // ❌ ReferenceError!
export const b = 'b 的值';
// 执行 node a.mjs
// 输出:
// b.mjs 开始执行
// ReferenceError: Cannot access 'a' before initialization
//
// 原因:const/let 存在暂时性死区 (TDZ),在初始化前访问会报错
使用 var 声明 ------ 得到 undefined
javascript
// a.mjs
import { b } from './b.mjs';
export var a = 'a 的值'; // 使用 var
// b.mjs
import { a } from './a.mjs';
console.log('在 b.mjs 中, a =', a); // undefined (var 会提升)
export var b = 'b 的值';
使用函数声明 ------ 正常工作
javascript
// a.mjs
import { b } from './b.mjs';
console.log('在 a.mjs 中, b() =', b());
export function a() { return 'a 的值'; } // 函数声明会提升
// b.mjs
import { a } from './a.mjs';
console.log('在 b.mjs 中, a() =', a()); // ✅ 正常工作!
export function b() { return 'b 的值'; }
循环依赖执行流程:
less
执行 a.mjs:
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. 解析 a.mjs,发现依赖 b.mjs │
│ 2. 解析 b.mjs,发现依赖 a.mjs (循环!) │
│ 3. a.mjs 已在处理中,跳过 (使用未初始化的绑定) │
│ 4. 执行 b.mjs,此时 a 的导出槽位: │
│ - const/let: 处于 TDZ → 访问抛出 ReferenceError │
│ - var: 已提升但未赋值 → undefined │
│ - function: 完整提升 → 可正常调用 │
│ 5. b.mjs 执行完毕(如果没有报错),b 的导出槽位被填充 │
│ 6. 回到 a.mjs 继续执行 │
│ 7. a.mjs 执行完毕,a 的导出槽位被填充 │
└─────────────────────────────────────────────────────────────┘
避免循环依赖问题的方法:
javascript
// ✅ 方法1: 使用函数延迟访问
// a.mjs
import { getB } from './b.mjs';
export const a = 'a';
export function getA() { return a; }
console.log(getB()); // 在函数调用时,b 已初始化
// b.mjs
import { getA } from './a.mjs';
export const b = 'b';
export function getB() { return b; }
// ✅ 方法2: 将共享状态提取到第三个模块
// shared.mjs
export const shared = { a: null, b: null };
// a.mjs
import { shared } from './shared.mjs';
shared.a = 'a';
// b.mjs
import { shared } from './shared.mjs';
shared.b = 'b';
3.5 ESM 在不同环境中的实现
浏览器中的 ESM
html
<!-- 使用 type="module" -->
<script type="module">
import { func } from './module.js';
func();
</script>
<!-- 外部模块 -->
<script type="module" src="./main.js"></script>
<!-- 模块特性 -->
<script type="module">
// 1. 默认 defer - 不阻塞 HTML 解析
// 2. 默认 strict mode
// 3. 顶层 this 是 undefined
// 4. 支持顶层 await (ES2022)
// 5. 同源策略 - 跨域需要 CORS
</script>
<!-- 兼容降级 -->
<script type="module" src="modern.js"></script>
<script nomodule src="legacy.js"></script>
Node.js 中的 ESM
javascript
// 方式1: 使用 .mjs 扩展名
// utils.mjs
export function helper() {}
// 方式2: package.json 中设置 "type": "module"
// package.json
{
"type": "module" // 所有 .js 文件视为 ESM
}
// 方式3: .cjs 扩展名强制 CommonJS
// legacy.cjs - 即使 type: module,也是 CommonJS
// Node.js 特有的导入方式
import fs from 'node:fs';
import { readFile } from 'node:fs/promises';
// import.meta 对象
console.log(import.meta.url); // file:///path/to/module.mjs
console.log(import.meta.dirname); // /path/to (Node 20.11+)
console.log(import.meta.filename); // /path/to/module.mjs
第四章:为什么需要构建工具?
本章解决的问题:ESM 已经是官方标准了,为什么还需要 Webpack 这样的构建工具?
4.1 前端工程化的痛点
虽然 ESM 解决了模块化的语法标准问题,但前端开发还面临更多挑战:
sql
┌─────────────────────────────────────────────────────────────────────────┐
│ 前端模块化的痛点 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 模块规范混乱 2. 浏览器兼容性 3. 资源类型多样 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ CommonJS │ │ ES Module │ │ .js .css │ │
│ │ AMD / UMD │ │ 浏览器支持 │ │ .png .svg │ │
│ │ ES Module │ │ 有限 │ │ .json .wasm │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 4. 开发效率 5. 性能优化 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 热更新 │ │ 代码分割 │ │
│ │ Source Map │ │ Tree Shaking│ │
│ └─────────────┘ │ 压缩混淆 │ │
│ └─────────────┘ │
│ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ Webpack │ │
│ │ 统一解决方案 │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
4.2 Webpack 的价值定位
Webpack 本质:一个静态模块打包器,以 entry 为起点,递归构建依赖图,将所有模块打包成浏览器可运行的 Bundle。
| 问题 | ESM 原生能力 | Webpack 解决方案 |
|---|---|---|
| 模块规范混乱 | 只支持 ESM | 统一处理 CJS/AMD/ESM |
| 浏览器兼容 | 需要现代浏览器 | 转译为兼容代码 |
| 非 JS 资源 | 不支持 | Loader 处理任意类型 |
| 性能优化 | 无 | Tree Shaking、代码分割 |
| 开发体验 | 无 | HMR、Source Map |
4.3 构建工具链全景
javascript
┌─────────────────────────────────────────────────────────────────────────────┐
│ 现代构建工具处理流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 源代码 (ES6+ / ESM) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Babel-loader │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ • 转换 ES6+ 语法(async/await, class, 箭头函数等) │ │
│ │ • 保留 import/export 语法(modules: false)← 关键! │ │
│ │ • 转换 JSX、TypeScript 等 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Webpack │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ 1. 解析入口文件,构建依赖图 │ │
│ │ 2. 将 import/export 转换为 __webpack_require__ │ │
│ │ 3. 标记未使用的导出(usedExports) │ │
│ │ 4. 代码分割(动态 import → 单独 chunk) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Terser │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ • 删除标记为未使用的代码(Tree Shaking 完成) │ │
│ │ • 压缩变量名、移除空白、内联简单函数 │ │
│ │ • 删除 console.log(可配置) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 最终 Bundle(优化后的 ES5 代码) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
第五章:Webpack 构建原理
本章解决的问题:Webpack 内部是如何工作的?构建流程是怎样的?
5.1 三大核心角色
| 角色 | 职责 | 类比 |
|---|---|---|
| Compiler | 编译器,全局单例,贯穿整个生命周期 | 总指挥 |
| Compilation | 单次编译过程,包含模块、依赖、Chunk 等 | 一次构建任务 |
| Module | 文件的抽象,每个源文件对应一个 Module | 构建的最小单位 |
5.2 核心概念:Module、Chunk、Bundle
| 概念 | 定义 | 生命阶段 |
|---|---|---|
| Module | 源文件的抽象,Webpack 处理的最小单位 | Make 阶段 |
| Chunk | 多个 Module 的集合,打包的中间态 | Seal 阶段 |
| Bundle | Chunk 经过处理后输出的最终文件 | Emit 阶段 |
css
┌─────────────────────────────────────────────────────────────────────────┐
│ 三者关系:一对多 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Module (源文件) Chunk (中间态) Bundle (产物) │
│ ┌─────────┐ │
│ │ a.js │─┐ │
│ └─────────┘ │ ┌─────────────────┐ │
│ ┌─────────┐ ├─────────────→│ main chunk │────────→ main.js │
│ │ b.js │─┤ └─────────────────┘ │
│ └─────────┘ │ │
│ ┌─────────┐─┘ │
│ │ c.css │ │
│ └─────────┘ │
│ │
│ ┌─────────┐ ┌─────────────────┐ │
│ │ lodash │───────────────→│ vendor chunk │────────→ vendor.js │
│ └─────────┘ └─────────────────┘ │
│ │
│ ┌─────────┐ ┌─────────────────┐ │
│ │ lazy.js │───────────────→│ async chunk │────────→ lazy.js │
│ └─────────┘ └─────────────────┘ │
│ │
│ 关系:N Module → 1 Chunk → 1 Bundle │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Chunk 的三种产生方式:
| 产生方式 | 触发条件 | 示例 |
|---|---|---|
| Entry Chunk | 每个 entry 配置 | entry: { main: './src/index.js' } |
| Async Chunk | 动态导入 import() |
import('./lazy.js') |
| Split Chunk | SplitChunks 配置提取公共模块 | splitChunks: { chunks: 'all' } |
5.3 完整构建流程
┌─────────────────────────────────────────────────────────────────────────────┐
│ Webpack 构建全流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────┐ │
│ │Initialize│───→│ Make │───→│ Seal │───→│ Optimize │───→│ Emit │ │
│ │ 初始化 │ │ 构建 │ │ 封装 │ │ 优化 │ │ 输出 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └───────┘ │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────┐ │
│ │合并配置 │ │从Entry │ │形成Chunk │ │SplitChunks│ │写入 │ │
│ │创建Compiler│ │递归解析 │ │建立映射 │ │TreeShaking│ │文件 │ │
│ │注册Plugin │ │执行Loader│ │生成代码 │ │压缩混淆 │ │系统 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └───────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.3.1 Initialize 阶段
javascript
// 核心任务
// 1. 合并配置(命令行 + 配置文件 + 默认值)
const options = merge(defaultConfig, userConfig, cliConfig);
// 2. 创建 Compiler 实例(全局单例)
const compiler = new Compiler(options);
// 3. 注册所有插件
for (const plugin of options.plugins) {
plugin.apply(compiler); // 插件通过 apply 方法注入 hooks
}
// 4. 触发 environment 钩子
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
5.3.2 Make 阶段(核心:构建 ModuleGraph)
javascript
┌─────────────────────────────────────────────────────────────────────────┐
│ Make 阶段详解 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Entry Point 递归处理流程 │
│ ┌─────────┐ │
│ │index.js │ │
│ └────┬────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Step 1: 创建 Module │ │
│ │ const module = new NormalModule({ │ │
│ │ request: './src/index.js', │ │
│ │ type: 'javascript/auto', │ │
│ │ loaders: [babel-loader, ...] │ │
│ │ }); │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Step 2: Loader 转换 │ │
│ │ source = runLoaders(loaders, originalSource); │ │
│ │ │ │
│ │ // Loader 链式调用(从右到左) │ │
│ │ // sass-loader → css-loader → style-loader │ │
│ │ // ts-loader → babel-loader │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Step 3: AST 解析,提取依赖 │ │
│ │ const ast = parse(source); │ │
│ │ │ │
│ │ // 识别 import/require 语句 │ │
│ │ import utils from './utils'; → HarmonyImportDependency │ │
│ │ require('./config'); → CommonJsDependency │ │
│ │ import('./lazy'); → ImportDependency (async) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Step 4: 递归处理依赖 │ │
│ │ for (const dep of module.dependencies) { │ │
│ │ this.handleModuleCreation(dep); // 递归回到 Step 1 │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 最终产物:ModuleGraph(依赖关系图) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
5.3.3 Seal 阶段(核心:生成 Chunk)
这是 SplitChunks 生效的阶段:
yaml
┌─────────────────────────────────────────────────────────────────────────┐
│ Seal 阶段详解 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: 形成初始 Chunk │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Entry A ──→ Initial Chunk A │ │
│ │ ├── a.js │ │
│ │ ├── utils.js (被 A、B 都引用) │ │
│ │ └── lodash (被 A、B 都引用) │ │
│ │ │ │
│ │ Entry B ──→ Initial Chunk B │ │
│ │ ├── b.js │ │
│ │ ├── utils.js (重复!) │ │
│ │ └── lodash (重复!) │ │
│ │ │ │
│ │ import() ──→ Async Chunk │ │
│ │ └── lazy.js │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 2: SplitChunks 优化(optimizeChunks 钩子) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 遍历所有 Module,检查: │ │
│ │ ┌───────────────────────────────────────────────────────────┐ │ │
│ │ │ lodash: │ │ │
│ │ │ - 被引用次数: 2 (Chunk A, B) ≥ minChunks: 1 ✓ │ │ │
│ │ │ - 模块大小: 70KB ≥ minSize: 20KB ✓ │ │ │
│ │ │ - 来源: node_modules 匹配 test 正则 ✓ │ │ │
│ │ │ → 提取到 vendors chunk │ │ │
│ │ └───────────────────────────────────────────────────────────┘ │ │
│ │ ┌───────────────────────────────────────────────────────────┐ │ │
│ │ │ utils.js: │ │ │
│ │ │ - 被引用次数: 2 (Chunk A, B) ≥ minChunks: 2 ✓ │ │ │
│ │ │ - 模块大小: 5KB < minSize: 20KB ✗ │ │ │
│ │ │ → 不提取,保留在原 chunk │ │ │
│ │ └───────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 3: 代码生成 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 最终 Chunk 结构: │ │
│ │ Chunk A: a.js + utils.js │ │
│ │ Chunk B: b.js + utils.js │ │
│ │ Chunk vendors: lodash │ │
│ │ Chunk async: lazy.js │ │
│ │ │ │
│ │ 为每个 Chunk 生成代码: │ │
│ │ - 包裹 Module 为函数 │ │
│ │ - 注入 Runtime 代码 │ │
│ │ - 建立 Chunk 间依赖关系 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
5.4 Module 包裹机制
为什么要包裹成函数?
java
┌─────────────────────────────────────────────────────────────────────────┐
│ Module 包裹的三大目的 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 创建独立作用域 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 每个模块的变量不会污染全局 │ │
│ │ var name = 'a.js' 和 var name = 'b.js' 不会冲突 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 2. 注入模块系统 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ module → 当前模块对象 │ │
│ │ exports → 导出对象 (module.exports 的引用) │ │
│ │ __webpack_require__ → 引入其他模块的函数 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 3. 按需执行 + 缓存 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 函数只有被 require 时才执行(惰性加载) │ │
│ │ 执行一次后结果被缓存到 installedModules(单例模式) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
包裹前后对比
源代码:
javascript
// src/utils.js
export const add = (a, b) => a + b;
export const name = 'utils';
编译后(ES Module):
javascript
"./src/utils.js": function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// 标记为 ES Module(用于和 CommonJS 区分)
__webpack_require__.r(__webpack_exports__);
// 定义导出(使用 getter,实现 live binding)
__webpack_require__.d(__webpack_exports__, {
"add": function() { return add; },
"name": function() { return name; }
});
// 原始代码
const add = (a, b) => a + b;
const name = 'utils';
}
5.5 最终 Bundle 结构
javascript
// bundle.js
(function(modules) {
// ========== Runtime 代码 ==========
// 模块缓存
var installedModules = {};
// 核心加载函数
function __webpack_require__(moduleId) {
// 检查缓存
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建新模块
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 执行模块函数
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
module.l = true;
return module.exports;
}
// 工具函数
__webpack_require__.r = function(exports) {
Object.defineProperty(exports, '__esModule', { value: true });
};
__webpack_require__.d = function(exports, definition) {
for (var key in definition) {
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key] // getter 实现 live binding
});
}
};
// ========== 启动入口 ==========
return __webpack_require__('./src/index.js');
})({
// ========== 所有 Module(包裹后)==========
"./src/index.js": function(module, exports, __webpack_require__) {
var utils = __webpack_require__("./src/utils.js");
console.log(utils.add(1, 2));
},
"./src/utils.js": function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"add": function() { return add; }
});
const add = (a, b) => a + b;
}
});
第六章:Webpack 运行时机制
本章解决的问题:Webpack 打包后的代码在浏览器中是如何运行的?同步和异步加载是如何实现的?
6.1 同步加载:__webpack_require__
java
┌─────────────────────────────────────────────────────────────────────────┐
│ __webpack_require__(moduleId) 流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ require('./utils.js') │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ 检查 installedModules │ │
│ │ 是否有缓存? │ │
│ └──────────────────────────────┘ │
│ │ │
│ ┌────────────┴────────────┐ │
│ │ YES │ NO │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────────────┐ │
│ │ 直接返回 │ │ 创建 module 对象 │ │
│ │ 缓存的 │ │ { │ │
│ │ exports │ │ i: moduleId, │ │
│ └─────────────┘ │ l: false, │ │
│ │ exports: {} │ │
│ │ } │ │
│ └─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 放入缓存 │ │
│ │ installedModules │ │
│ │ [moduleId] = module │ │
│ └─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 执行模块函数 │ │
│ │ modules[moduleId] │ │
│ │ .call(...) │ │
│ └─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ module.l = true │ │
│ │ 返回 module.exports │ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
6.2 异步加载:__webpack_require__.e
动态 import() 会被转换为 __webpack_require__.e:
javascript
// 源代码
import('./lazy').then(module => {
module.doSomething();
});
// 编译后
__webpack_require__.e(/* chunkId */ 'lazy')
.then(__webpack_require__.bind(null, './src/lazy.js'))
.then(module => {
module.doSomething();
});
__webpack_require__.e 实现原理:
javascript
// Chunk 加载状态
// undefined: 未加载
// [resolve, reject]: 正在加载
// 0: 已加载
var installedChunks = {};
__webpack_require__.e = function(chunkId) {
var promises = [];
var installedChunkData = installedChunks[chunkId];
// 0 表示已加载
if (installedChunkData !== 0) {
// 正在加载中,复用 Promise
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// 创建新的 Promise
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// 创建 script 标签
var script = document.createElement('script');
script.charset = 'utf-8';
script.timeout = 120;
script.src = __webpack_require__.p + chunkId + '.bundle.js';
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
6.3 异步 Chunk 的注册(JSONP)
javascript
// lazy.chunk.js(异步 chunk 文件格式)
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
["lazy"], // chunkIds
{ // modules
"./src/lazy.js": function(module, exports) {
exports.doSomething = function() {
console.log("lazy loaded!");
};
}
}
]);
主 bundle 中的 JSONP 回调注册:
javascript
// 注册 JSONP 回调
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
// 将新模块添加到 modules 对象
for (moduleId in moreModules) {
modules[moduleId] = moreModules[moduleId];
}
// 标记 chunk 已加载,执行 resolve
for (var i = 0; i < chunkIds.length; i++) {
var chunkId = chunkIds[i];
if (installedChunks[chunkId]) {
installedChunks[chunkId][0](); // resolve
}
installedChunks[chunkId] = 0; // 标记已加载
}
}
6.4 运行时流程总览
xml
┌─────────────────────────────────────────────────────────────────────────┐
│ 运行时完整流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 页面加载 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ <script src="main.js"></script> │ │
│ │ <script src="vendor.js"></script> │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 初始化 Runtime │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ • 创建 installedModules 缓存 │ │
│ │ • 创建 installedChunks 缓存 │ │
│ │ • 定义 __webpack_require__ 函数 │ │
│ │ • 注册 JSONP 回调 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 执行入口模块 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ __webpack_require__("./src/index.js") │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────┴──────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ 同步依赖 异步依赖 │
│ ┌────────────────────┐ ┌────────────────────┐ │
│ │__webpack_require__ │ │__webpack_require__.e│ │
│ │ 从 modules 取 │ │ 创建 script 标签 │ │
│ │ 执行 + 缓存 │ │ 加载 chunk 文件 │ │
│ └────────────────────┘ └────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ JSONP 回调 │ │
│ │ 注册新 modules │ │
│ │ resolve Promise │ │
│ └────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │__webpack_require__ │ │
│ │ 执行异步模块 │ │
│ └────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
第七章:优化策略与最佳实践
本章解决的问题:如何利用 ESM 的静态特性和 Webpack 的优化能力,实现最佳的构建效果?
7.1 Tree Shaking 原理
Tree Shaking:移除 JavaScript 中未使用的代码(Dead Code Elimination)
javascript
┌─────────────────────────────────────────────────────────────────┐
│ Tree Shaking 工作原理 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 源代码(math.js) │
│ ───────────── │
│ export const add = (a, b) => a + b; // 被使用 │
│ export const sub = (a, b) => a - b; // 未使用 │
│ export const mul = (a, b) => a * b; // 未使用 │
│ │
│ 使用方(index.js) │
│ ───────────── │
│ import { add } from './math.js'; │
│ console.log(add(1, 2)); │
│ │
│ │ │
│ ▼ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 阶段 1: Webpack 标记(Mark Phase) │ │
│ │ ───────────────────────────────── │ │
│ │ 分析 import 语句 → 标记 add 为"已使用" │ │
│ │ sub, mul 没有被任何 import → 标记为"未使用" │ │
│ │ │ │
│ │ 生成代码(带标记): │ │
│ │ __webpack_require__.d(exports, { add: () => add }); │ │
│ │ /* unused harmony export sub */ │ │
│ │ /* unused harmony export mul */ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ │ │
│ ▼ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 阶段 2: Terser 删除(Sweep Phase) │ │
│ │ ───────────────────────────────── │ │
│ │ 识别 unused 标记 → 这些变量没有被引用 │ │
│ │ 安全删除 sub 和 mul 的定义 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ │ │
│ ▼ │
│ │
│ 最终 Bundle │
│ ────────── │
│ const add = (a, b) => a + b; // 只保留 add! │
│ console.log(add(1, 2)); │
│ │
└─────────────────────────────────────────────────────────────────┘
Tree Shaking 生效条件:
| 条件 | 说明 | 原因 |
|---|---|---|
| 使用 ESM | import/export 语法 |
静态分析的前提 |
| production 模式 | 或 usedExports: true |
启用标记功能 |
| 声明无副作用 | sideEffects: false |
告诉打包工具可安全删除 |
| 启用压缩 | Terser/UglifyJS | 实际删除代码 |
| 避免整体导入 | 用 { a } 不用 * as |
明确使用范围 |
7.2 sideEffects 配置
什么是副作用?
javascript
// ─────────── 有副作用 ───────────
// polyfill.js - 修改全局对象
Array.prototype.myMethod = function() {};
// analytics.js - 执行时发送请求
fetch('/api/track?page=home');
// styles.css - 影响页面样式
.button { color: red; }
// ─────────── 无副作用 ───────────
// utils.js - 纯函数,不影响外部
export const add = (a, b) => a + b;
export const format = (str) => str.trim();
package.json 配置:
json
{
"name": "my-library",
"sideEffects": false
}
json
{
"name": "my-library",
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfill.js"
]
}
7.3 代码分割策略(SplitChunks)
默认配置详解:
javascript
optimization: {
splitChunks: {
// 对哪些 chunk 生效
// 'async': 只处理异步 chunk(默认)
// 'initial': 只处理入口 chunk
// 'all': 处理所有 chunk(推荐)
chunks: 'async',
// 分割阈值
minSize: 20000, // 最小 20KB 才分割
minRemainingSize: 0, // 分割后剩余最小体积
minChunks: 1, // 最少被引用 1 次
// 并行请求限制
maxAsyncRequests: 30, // 按需加载时最大并行请求数
maxInitialRequests: 30, // 入口点最大并行请求数
// 缓存组(核心配置)
cacheGroups: {
// 第三方库
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
// 公共模块
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
}
推荐配置:
javascript
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// React 全家桶单独打包
react: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router)[\\/]/,
name: 'react-vendor',
priority: 20,
chunks: 'all',
},
// 其他第三方库
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
chunks: 'all',
},
// 业务公共模块
common: {
name: 'common',
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
},
},
// 运行时代码单独分离(利于缓存)
runtimeChunk: {
name: 'runtime',
},
}
7.4 模块输出格式
| 格式 | 导出语法 | 使用场景 | Tree Shaking |
|---|---|---|---|
var |
var MyLib = ... |
全局变量 | ❌ |
commonjs |
exports.MyLib = ... |
Node.js | ❌ |
commonjs2 |
module.exports = ... |
Node.js | ❌ |
amd |
define([], factory) |
RequireJS | ❌ |
umd |
通用模块定义 | 多环境兼容 | ❌ |
module |
export { ... } |
ES Module | ✅ |
多格式输出配置:
javascript
// webpack.config.js - 同时输出多种格式
module.exports = [
// ESM 版本(现代环境,支持 Tree Shaking)
{
entry: './src/index.js',
experiments: { outputModule: true },
output: {
filename: 'my-library.esm.js',
library: { type: 'module' },
},
},
// CommonJS 版本(Node.js)
{
entry: './src/index.js',
output: {
filename: 'my-library.cjs.js',
library: { type: 'commonjs2' },
},
},
// UMD 版本(浏览器直接引用)
{
entry: './src/index.js',
output: {
filename: 'my-library.umd.js',
library: { name: 'MyLibrary', type: 'umd' },
globalObject: 'this',
},
},
];
对应 package.json:
json
{
"name": "my-library",
"main": "./dist/my-library.cjs.js",
"module": "./dist/my-library.esm.js",
"browser": "./dist/my-library.umd.js",
"exports": {
".": {
"import": "./dist/my-library.esm.js",
"require": "./dist/my-library.cjs.js"
}
},
"sideEffects": false
}
7.5 最佳实践汇总
模块规范选择
┌────────────────────────────────────────────────────────────────┐
│ 模块规范选择指南 │
├────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ 新项目? │ │
│ └────────┬────────┘ │
│ │ │
│ 是 ←──┴──→ 否 │
│ │ │ │
│ ▼ ▼ │
│ ┌──────┐ ┌─────────────────┐ │
│ │ ESM │ │ 需要兼容老环境? │ │
│ └──────┘ └────────┬────────┘ │
│ │ │
│ 是 ←──┴──→ 否 │
│ │ │ │
│ ▼ ▼ │
│ ┌─────┐ ┌──────┐ │
│ │ UMD │ │ ESM │ │
│ └─────┘ └──────┘ │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ 发布 npm 包? ────────────→ 同时提供 ESM + CJS │
│ │
│ CDN 直接引用? ───────────→ UMD │
│ │
│ 需要 Tree Shaking? ──────→ ESM(必须) │
│ │
└────────────────────────────────────────────────────────────────┘
导入最佳实践
javascript
// ❌ 不推荐:导入整个库
import _ from 'lodash';
_.map([1, 2], x => x * 2);
// ✅ 推荐:按需导入
import { map } from 'lodash-es';
map([1, 2], x => x * 2);
// ❌ 不推荐:命名空间导入(阻止 Tree Shaking)
import * as utils from './utils';
utils.format(str);
// ✅ 推荐:具名导入
import { format } from './utils';
format(str);
// ✅ 懒加载:大模块动态导入
const loadChart = async () => {
const { Chart } = await import('./chart');
return new Chart();
};
CommonJS 迁移到 ESM
javascript
// ─────────── CommonJS ───────────
const fs = require('fs');
const path = require('path');
const { myFunc } = require('./utils');
module.exports = { a, b };
module.exports.c = c;
// ─────────── ESM ───────────
import fs from 'fs';
import path from 'path';
import { myFunc } from './utils.js'; // 注意扩展名!
export { a, b };
export { c };
// __dirname/__filename 替代方案
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
附录:核心概念速查表
A. 模块规范对比
| 特性 | CommonJS | ESM |
|---|---|---|
| 语法 | require/exports | import/export |
| 加载时机 | 运行时 | 编译时 |
| 绑定类型 | 值拷贝 | 实时绑定 |
| 静态分析 | ❌ | ✅ |
| Tree Shaking | ❌ | ✅ |
| 顶层 await | ❌ | ✅ |
B. Webpack 构建阶段
| 阶段 | 核心任务 | 关键 Hook |
|---|---|---|
| Initialize | 合并配置、创建 Compiler、注册 Plugin | environment |
| Make | 从 Entry 递归解析,执行 Loader,构建 ModuleGraph | make |
| Seal | 生成 Chunk、执行 SplitChunks、生成代码 | optimizeChunks |
| Optimize | Tree Shaking、压缩混淆 | optimizeTree |
| Emit | 输出 Bundle 文件 | emit |
C. 运行时机制
| 机制 | 说明 |
|---|---|
__webpack_require__ |
同步加载模块,从 modules 对象取并执行 |
__webpack_require__.e |
异步加载 Chunk,动态创建 script 标签 |
installedModules |
模块缓存,避免重复执行 |
installedChunks |
Chunk 缓存,避免重复加载 |
webpackJsonp |
JSONP 回调,注册异步 Chunk 的模块 |
D. 知识体系一图总结
scss
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ JavaScript 模块化知识体系 │
│ │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ 历史演进 │
│ ──────── │
│ 全局变量 → CommonJS(Node) → AMD(浏览器) → UMD(通用) → ESM(标准) │
│ │
│ 核心区别 │
│ ──────── │
│ CommonJS: 运行时加载,值拷贝,动态路径 → 无法 Tree Shaking │
│ ESM: 编译时分析,引用绑定,静态路径 → 支持 Tree Shaking │
│ │
│ ESM 三阶段 │
│ ────────── │
│ 构建(解析依赖) → 实例化(分配内存/连接绑定) → 求值(执行代码) │
│ │
│ 构建工具链 │
│ ────────── │
│ 源码(ESM) → Babel(保留ESM,转语法) → Webpack(打包,标记) → Terser(删除,压缩) │
│ │
│ Webpack 核心 │
│ ──────────── │
│ Module(源文件) → Chunk(中间态) → Bundle(产物) │
│ Make(构建ModuleGraph) → Seal(生成Chunk) → Emit(输出文件) │
│ │
│ 最佳实践 │
│ ──────── │
│ • 新项目首选 ESM │
│ • npm 包同时提供 ESM + CJS │
│ • 按需导入,避免 import * │
│ • 配置 sideEffects: false │
│ • 大模块使用动态 import() 懒加载 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘