103 lines
4.6 KiB
Markdown
103 lines
4.6 KiB
Markdown
---
|
||
id: frontend.webpack.build-optimization
|
||
title: webpack 5:loader 与 plugin、splitChunks、tree shaking、缓存、模块联邦
|
||
type: lesson
|
||
domain: frontend
|
||
stack: [webpack]
|
||
tags: [webpack, loader, plugin, splitChunks, tree shaking, contenthash, 持久化缓存, Module Federation]
|
||
level: intermediate
|
||
prerequisites: []
|
||
est_hours: 2.5
|
||
related: [frontend.vite.dev-build-config, frontend.webpack.scenario.build-speed, frontend.webpack.interview]
|
||
week: [7]
|
||
order: 1
|
||
---
|
||
|
||
# webpack 5:loader 与 plugin、splitChunks、tree shaking、缓存、模块联邦
|
||
|
||
> 基于 webpack 5.112 + webpack-cli 7 编写并实际构建验证。很多公司的存量项目仍在用 webpack,面试也常问原理。以官方文档为准:https://webpack.js.org/concepts/ 。
|
||
|
||
## 解释说明
|
||
|
||
### 核心概念
|
||
|
||
| 概念 | 一句话 |
|
||
| --- | --- |
|
||
| entry | 从哪里开始分析依赖 |
|
||
| output | 产物放在哪里、叫什么名字 |
|
||
| **loader** | 把非 JS 文件(或需要转换的 JS)**转换**成 JS 模块;从右到左(从下到上)链式执行 |
|
||
| **plugin** | 在编译**生命周期的钩子**上做事(生成 HTML、压缩、提取 CSS、分析体积),基于 tapable 事件机制 |
|
||
| chunk | 打包过程中的代码块;入口、动态 `import()`、`splitChunks` 都会产生 chunk |
|
||
| mode | `development` / `production`,决定默认优化项 |
|
||
|
||
### 构建流程(面试常问)
|
||
|
||
初始化参数 → 创建 Compiler → 从 entry 开始,对每个模块调用匹配的 loader 转换 → 解析出依赖,递归构建模块依赖图 → 根据入口和拆包规则组装 chunk → 生成代码写入 output。过程中在各个阶段触发钩子,plugin 在钩子上执行。
|
||
|
||
### 优化手段
|
||
|
||
- **tree shaking**:依赖 ES 模块的静态结构,`usedExports` 标记未使用的导出,压缩时删除;`package.json` 的 `sideEffects` 声明哪些文件有副作用(如 CSS),其余可以整体删除。
|
||
- **splitChunks**:提取公共代码和第三方库;`runtimeChunk: "single"` 把运行时单独拆出。
|
||
- **contenthash**:文件名随内容变化,配合 CDN 长期缓存。
|
||
- **持久化缓存**:`cache: { type: "filesystem" }`,第二次构建明显变快。
|
||
- **构建提速**:缩小 loader 的处理范围(`include` / `exclude`)、用 swc / esbuild 类 loader 代替 babel、多进程、减少不必要的 source map。
|
||
- **Module Federation(模块联邦)**:多个独立构建的应用在运行时共享模块,用于微前端。
|
||
|
||
## 案例
|
||
|
||
一个主站和后台两个入口共享工具函数:通过 `splitChunks` 提取出 `shared`,运行时拆出 `runtime`,报表模块用动态 `import()` 按需加载。
|
||
|
||
## 代码示例
|
||
|
||
完整项目:[examples/webpack-config](examples/webpack-config/),`npm install && npm run build` 输出:
|
||
|
||
```text
|
||
[size] runtime.981552ae.js 2.74 KiB
|
||
[size] main.38cd14b7.js 0.33 KiB
|
||
[size] 531.30ecc347.js 0.21 KiB ← 动态 import 的 report.js
|
||
[size] admin.e2a620fd.js 0.20 KiB
|
||
[size] shared.5f4a3ab4.js 0.18 KiB ← 两个入口共用的 utils.js
|
||
Entrypoint main 3.25 KiB = runtime + shared + main
|
||
Entrypoint admin 3.12 KiB = runtime + shared + admin
|
||
```
|
||
|
||
已验证:`utils.js` 中未被使用的 `unusedHelper` 在产物中搜索不到(tree shaking 生效)。
|
||
|
||
自定义 loader([loaders/md-loader.cjs](examples/webpack-config/loaders/md-loader.cjs)):
|
||
|
||
```js
|
||
module.exports = function mdLoader(source) {
|
||
const { trim = false } = this.getOptions();
|
||
const text = trim ? source.trim() : source;
|
||
return `export default ${JSON.stringify(text)};`;
|
||
};
|
||
```
|
||
|
||
自定义 plugin([webpack.config.js](examples/webpack-config/webpack.config.js)):
|
||
|
||
```js
|
||
class SizeReportPlugin {
|
||
apply(compiler) {
|
||
compiler.hooks.done.tap("SizeReportPlugin", (stats) => {
|
||
const assets = stats.toJson({ all: false, assets: true }).assets ?? [];
|
||
for (const a of assets) console.log(`[size] ${a.name} ${(a.size / 1024).toFixed(2)} KiB`);
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
## 易错点
|
||
|
||
- 混淆 loader 和 plugin 的职责。
|
||
- loader 的执行顺序写反(`use: ["style-loader", "css-loader"]` 中 css-loader 先执行)。
|
||
- 用了 CommonJS(`require` / `module.exports`)导致 tree shaking 失效;`sideEffects: false` 配置错误导致 CSS 被删掉。
|
||
- 用 `[hash]` 而不是 `[contenthash]`,每次构建所有文件名都变,缓存失效。
|
||
- 生产环境开启了暴露源码的 source map 并公开访问。
|
||
|
||
## 面试常问
|
||
|
||
- loader 和 plugin 的区别?写过吗?
|
||
- webpack 的构建流程?
|
||
- tree shaking 的原理和失效原因?
|
||
- 怎么优化构建速度和产物体积?
|