前端模块化发展

前端模块化

前言

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>

核心问题:

  1. 命名冲突:全局变量相互覆盖
  2. 依赖管理:手动维护 script 标签顺序
  3. 按需加载:无法实现,所有代码一次性加载

早期解决方案: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() 懒加载                                           │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
相关推荐
sycmancia33 分钟前
Qml——Window元素使用
前端
每天都好困41 分钟前
03 AR 投影与误差修正:把地图上的点贴到视频画面上
前端
每天都好困43 分钟前
04 地图图层封装:像管理 Photoshop 图层一样管理地图元素
前端
去伪存真1 小时前
构建你的第一个 DevOps AI Agent:自动化捕获、分析并修复 CI 故障
前端·agent
niucloud-admin1 小时前
JAVA V6 多商户商城 开发文档——手机端前端
java·开发语言·前端
skywalk81631 小时前
deepseekharness在对话中,有四种模式,分别是极简、PTC、创造和标准模式,每种模式下挂载的插件、skill等数量不一样
前端·人工智能·实践
愛芳芳2 小时前
element-plus+vite+vue3+pinpa+mock+typeScript搭建经典的电商管理后台
前端·javascript·typescript
liangshanbo12152 小时前
面试题:常见的前端构建工具有哪些?分别适用于什么场景?
前端
linux_cfan2 小时前
videojs v10 源代码系列解读:30 · `SerialRunner` 与 `ConcurrentRunner`
前端
liangshanbo12152 小时前
面试题:页面需要发起大量 Ajax / HTTP 请求时,如何优化请求性能?
前端·http·ajax