前端模块化发展

前端模块化

前言

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() 懒加载                                           │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
相关推荐
不务正业的前端学徒15 小时前
docker+nginx部署
前端
不务正业的前端学徒15 小时前
webpack/vite配置
前端
hhcccchh16 小时前
学习vue第八天 Vue3 模板语法和内置指令 - 简单入门
前端·vue.js·学习
yyf1989052516 小时前
Vue 框架相关中文文献
前端·javascript·vue.js
粥里有勺糖16 小时前
开发一个美观的 VitePress 图片预览插件
前端·vue.js·vitepress
陟上青云16 小时前
一篇文章带你搞懂原型和原型链
前端
我的写法有点潮16 小时前
推荐几个国外比较流行的UI库(上)
前端·javascript·css
鹏多多16 小时前
jsx/tsx使用cssModule和typescript-plugin-css-modules
前端·vue.js·react.js
ssshooter17 小时前
复古话题:Vue2 的空格间距切换到 Vite 后消失了
前端·vue.js·面试