Skip to content

Webpack 系统学习文档 ​

适合人群:想真正理解“前端构建工具到底在做什么”的学习者。

学习目标:从最小配置开始,逐步掌握 entry / output / loader / plugin / module / chunk / tree shaking / code splitting / cache。

核对时间:2026-06-24。Webpack 生态版本变化较快,具体安装版本请以官方文档和项目实际依赖为准。2026-07-04 已补充常用 loader/plugin 源码阅读导向,详见同目录 AST与Webpack自定义Loader和Plugin系统学习文档.md 第 16 章。2026-07-09 已按 Webpack 5.108.4、Tapable 2.3.3 补充 Compiler、Compilation、loader-runner、enhanced-resolve、生命周期与源码级工作原理。2026-09-01 已按 Webpack 5.110.x 官方文档补充 Tapable 钩子类型、模块解析决策树、Tree Shaking 双层机制与持久化缓存细节。


目录 ​

  1. Webpack 是什么
  2. 为什么现在还要学 Webpack
  3. 学习前置知识
  4. 快速开始
  5. 最小可运行配置
  6. 核心概念:entry、output、module、loader、plugin
  7. Loader 深入理解
  8. Plugin 深入理解
  9. 开发环境配置
  10. 处理 CSS、图片、字体与静态资源
  11. 在 Webpack 中使用 TypeScript
  12. 生产环境优化
  13. 代码分割与缓存策略
  14. Tree Shaking 与副作用
  15. 配置拆分与工程化组织方式
  16. 常见进阶主题
  17. Webpack 与 Vite 如何选择
  18. 初学者常见误区
  19. 推荐学习顺序
  20. 高级阶段:配置项全注释实战
  21. 核心机制深度补全
  22. 学习资源

1. Webpack 是什么 ​

Webpack 是一个模块打包工具。

它最核心的事情是:

  1. 从入口文件开始。
  2. 分析项目里的模块依赖关系。
  3. 根据规则处理 JavaScript、CSS、图片、字体等资源。
  4. 生成浏览器可运行的构建产物。

这里的模块不只是 JavaScript。

在 Webpack 的视角里,下面这些都可以被纳入模块系统:

  • JavaScript。
  • TypeScript。
  • CSS。
  • Sass / Less。
  • 图片。
  • 字体。
  • JSON。
  • WebAssembly。

这也是为什么 Webpack 经常被叫作 bundler。它会把项目中互相关联的资源组织成最终可发布的文件。

1.1 一个简单类比 ​

你可以把 Webpack 理解成一个“前端构建工厂”:

  • entry 告诉工厂从哪里开始。
  • loader 告诉工厂不同材料怎么加工。
  • plugin 告诉工厂在整个生产流程中做哪些额外动作。
  • output 告诉工厂成品放在哪里、叫什么名字。
  • optimization 告诉工厂如何压缩、拆分、缓存。

2. 为什么现在还要学 Webpack ​

很多人会问:

现在很多新项目都用 Vite,为什么还要学 Webpack?

原因有三个。

2.1 大量存量项目仍然依赖 Webpack ​

很多公司项目,尤其是中大型后台、老业务、微前端、复杂定制项目,仍然基于 Webpack。

你进入真实团队后,很可能会遇到:

  • Vue CLI 项目。
  • React CRA 老项目。
  • 自研脚手架。
  • 微前端主应用或子应用。
  • 复杂多页面构建系统。

这些项目背后经常都有 Webpack。

2.2 Webpack 能训练工程化底层理解 ​

学 Vite 更像先把现代项目跑起来。

学 Webpack 更像拆开构建工具看内部结构。

Webpack 能帮助你真正理解:

  • 什么是依赖图。
  • 为什么 CSS 也能被 import。
  • loader 为什么有执行顺序。
  • plugin 为什么能改变构建结果。
  • chunk 和 bundle 有什么关系。
  • tree shaking 为什么有时不生效。
  • 长缓存为什么依赖 contenthash。

这些理解反过来会帮助你更好地使用 Vite、Rollup、Rspack、Turbopack 等工具。

2.3 复杂定制能力依然强 ​

Webpack 的生态非常成熟:

  • loader 生态丰富。
  • plugin 生态丰富。
  • 构建流程可定制程度高。
  • 对老浏览器和复杂兼容场景经验丰富。
  • 大量企业内部工具链基于 Webpack 二次封装。

所以 Webpack 不只是“旧工具”,它仍然是理解前端工程化的重要入口。


3. 学习前置知识 ​

学习 Webpack 前,建议先掌握:

  • JavaScript 模块:import、export、CommonJS require。
  • Node.js 基础:package.json、npm scripts、路径处理。
  • 浏览器基础:HTML、CSS、JS 的加载方式。
  • 基础命令行操作。
  • TypeScript 基础,如果项目使用 TS。

如果你还不理解“模块”,先看这个例子:

js
// src/math.js
export function add(a, b) {
  return a + b
}
js
// src/index.js
import { add } from './math.js'

console.log(add(1, 2))

Webpack 会从 index.js 开始,发现它依赖 math.js,然后把它们纳入同一张依赖图。


4. 快速开始 ​

初始化项目:

bash
mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev

创建目录:

text
webpack-demo/
├── src/
│   └── index.js
├── package.json
└── webpack.config.js

src/index.js:

js
const title = document.createElement('h1')
title.textContent = 'Hello Webpack'
document.body.appendChild(title)

package.json:

json
{
  "scripts": {
    "build": "webpack"
  }
}

运行:

bash
npm run build

默认会生成 dist/main.js。


5. 最小可运行配置 ​

webpack.config.js:

js
const path = require('node:path')

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
}

如果项目使用 ESM 配置,也可以写成:

js
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

export default {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
}

5.1 这段配置做了什么 ​

  • mode: 'development':使用开发模式默认配置。
  • entry:入口文件。
  • output.filename:输出文件名。
  • output.path:输出目录,必须是绝对路径。
  • output.clean:构建前清理输出目录。

5.2 为什么 output.path 要用绝对路径 ​

Webpack 需要明确知道产物写到哪里。相对路径容易受运行目录影响,所以官方配置通常使用:

js
path.resolve(__dirname, 'dist')

6. 核心概念:entry、output、module、loader、plugin ​

6.1 entry ​

入口。Webpack 从这里开始分析依赖。

单入口:

js
entry: './src/index.js'

多入口:

js
entry: {
  main: './src/index.js',
  admin: './src/admin.js',
}

多入口常用于多页面应用。

6.2 output ​

输出配置。

js
output: {
  filename: '[name].[contenthash].js',
  path: path.resolve(__dirname, 'dist'),
  clean: true,
}

常见占位符:

  • [name]:入口名或 chunk 名。
  • [contenthash]:基于文件内容生成 hash。
  • [id]:chunk id。

生产环境常用 [contenthash] 做长期缓存。

6.3 module ​

模块处理规则集合。

js
module: {
  rules: [
    {
      test: /\.css$/i,
      use: ['style-loader', 'css-loader'],
    },
  ],
}

Webpack 默认能处理 JavaScript 和 JSON。其他类型资源通常需要 loader 或内置 asset modules。

6.4 loader ​

loader 用于转换某类文件。

例如处理 CSS:

js
{
  test: /\.css$/i,
  use: ['style-loader', 'css-loader'],
}

css-loader 让 Webpack 能理解 CSS 中的 @import 和 url()。

style-loader 把 CSS 注入页面的 <style> 标签。

6.5 plugin ​

plugin 参与整个构建流程。

例如自动生成 HTML:

bash
npm install html-webpack-plugin -D
js
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
}

loader 更像“处理文件”,plugin 更像“介入流程”。


7. Loader 深入理解 ​

7.1 loader 的执行顺序 ​

Webpack 中多个 loader 的执行顺序是从右到左、从下到上。

js
use: ['style-loader', 'css-loader']

实际顺序:

  1. css-loader 先处理 CSS。
  2. style-loader 再把结果注入页面。

所以不要写反:

js
use: ['css-loader', 'style-loader']

这通常会出错。

7.2 loader 的核心作用 ​

loader 本质是一个转换函数:

text
源文件内容 -> loader 处理 -> 新内容

例如:

  • Sass 转 CSS。
  • TypeScript 转 JavaScript。
  • Vue 单文件组件转 JavaScript 模块。
  • 图片转资源路径或内联 base64。

7.3 常见 loader ​

处理 CSS:

bash
npm install style-loader css-loader -D

处理 Sass:

bash
npm install sass sass-loader -D

处理 TypeScript:

bash
npm install ts-loader typescript -D

处理现代 JS:

bash
npm install babel-loader @babel/core @babel/preset-env -D

7.4 include 和 exclude ​

为了提升构建性能,loader 通常不要处理整个项目。

js
{
  test: /\.js$/,
  include: path.resolve(__dirname, 'src'),
  use: 'babel-loader',
}

常见写法:

js
exclude: /node_modules/

但如果某些第三方包发布了过新的语法,也可能需要把它们纳入转译。


8. Plugin 深入理解 ​

Plugin 比 loader 更强。它可以参与构建生命周期。

常见 plugin:

  • html-webpack-plugin:生成 HTML。
  • mini-css-extract-plugin:提取 CSS 文件。
  • copy-webpack-plugin:复制静态资源。
  • webpack-bundle-analyzer:分析构建体积。
  • define-plugin:定义编译时常量,Webpack 内置。

8.1 HtmlWebpackPlugin ​

js
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      title: 'Webpack App',
    }),
  ],
}

它会自动把构建出的 JS/CSS 注入 HTML。

8.2 DefinePlugin ​

js
const webpack = require('webpack')

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      __APP_VERSION__: JSON.stringify('1.0.0'),
    }),
  ],
}

业务代码:

js
console.log(__APP_VERSION__)

注意:这是编译时替换,不是运行时读取。

8.3 MiniCssExtractPlugin ​

开发环境可以用 style-loader,生产环境通常提取 CSS 文件。

bash
npm install mini-css-extract-plugin -D
js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash].css',
    }),
  ],
}

9. 开发环境配置 ​

开发环境重视:

  • 启动速度。
  • 热更新。
  • source map。
  • 本地代理。
  • 错误提示。

9.1 webpack-dev-server ​

安装:

bash
npm install webpack-dev-server -D

package.json:

json
{
  "scripts": {
    "dev": "webpack serve",
    "build": "webpack"
  }
}

配置:

js
module.exports = {
  devServer: {
    host: '0.0.0.0',
    port: 8080,
    open: true,
    hot: true,
    historyApiFallback: true,
  },
}

9.2 source map ​

开发环境推荐:

js
devtool: 'eval-cheap-module-source-map'

生产环境可以根据需要:

js
devtool: 'source-map'

或者关闭:

js
devtool: false

source map 的作用是让浏览器错误能映射回源码位置。

9.3 本地代理 ​

js
devServer: {
  proxy: [
    {
      context: ['/api'],
      target: 'http://localhost:3000',
      changeOrigin: true,
      pathRewrite: { '^/api': '' },
    },
  ],
}

业务代码:

js
fetch('/api/users')

注意:开发代理只在本地开发服务器生效,生产环境需要 Nginx、网关或后端服务配置。

9.4 history 路由刷新 ​

SPA 使用 history 路由时,本地刷新子路由可能 404。

解决:

js
devServer: {
  historyApiFallback: true,
}

生产环境也要配置 fallback。


10. 处理 CSS、图片、字体与静态资源 ​

10.1 CSS ​

开发环境:

js
{
  test: /\.css$/i,
  use: ['style-loader', 'css-loader'],
}

生产环境:

js
{
  test: /\.css$/i,
  use: [MiniCssExtractPlugin.loader, 'css-loader'],
}

10.2 Sass ​

bash
npm install sass sass-loader -D
js
{
  test: /\.s[ac]ss$/i,
  use: ['style-loader', 'css-loader', 'sass-loader'],
}

执行顺序:

  1. sass-loader:Sass 转 CSS。
  2. css-loader:处理 CSS 依赖。
  3. style-loader:注入页面。

10.3 PostCSS ​

bash
npm install postcss postcss-loader autoprefixer -D

postcss.config.js:

js
module.exports = {
  plugins: {
    autoprefixer: {},
  },
}

Webpack rule:

js
{
  test: /\.css$/i,
  use: ['style-loader', 'css-loader', 'postcss-loader'],
}

10.4 Asset Modules ​

Webpack 5 内置了资源模块,不再必须使用 file-loader、url-loader。

js
{
  test: /\.(png|jpe?g|gif|svg)$/i,
  type: 'asset',
  parser: {
    dataUrlCondition: {
      maxSize: 8 * 1024,
    },
  },
  generator: {
    filename: 'images/[name].[contenthash][ext]',
  },
}

type: 'asset' 会自动在资源文件和 base64 内联之间选择。

其他类型:

  • asset/resource:输出单独文件。
  • asset/inline:转成 base64。
  • asset/source:导出源代码字符串。

10.5 字体 ​

js
{
  test: /\.(woff2?|eot|ttf|otf)$/i,
  type: 'asset/resource',
  generator: {
    filename: 'fonts/[name].[contenthash][ext]',
  },
}

11. 在 Webpack 中使用 TypeScript ​

常见方案有三种。

11.1 ts-loader ​

安装:

bash
npm install typescript ts-loader -D

配置:

js
{
  test: /\.tsx?$/,
  use: 'ts-loader',
  exclude: /node_modules/,
}

优点:

  • 和 TypeScript 编译器结合紧密。
  • 配置直观。

缺点:

  • 大项目中可能较慢。

11.2 Babel 转译 + TypeScript 类型检查 ​

bash
npm install babel-loader @babel/core @babel/preset-env @babel/preset-typescript -D

.babelrc:

json
{
  "presets": ["@babel/preset-env", "@babel/preset-typescript"]
}

Webpack:

js
{
  test: /\.tsx?$/,
  use: 'babel-loader',
  exclude: /node_modules/,
}

Babel 只负责转译,不负责完整类型检查。需要额外脚本:

json
{
  "scripts": {
    "type-check": "tsc --noEmit"
  }
}

11.3 ForkTsCheckerWebpackPlugin ​

大项目中常用它把类型检查放到独立进程。

bash
npm install fork-ts-checker-webpack-plugin -D
js
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')

module.exports = {
  plugins: [
    new ForkTsCheckerWebpackPlugin(),
  ],
}

11.4 路径别名 ​

Webpack:

js
resolve: {
  extensions: ['.ts', '.tsx', '.js'],
  alias: {
    '@': path.resolve(__dirname, 'src'),
  },
}

TypeScript:

json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

只配 Webpack 不配 TypeScript,编辑器可能报错。


12. 生产环境优化 ​

生产环境重视:

  • 文件体积。
  • 缓存命中。
  • 首屏加载。
  • 代码拆分。
  • 稳定产物。

12.1 mode: 'production' ​

js
module.exports = {
  mode: 'production',
}

生产模式默认启用很多优化,例如压缩、tree shaking 相关优化。

12.2 文件名加 hash ​

js
output: {
  filename: 'js/[name].[contenthash].js',
  chunkFilename: 'js/[name].[contenthash].chunk.js',
  path: path.resolve(__dirname, 'dist'),
  clean: true,
}

内容不变,hash 尽量不变,浏览器缓存就能命中。

12.3 提取 CSS ​

js
new MiniCssExtractPlugin({
  filename: 'css/[name].[contenthash].css',
  chunkFilename: 'css/[name].[contenthash].chunk.css',
})

这样 CSS 可以独立缓存,也不会依赖 JS 注入。

12.4 压缩 ​

生产模式默认会压缩 JS。

如果需要自定义:

bash
npm install terser-webpack-plugin css-minimizer-webpack-plugin -D
js
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin(),
      new CssMinimizerPlugin(),
    ],
  },
}

12.5 构建体积分析 ​

bash
npm install webpack-bundle-analyzer -D
js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin(),
  ],
}

用于发现:

  • 大型依赖。
  • 重复依赖。
  • 不必要的全量引入。
  • 可懒加载模块。

12.6 Babel 转译体积优化 ​

Babel 不是压缩器。真正压缩 JS 通常交给 TerserPlugin 或 Webpack 生产模式默认优化。Babel 影响体积的重点是:

  • 不要把 ES Module 转成 CommonJS,否则会影响 tree shaking。
  • 不要为每个文件重复注入 Babel helper。
  • 不要引入超出目标浏览器需要的 polyfill。
  • 不要把浏览器已经支持的语法过度降级。

方式一:应用项目按需注入 polyfill ​

适合普通业务应用:

bash
npm install babel-loader @babel/core @babel/preset-env core-js -D
js
{
  loader: 'babel-loader',
  options: {
    cacheDirectory: true,
    presets: [
      [
        '@babel/preset-env',
        {
          // targets: 告诉 Babel 要兼容哪些浏览器。
          // 真实项目建议放到 package.json 的 browserslist 或 .browserslistrc 中统一管理。
          targets: '> 0.25%, not dead',

          // modules: false 表示保留 ES Module。
          // 这样 Webpack 才更容易做 tree shaking。
          modules: false,

          // useBuiltIns: 'usage' 表示按代码实际用到的 API 自动引入 polyfill。
          // 比一次性引入整个 core-js 体积更可控。
          // 注意:Babel 8 起该选项路线有变化,新项目要以当前 Babel 文档为准。
          useBuiltIns: 'usage',

          // corejs: 指定 core-js 主版本。
          // 与项目安装的 core-js 版本保持一致。
          corejs: 3,
        },
      ],
    ],
  },
}

关键配置项:

  • targets:兼容目标越老,转换和 polyfill 越多,体积越大。
  • modules: false:保留 ESM,避免破坏 Webpack 的 tree shaking。
  • useBuiltIns: 'usage':按使用情况注入 polyfill,适合应用项目。
  • corejs: 3:告诉 Babel 使用 core-js 3 的 polyfill 映射。
  • cacheDirectory: true:开启 Babel 缓存,主要优化构建速度,不直接减少体积。

方式二:复用 Babel helper ​

适合组件库、工具库,或者大型应用中 helper 重复明显的场景:

bash
npm install @babel/plugin-transform-runtime -D
npm install @babel/runtime
js
{
  loader: 'babel-loader',
  options: {
    presets: [
      [
        '@babel/preset-env',
        {
          targets: '> 0.25%, not dead',
          modules: false,

          // 重要:启用 @babel/plugin-transform-runtime 时,不要同时使用 useBuiltIns。
          // transform-runtime 更适合复用 helper 和避免污染全局环境。
          useBuiltIns: false,
        },
      ],
    ],
    plugins: [
      [
        '@babel/plugin-transform-runtime',
        {
          // helpers: 把每个文件里重复内联的 helper 改成从 @babel/runtime 引入。
          helpers: true,

          // regenerator: 复用 async/generator 所需运行时代码。
          regenerator: true,

          // corejs: false 表示不通过 transform-runtime 注入 polyfill。
          // 如果需要局部 polyfill,要进一步评估 @babel/runtime-corejs3。
          corejs: false,
        },
      ],
    ],
  },
}

经验判断:

  • 应用项目优先用 @babel/preset-env + browserslist 控制兼容目标。
  • 库项目优先考虑 @babel/plugin-transform-runtime,避免污染使用方全局环境。
  • 不要同时使用 useBuiltIns 和 @babel/plugin-transform-runtime 来处理 polyfill。
  • 体积变化要用 bundle analyzer 验证,不要只看配置是否“高级”。

13. 代码分割与缓存策略 ​

代码分割的目标是:

  • 首屏少加载。
  • 后续按需加载。
  • 公共依赖单独缓存。

13.1 动态导入 ​

js
button.addEventListener('click', async () => {
  const module = await import('./heavy-module')
  module.run()
})

Webpack 会为动态导入生成独立 chunk。

路由懒加载:

js
const UserPage = () => import('./pages/UserPage')

13.2 splitChunks ​

js
optimization: {
  splitChunks: {
    chunks: 'all',
  },
}

更细配置:

js
optimization: {
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      vendors: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendors',
        priority: -10,
      },
      common: {
        minChunks: 2,
        name: 'common',
        priority: -20,
      },
    },
  },
}

不要盲目把所有依赖都拆得很碎。拆包会减少单文件体积,但也可能增加请求数量和运行调度成本。

13.3 runtimeChunk ​

js
optimization: {
  runtimeChunk: 'single',
}

Webpack runtime 单独抽离后,可以减少业务 chunk hash 的连锁变化。

13.4 长缓存策略 ​

构建产物:

text
dist/
├── js/main.a1b2c3.js
├── js/vendors.d4e5f6.js
├── css/main.aaa111.css
└── index.html

推荐缓存:

  • JS/CSS/图片字体:长缓存,因为文件名带 hash。
  • index.html:短缓存或不缓存,因为它引用最新资源。

Nginx 示例:

nginx
location /assets/ {
  expires 1y;
  add_header Cache-Control "public, immutable";
}

location / {
  try_files $uri $uri/ /index.html;
  add_header Cache-Control "no-cache";
}

13.5 preload 与 prefetch ​

Webpack 可以通过动态导入的 magic comments 输出资源提示。

prefetch 表示“未来可能会用到”:

js
const UserSettings = () =>
  import(
    /* webpackChunkName: "user-settings" */
    /* webpackPrefetch: true */
    './pages/UserSettings'
  )

作用:

  • 父 chunk 加载完成后,浏览器空闲时再下载这个异步 chunk。
  • 适合“用户大概率下一步会访问”的页面,例如登录后可能进入设置页。
  • 优先级较低,不应该影响当前页面关键资源。

preload 表示“当前导航马上会用到”:

js
const ChartPanel = () =>
  import(
    /* webpackChunkName: "chart-panel" */
    /* webpackPreload: true */
    './components/ChartPanel'
  )

作用:

  • 与父 chunk 并行加载,优先级高于 prefetch。
  • 适合同一页面中很快就会渲染的异步模块。
  • 不能滥用,否则会抢占首屏关键资源带宽。

常见 magic comments:

  • webpackChunkName:指定异步 chunk 名称,便于排查和缓存观察。
  • webpackPrefetch:生成 rel="prefetch" 资源提示。
  • webpackPreload:生成 rel="preload" 资源提示。
  • webpackFetchPriority:给资源请求附加优先级提示,例如 "high"。
  • webpackInclude / webpackExclude:限制动态表达式导入时可能被打包的文件范围。

使用建议:

  • 路由懒加载先做好,再考虑 prefetch/preload。
  • prefetch 适合下一屏、下一步、用户即将访问的资源。
  • preload 适合当前页面马上需要的资源,不适合大面积加。
  • 加完后打开浏览器 Network,查看是否出现 prefetch / preload 请求以及是否影响首屏资源。

14. Tree Shaking 与副作用 ​

Tree Shaking 的目标是移除没有用到的代码。

14.1 基本条件 ​

通常需要:

  • 使用 ES Modules。
  • 生产模式。
  • 代码没有不可判断的副作用。
  • 包正确声明 sideEffects。

例子:

js
// math.js
export function add(a, b) {
  return a + b
}

export function multiply(a, b) {
  return a * b
}
js
// index.js
import { add } from './math'

console.log(add(1, 2))

如果 multiply 没有被使用,生产构建时有机会被移除。

14.2 sideEffects ​

package.json:

json
{
  "sideEffects": false
}

意思是项目模块没有副作用,未使用导出可以安全删除。

但 CSS import 是副作用:

js
import './style.css'

如果项目里有 CSS,需要写:

json
{
  "sideEffects": ["*.css"]
}

否则 CSS 可能被误删。

14.3 为什么 tree shaking 不生效 ​

常见原因:

  • 使用 CommonJS。
  • 引入方式导致全量导入。
  • 包没有正确声明 ESM 入口。
  • 代码有副作用。
  • Babel 把 ESM 转成 CommonJS。

错误示例:

js
const utils = require('./utils')

更推荐:

js
import { formatDate } from './utils'

15. 配置拆分与工程化组织方式 ​

项目复杂后,不建议把所有配置写在一个文件里。

常见结构:

text
build/
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
webpack.config.js

安装:

bash
npm install webpack-merge -D

webpack.common.js:

js
const path = require('node:path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/index.ts',
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
    alias: {
      '@': path.resolve(__dirname, '../src'),
    },
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
}

webpack.dev.js:

js
const { merge } = require('webpack-merge')
const common = require('./webpack.common')

module.exports = merge(common, {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map',
  devServer: {
    port: 8080,
    hot: true,
    historyApiFallback: true,
  },
})

webpack.prod.js:

js
const { merge } = require('webpack-merge')
const common = require('./webpack.common')

module.exports = merge(common, {
  mode: 'production',
  devtool: false,
  output: {
    filename: 'js/[name].[contenthash].js',
    chunkFilename: 'js/[name].[contenthash].chunk.js',
  },
})

package.json:

json
{
  "scripts": {
    "dev": "webpack serve --config build/webpack.dev.js",
    "build": "webpack --config build/webpack.prod.js"
  }
}

15.1 ESLint 接入 Webpack ​

Webpack 中现在不推荐继续使用老的 eslint-loader,更常见的是使用 eslint-webpack-plugin。

安装:

bash
npm install eslint eslint-webpack-plugin -D

配置:

js
const ESLintPlugin = require('eslint-webpack-plugin')

module.exports = {
  plugins: [
    new ESLintPlugin({
      // context: 从哪个目录开始检查。
      context: path.resolve(__dirname, 'src'),

      // extensions: 检查哪些扩展名。
      extensions: ['js', 'jsx', 'ts', 'tsx'],

      // exclude: 排除目录。相对于 context。
      exclude: ['node_modules', 'dist'],

      // cache: 开启 ESLint 缓存,提高二次检查速度。
      cache: true,

      // cacheLocation: 缓存文件位置。
      cacheLocation: path.resolve(__dirname, 'node_modules/.cache/eslint-webpack-plugin/.eslintcache'),

      // fix: 是否自动修复。
      // 会直接修改源文件,团队项目建议放在单独 npm script 中,而不是每次构建都开启。
      fix: false,

      // lintDirtyModulesOnly: 开发环境只检查改动模块。
      // 大项目启动更快,但首次启动不会全量检查。
      lintDirtyModulesOnly: process.env.NODE_ENV === 'development',

      // failOnError: 有 error 时是否让构建失败。
      failOnError: process.env.NODE_ENV === 'production',

      // failOnWarning: 有 warning 时是否让构建失败。
      // 严格团队可以在 CI 中开启。
      failOnWarning: false,
    }),
  ],
}

每个配置项的意义:

  • context:缩小检查范围,避免扫到构建产物或脚本目录。
  • extensions:决定哪些源码文件会被 ESLint 检查。
  • exclude:排除不需要检查的目录。
  • cache / cacheLocation:缓存 lint 结果,改善开发体验。
  • fix:自动修复,但会改源文件,建议谨慎使用。
  • lintDirtyModulesOnly:开发时只检查变更模块,适合大型项目。
  • failOnError:生产构建或 CI 中建议开启,避免带明显错误上线。
  • failOnWarning:适合规范要求非常严格的团队。

工程实践里,通常同时保留独立脚本:

json
{
  "scripts": {
    "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
    "lint:fix": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --fix"
  }
}

原因是:Webpack 插件适合开发时即时反馈,独立 lint 脚本适合 CI、提交前检查和批量修复。


16. 常见进阶主题 ​

16.1 多页面应用 ​

多入口:

js
entry: {
  index: './src/pages/index/main.js',
  admin: './src/pages/admin/main.js',
}

多个 HTML:

js
plugins: [
  new HtmlWebpackPlugin({
    filename: 'index.html',
    chunks: ['index'],
    template: './public/index.html',
  }),
  new HtmlWebpackPlugin({
    filename: 'admin.html',
    chunks: ['admin'],
    template: './public/admin.html',
  }),
]

16.2 externals ​

某些依赖不打进 bundle,而是通过 CDN 提供。

js
externals: {
  vue: 'Vue',
}

HTML 中需要引入:

html
<script src="https://cdn.example.com/vue.global.prod.js"></script>

使用 externals 要保证生产环境 CDN 可用。

16.3 Module Federation ​

Module Federation 常用于微前端或跨应用共享模块。

它解决的问题是:

  • 一个应用可以在运行时加载另一个应用暴露的模块。
  • 多个应用可以共享依赖。

初学者不建议一开始就学它。先把 entry、loader、plugin、chunk 学透。

16.4 性能排查 ​

构建慢时,按下面顺序排查:

  1. loader 是否处理了过多文件。
  2. 是否没有排除 node_modules。
  3. source map 是否太重。
  4. TypeScript 类型检查是否阻塞主构建。
  5. 是否有过多大型插件。
  6. 缓存是否开启。

Webpack 5 支持 filesystem cache:

js
cache: {
  type: 'filesystem',
}

16.5 PWA 与 Workbox ​

PWA 的核心不是“加一个插件就完事”,而是引入 Service Worker,让应用具备离线缓存、资源预缓存、弱网可用等能力。Webpack 项目里常用 workbox-webpack-plugin。

安装:

bash
npm install workbox-webpack-plugin -D

最简单方式:GenerateSW

js
const { GenerateSW } = require('workbox-webpack-plugin')

module.exports = {
  plugins: [
    new GenerateSW({
      // clientsClaim: 新 Service Worker 激活后尽快接管页面。
      clientsClaim: true,

      // skipWaiting: 新 Service Worker 安装后跳过等待阶段。
      // 开启后更新更快,但要处理好“用户正在使用旧页面”的情况。
      skipWaiting: true,

      // maximumFileSizeToCacheInBytes: 超过该大小的资源不进入预缓存。
      maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,

      // navigateFallback: SPA 离线或刷新时回退到 index.html。
      navigateFallback: '/index.html',

      // runtimeCaching: 运行时缓存策略。
      runtimeCaching: [
        {
          urlPattern: /^https:\/\/api\.example\.com\//,
          handler: 'NetworkFirst',
          options: {
            cacheName: 'api-cache',
            expiration: {
              maxEntries: 50,
              maxAgeSeconds: 5 * 60,
            },
          },
        },
      ],
    }),
  ],
}

关键配置项:

  • clientsClaim:让激活后的 Service Worker 尽快控制页面。
  • skipWaiting:跳过等待旧 Service Worker 退出的阶段。
  • maximumFileSizeToCacheInBytes:限制预缓存资源大小,避免把大视频、大地图包塞进缓存。
  • navigateFallback:SPA 离线访问路由时回退到入口 HTML。
  • runtimeCaching:配置运行时缓存,例如接口、图片、字体、CDN 资源。
  • urlPattern:匹配要缓存的请求。
  • handler:缓存策略,例如 NetworkFirst、CacheFirst、StaleWhileRevalidate。
  • cacheName:缓存桶名称,方便 DevTools 中排查。
  • expiration:缓存数量和时间限制。

更可控方式:InjectManifest

js
const { InjectManifest } = require('workbox-webpack-plugin')

module.exports = {
  plugins: [
    new InjectManifest({
      // swSrc: 自己编写的 Service Worker 源文件。
      swSrc: './src/service-worker.js',

      // swDest: 构建后输出的 Service Worker 文件名。
      swDest: 'service-worker.js',
    }),
  ],
}

选择建议:

  • GenerateSW:适合快速接入、缓存策略简单的项目。
  • InjectManifest:适合需要自定义 Service Worker 逻辑、Web Push、复杂缓存策略的项目。

注意事项:

  • Service Worker 通常要求 HTTPS 或 localhost。
  • PWA 缓存可能导致“用户一直看到旧版本”,上线前要设计更新提示。
  • 不要缓存所有接口,尤其是用户权限、订单、支付、实时数据。
  • 每次修改 PWA 配置后,都要在 Chrome DevTools 的 Application 面板检查 Service Worker 和 Cache Storage。

17. Webpack 与 Vite 如何选择 ​

17.1 新项目 ​

如果没有历史包袱,Vue、React 等现代项目通常可以优先考虑 Vite。

原因:

  • 开发体验好。
  • 配置少。
  • 启动快。
  • 现代生态支持好。

17.2 老项目 ​

如果项目已经基于 Webpack:

  • 不要为了追新盲目迁移。
  • 先评估 loader/plugin 依赖。
  • 评估构建脚本、部署流程、环境变量、代理、资源路径。
  • 再决定是否迁移到 Vite。

17.3 学习角度 ​

推荐两个都学:

  • Vite 用来快速搭现代项目。
  • Webpack 用来理解工程化底层。

当你理解 Webpack 后,再看 Vite、Rollup、Rspack,会更容易。


18. 初学者常见误区 ​

18.1 以为 Webpack 只是压缩代码 ​

Webpack 不只是压缩。它的核心是构建依赖图并处理各种资源。

18.2 loader 和 plugin 分不清 ​

记住:

  • loader 处理文件内容。
  • plugin 参与构建流程。

18.3 忽略 loader 执行顺序 ​

use: ['style-loader', 'css-loader'] 是从右到左执行。

18.4 只追求配置多 ​

Webpack 配置不是越多越好。能用默认值就别过度配置。

18.5 开发配置和生产配置混在一起 ​

开发环境和生产环境目标不同:

  • 开发重速度和调试。
  • 生产重体积和缓存。

配置应该拆分。

18.6 以为 source map 越完整越好 ​

完整 source map 有调试价值,但生产环境公开 source map 可能泄露源码结构。


19. 推荐学习顺序 ​

第一阶段:跑起来 ​

目标:

  • 手写最小配置。
  • 理解 entry 和 output。
  • 能成功构建 JS。

练习:

  • 创建 webpack-demo。
  • 写 src/index.js。
  • 运行 npm run build。

第二阶段:处理资源 ​

目标:

  • 会处理 CSS。
  • 会处理 Sass。
  • 会处理图片和字体。
  • 会生成 HTML。

练习:

  • 添加 html-webpack-plugin。
  • 添加 CSS。
  • 添加图片资源。

第三阶段:开发体验 ​

目标:

  • 会配置 dev server。
  • 会配置 source map。
  • 会配置代理。
  • 会处理 history 路由刷新。

练习:

  • 启动 webpack serve。
  • 模拟接口代理。
  • 刷新子路由不 404。

第四阶段:生产优化 ​

目标:

  • 会提取 CSS。
  • 会加 contenthash。
  • 会配置 splitChunks。
  • 理解 tree shaking。

练习:

  • 构建生产产物。
  • 使用 bundle analyzer 看体积。
  • 调整拆包策略。

第五阶段:工程化 ​

目标:

  • 会拆分配置。
  • 会接入 TypeScript。
  • 会处理环境变量。
  • 会排查构建问题。

练习:

  • 拆成 common/dev/prod 三份配置。
  • 接入 TypeScript。
  • 加类型检查脚本。

20. 高级阶段:配置项全注释实战 ​

Webpack 的高级阶段不是“背更多配置”,而是能回答三个问题:

  • 这个配置参与构建流程的哪个阶段。
  • 它影响的是模块解析、资源转换、代码生成、开发服务器、缓存,还是优化策略。
  • 改完后应该用什么现象验证它真的生效。

20.1 一份偏中大型项目的 webpack.config.js ​

下面这份配置偏学习用途。真实项目应该按需要拆成 webpack.common.js、webpack.dev.js、webpack.prod.js,不要机械复制全量配置。

js
const path = require('node:path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const ESLintPlugin = require('eslint-webpack-plugin')
const { GenerateSW } = require('workbox-webpack-plugin')
const { ModuleFederationPlugin } = require('webpack').container

module.exports = (env = {}, argv = {}) => {
  // argv.mode 来自命令行 --mode。
  // 没传时默认按 development 处理,避免配置里到处写 undefined 判断。
  const mode = argv.mode || 'development'
  const isProd = mode === 'production'
  const isDev = !isProd

  return {
    // context: 入口和 loader 相对路径的基础目录,默认是 process.cwd()。
    // 固定 context 可以减少在不同目录执行命令导致的路径歧义。
    context: __dirname,

    // mode: Webpack 内置模式。
    // development 会偏向构建速度和调试信息。
    // production 会默认开启压缩、tree shaking、作用域提升等优化。
    // none 表示不套用默认优化,适合研究内部行为。
    mode,

    // target: 构建产物运行环境。
    // 'web' 面向浏览器;'node' 面向 Node;也可以写 browserslist。
    target: 'web',

    // entry: 构建入口。Webpack 从这里开始建立依赖图。
    // 字符串适合单入口;对象适合多入口或多页面。
    entry: {
      app: './src/main.ts',
    },

    output: {
      // path: 构建产物输出目录,必须是绝对路径。
      path: path.resolve(__dirname, 'dist'),

      // filename: 入口 chunk 的文件名。
      // [name] 来自 entry 的 key;[contenthash] 根据文件内容生成,适合长期缓存。
      filename: isProd ? 'assets/js/[name].[contenthash:8].js' : 'assets/js/[name].js',

      // chunkFilename: 非入口 chunk 的文件名。
      // 动态 import、splitChunks 拆出来的包会使用它。
      chunkFilename: isProd ? 'assets/js/[name].[contenthash:8].chunk.js' : 'assets/js/[name].chunk.js',

      // assetModuleFilename: Asset Modules 默认输出文件名。
      // 图片、字体等资源如果没有在 rule.generator.filename 中覆盖,就用这里。
      assetModuleFilename: 'assets/media/[name].[contenthash:8][ext][query]',

      // publicPath: 浏览器加载资源时使用的公共路径。
      // '/' 适合根路径部署;'/admin/' 适合子路径部署;'auto' 常用于微前端和动态 CDN 场景。
      publicPath: env.PUBLIC_PATH || '/',

      // clean: 构建前清理 output.path。
      // true 会删除旧产物,避免旧 hash 文件残留。
      clean: true,

      // crossOriginLoading: 给异步脚本加 crossorigin。
      // 需要收集跨域脚本错误详情或配合 SRI/CDN 时会用到。
      crossOriginLoading: 'anonymous',

      // uniqueName: 当前构建的唯一名称。
      // Module Federation、多个 Webpack 应用共存时用于避免 runtime 全局变量冲突。
      uniqueName: 'learning-app',
    },

    // devtool: source map 策略。
    // 开发环境推荐 eval-cheap-module-source-map,速度和定位较平衡。
    // 生产环境可用 hidden-source-map 上传监控平台,但不公开给浏览器引用。
    devtool: isProd ? 'hidden-source-map' : 'eval-cheap-module-source-map',

    cache: {
      // type: filesystem 表示使用文件系统持久化缓存。
      // 二次构建速度会明显改善,适合中大型项目。
      type: 'filesystem',

      // name: 缓存命名。不同环境分开,避免 dev/prod 缓存互相污染。
      name: mode,

      // cacheDirectory: 缓存目录。
      cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'),

      // buildDependencies: 这些文件变化时,让 Webpack 主动失效缓存。
      // 修改构建配置后如果缓存没有失效,常见原因就是这里漏了配置文件。
      buildDependencies: {
        config: [__filename],
      },

      // compression: 是否压缩缓存数据。
      // gzip 可节省磁盘空间,但会增加一点 CPU 成本。
      compression: 'gzip',
    },

    resolve: {
      // extensions: import 省略扩展名时按顺序尝试。
      // 顺序越靠前优先级越高。不要放太多,避免解析成本和歧义。
      extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],

      // alias: 路径别名。
      // $ 结尾表示精确匹配,例如 vue$ 只匹配 import 'vue'。
      alias: {
        '@': path.resolve(__dirname, 'src'),
        '@components': path.resolve(__dirname, 'src/components'),
      },

      // modules: 解析第三方模块时查找的目录。
      // 默认包含 node_modules;加入 src 后可以从 src 做绝对导入。
      modules: [path.resolve(__dirname, 'src'), 'node_modules'],

      // mainFields: 解析包入口字段顺序。
      // 浏览器项目优先 browser,再 module,最后 main。
      mainFields: ['browser', 'module', 'main'],

      // conditionNames: 解析 package.json exports 时使用的条件。
      // import/browser/development/production 会影响命中哪个导出文件。
      // 注意:显式设置会覆盖默认条件集(默认含 webpack 条件),如需保留默认值要使用 "..." 继承。
      conditionNames: ['import', 'browser', isProd ? 'production' : 'development'],

      // symlinks: true 表示解析符号链接到真实路径。
      // Monorepo 或 npm link 中如果出现重复 React/Vue 实例,可以尝试调整。
      symlinks: true,

      // fallback: 浏览器环境中 Node 内置模块的替代实现。
      // Webpack 5 不再默认 polyfill Node 核心模块,因此需要显式配置。
      fallback: {
        crypto: false,
        stream: false,
        path: false,
      },

      // extensionAlias: 允许请求 .js 时优先尝试 .ts。
      // 迁移 TS 或维护 ESM 包时偶尔有用,普通业务项目不是必需。
      extensionAlias: {
        '.js': ['.ts', '.tsx', '.js'],
      },
    },

    module: {
      // noParse: 跳过对大型、无依赖的库做依赖解析。
      // 适合已经打包好的 UMD 文件。误用会导致其中的 import/require 不被处理。
      noParse: /[\\/]node_modules[\\/]lodash[\\/]lodash\.js$/,

      rules: [
        {
          // oneOf: 命中其中一个 rule 后停止继续匹配。
          // 能避免同一个资源被多个互斥规则重复处理。
          oneOf: [
            {
              // test: 匹配 TS/JS 文件。
              test: /\.[cm]?[tj]sx?$/,

              // include: 只处理 src,提高构建速度。
              include: path.resolve(__dirname, 'src'),

              // exclude: 排除 node_modules。
              exclude: /node_modules/,

              use: [
                {
                  // loader: 使用 babel-loader 转译 JS/TS。
                  loader: 'babel-loader',

                  options: {
                    // cacheDirectory: Babel 自己的缓存。
                    cacheDirectory: true,

                    // presets: 预设转换规则。
                    presets: [
                      [
                        '@babel/preset-env',
                        {
                          // targets: 目标浏览器。
                          // 配合 browserslist 可统一项目兼容目标。
                          targets: 'defaults',

                          // modules: false 保留 ESM,便于 Webpack tree shaking。
                          modules: false,

                          // useBuiltIns/corejs: 按需注入 polyfill。
                          // 需要安装 core-js,并确认浏览器兼容策略。
                          // 如果改用 @babel/plugin-transform-runtime 管理运行时 helper,就不要同时用 useBuiltIns。
                          useBuiltIns: 'usage',
                          corejs: 3,
                        },
                      ],
                      [
                        '@babel/preset-typescript',
                        {
                          // allowDeclareFields: 支持 TS declare class fields。
                          allowDeclareFields: true,
                        },
                      ],
                    ],

                    // plugins: Babel 插件列表。
                    // 体积优化常见选择是 @babel/plugin-transform-runtime:
                    // - helpers: 复用 Babel helper,避免每个文件重复注入。
                    // - regenerator: 复用 async/generator 运行时代码。
                    // - corejs: false 表示不在这里处理 polyfill。
                    // 注意:如果启用 transform-runtime,通常要把 preset-env 的 useBuiltIns 改为 false。
                    // plugins: [
                    //   [
                    //     '@babel/plugin-transform-runtime',
                    //     {
                    //       helpers: true,
                    //       regenerator: true,
                    //       corejs: false,
                    //     },
                    //   ],
                    // ],
                  },
                },
              ],
            },
            {
              // resourceQuery: 匹配 import './x.css?inline' 这类请求。
              // 可针对同一种文件的不同查询参数走不同处理逻辑。
              resourceQuery: /inline/,
              type: 'asset/source',
            },
            {
              // test: 普通 CSS。
              test: /\.css$/,
              use: [
                // 开发用 style-loader 把 CSS 注入页面,HMR 体验好。
                // 生产用 MiniCssExtractPlugin.loader 抽离 CSS 文件,利于缓存和并行加载。
                isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
                {
                  loader: 'css-loader',
                  options: {
                    // importLoaders: CSS 中 @import 的资源在 css-loader 后还要经过几个 loader。
                    importLoaders: 1,

                    // modules.auto: 只有 .module.css 启用 CSS Modules。
                    modules: {
                      auto: /\.module\.css$/,
                      localIdentName: isDev ? '[path][name]__[local]' : '[hash:base64:8]',
                    },
                  },
                },
                {
                  loader: 'postcss-loader',
                  options: {
                    // postcssOptions: PostCSS 插件配置。
                    // 也可以放到 postcss.config.js。
                    postcssOptions: {
                      plugins: [['postcss-preset-env', {}]],
                    },
                  },
                },
              ],
            },
            {
              // test: Sass/SCSS。
              test: /\.s[ac]ss$/,
              use: [
                isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
                'css-loader',
                'postcss-loader',
                {
                  loader: 'sass-loader',
                  options: {
                    // implementation: 指定 Sass 实现。
                    // 可用 sass 或 sass-embedded。
                    implementation: require('sass'),

                    // additionalData: 注入全局变量/mixin。
                    // 不要注入实际 CSS 规则,否则会重复输出。
                    additionalData: '@use "@/styles/variables.scss" as *;',
                  },
                },
              ],
            },
            {
              // test: 图片资源。
              test: /\.(png|jpe?g|gif|webp|avif|svg)$/i,

              // type: asset 会按大小自动选择 inline 或 resource。
              type: 'asset',

              parser: {
                dataUrlCondition: {
                  // maxSize: 小于该值的图片转成 base64 内联。
                  // 小图减少请求,大图内联会撑大 JS/CSS。
                  maxSize: 4 * 1024,
                },
              },

              generator: {
                // filename: 图片输出路径和命名。
                filename: 'assets/images/[name].[contenthash:8][ext]',
              },
            },
            {
              // test: 字体资源。
              test: /\.(woff2?|eot|ttf|otf)$/i,
              type: 'asset/resource',
              generator: {
                filename: 'assets/fonts/[name].[contenthash:8][ext]',
              },
            },
          ],
        },
      ],
    },

    plugins: [
      new HtmlWebpackPlugin({
        // template: HTML 模板。
        // 插件会自动把生成的 JS/CSS 注入模板。
        template: './public/index.html',

        // filename: 输出 HTML 文件名。
        filename: 'index.html',

        // inject: script 注入位置。body 通常能避免阻塞首屏 HTML 解析。
        inject: 'body',

        // minify: 生产环境压缩 HTML。
        minify: isProd
          ? {
              removeComments: true,
              collapseWhitespace: true,
              removeRedundantAttributes: true,
            }
          : false,
      }),

      new webpack.DefinePlugin({
        // DefinePlugin: 编译时文本替换,不是在浏览器运行时赋值。
        // 字符串值必须 JSON.stringify。
        __APP_VERSION__: JSON.stringify(process.env.npm_package_version || '0.0.0'),
        __BUILD_MODE__: JSON.stringify(mode),
      }),

      new webpack.EnvironmentPlugin({
        // EnvironmentPlugin: 把 process.env 中的变量注入到客户端构建。
        // 只放可以公开的变量,不要放密钥。
        NODE_ENV: mode,
      }),

      new MiniCssExtractPlugin({
        // filename: 入口 CSS 文件名。
        filename: isProd ? 'assets/css/[name].[contenthash:8].css' : 'assets/css/[name].css',

        // chunkFilename: 异步 CSS chunk 文件名。
        chunkFilename: isProd ? 'assets/css/[name].[contenthash:8].chunk.css' : 'assets/css/[name].chunk.css',
      }),

      new ESLintPlugin({
        // context: ESLint 检查起点。
        context: path.resolve(__dirname, 'src'),

        // extensions: 要检查的文件类型。
        extensions: ['js', 'jsx', 'ts', 'tsx'],

        // exclude: 排除目录。
        exclude: ['node_modules', 'dist'],

        // cache/cacheLocation: 缓存 lint 结果,提高二次构建速度。
        cache: true,
        cacheLocation: path.resolve(__dirname, 'node_modules/.cache/eslint-webpack-plugin/.eslintcache'),

        // lintDirtyModulesOnly: 开发环境只检查变化模块。
        lintDirtyModulesOnly: isDev,

        // failOnError: 生产构建遇到 lint error 时失败。
        failOnError: isProd,

        // failOnWarning: 是否把 warning 也作为构建失败。
        failOnWarning: false,
      }),

      new ForkTsCheckerWebpackPlugin({
        // async: 开发环境异步报错,不阻塞构建;生产环境建议阻塞,避免带类型错误上线。
        async: isDev,

        typescript: {
          // configFile: 指定 tsconfig。
          configFile: path.resolve(__dirname, 'tsconfig.json'),
        },
      }),

      env.MF === 'true' &&
        new ModuleFederationPlugin({
          // name: 当前容器名。
          name: 'host',

          // filename: 当前应用作为 remote 暴露时的入口文件。
          filename: 'remoteEntry.js',

          // remotes: 引用其他远程应用。
          remotes: {
            remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
          },

          // exposes: 把当前应用的模块暴露给别人。
          exposes: {
            './Button': './src/components/Button',
          },

          // shared: 共享依赖,避免多个应用重复加载框架。
          shared: {
            react: { singleton: true, requiredVersion: false },
            'react-dom': { singleton: true, requiredVersion: false },
          },
        }),

      isProd &&
        env.PWA === 'true' &&
        new GenerateSW({
          // clientsClaim: 新 Service Worker 激活后尽快接管页面。
          clientsClaim: true,

          // skipWaiting: 新 Service Worker 安装后跳过等待阶段。
          // 更新更快,但要设计好页面刷新或更新提示。
          skipWaiting: true,

          // maximumFileSizeToCacheInBytes: 限制进入预缓存的单文件大小。
          maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,

          // navigateFallback: SPA 离线路由回退入口。
          navigateFallback: '/index.html',

          // runtimeCaching: 运行时缓存策略。
          runtimeCaching: [
            {
              // urlPattern: 匹配需要缓存的请求。
              urlPattern: /^https:\/\/api\.example\.com\//,

              // handler: NetworkFirst 优先请求网络,失败时读缓存。
              handler: 'NetworkFirst',

              options: {
                // cacheName: 缓存桶名称。
                cacheName: 'api-cache',

                expiration: {
                  // maxEntries: 最多缓存多少条请求。
                  maxEntries: 50,

                  // maxAgeSeconds: 缓存最长保留多久。
                  maxAgeSeconds: 5 * 60,
                },
              },
            },
          ],
        }),
    ].filter(Boolean),

    devServer: {
      // host: 开发服务器监听地址。
      host: '0.0.0.0',

      // port: 开发服务器端口。
      port: Number(env.PORT || 8080),

      // hot: 启用 HMR。
      hot: true,

      // liveReload: HMR 无法处理时是否整页刷新。
      liveReload: true,

      // open: 启动后自动打开浏览器。
      open: false,

      // compress: 对开发服务器响应启用 gzip。
      compress: true,

      // allowedHosts: 允许访问开发服务器的 Host。
      // 'all' 方便局域网和代理调试,但公共网络环境要谨慎。
      allowedHosts: 'all',

      static: {
        // directory: 静态资源目录。
        directory: path.resolve(__dirname, 'public'),

        // publicPath: 这些静态资源挂载到哪个 URL 路径。
        publicPath: '/',

        // watch: public 目录变化后是否触发刷新。
        watch: true,
      },

      client: {
        // overlay: 编译错误是否用浏览器遮罩层展示。
        overlay: {
          errors: true,
          warnings: false,
        },

        // logging: 浏览器控制台中 dev-server 客户端日志级别。
        logging: 'info',

        // progress: 是否在浏览器中显示编译进度。
        progress: false,
      },

      historyApiFallback: {
        // index: SPA history 路由刷新时回退到哪个 HTML。
        index: '/index.html',

        // disableDotRule: 带点路径也走 fallback,某些前端路由会需要。
        disableDotRule: true,
      },

      proxy: {
        '/api': {
          // target: 后端服务地址。
          target: env.API_TARGET || 'http://localhost:3000',

          // changeOrigin: 修改请求头 Host 为 target。
          changeOrigin: true,

          // secure: 代理自签名 HTTPS 时设 false。
          secure: false,

          // pathRewrite: 重写路径。
          pathRewrite: {
            '^/api': '',
          },

          // ws: 代理 WebSocket。
          ws: true,
        },
      },

      headers: {
        // headers: 给开发响应增加头信息。
        'Cache-Control': 'no-store',
      },

      watchFiles: {
        // paths: 额外监听的文件。
        paths: ['src/**/*', 'public/**/*'],

        // options: chokidar 监听配置。
        options: {
          ignored: ['**/node_modules/**', '**/dist/**'],
        },
      },
    },

    optimization: {
      // runtimeChunk: 把 Webpack runtime 单独抽出来。
      // 有利于业务 chunk 的 contenthash 稳定。
      runtimeChunk: 'single',

      // moduleIds/chunkIds: 稳定模块和 chunk id。
      // production 默认通常已较合理,显式写出便于理解缓存策略。
      moduleIds: 'deterministic',
      chunkIds: 'deterministic',

      // usedExports: 标记哪些导出被使用,是 tree shaking 的前置分析。
      usedExports: true,

      // sideEffects: 根据 package.json sideEffects 和源码分析副作用。
      // 错误标记副作用会导致样式或初始化代码被误删。
      sideEffects: true,

      // concatenateModules: 作用域提升,把模块合并到更少闭包中。
      // 生产环境默认常会启用,有助于运行性能和压缩。
      concatenateModules: isProd,

      // realContentHash: 基于最终资源内容生成 hash,避免无意义缓存失效。
      realContentHash: isProd,

      // minimize: 是否压缩产物。
      minimize: isProd,

      minimizer: [
        new TerserPlugin({
          terserOptions: {
            compress: {
              // drop_console: 删除 console。
              // 真实项目要考虑线上排障和日志策略。
              drop_console: false,
              drop_debugger: true,
            },
            format: {
              // comments: 是否保留注释。
              comments: false,
            },
          },
          // extractComments: 是否把许可证注释抽离到单独文件。
          extractComments: false,
        }),
        new CssMinimizerPlugin(),
      ],

      splitChunks: {
        // chunks: 对哪些 chunk 做拆分。
        // 'all' 表示同步和异步 chunk 都参与。
        chunks: 'all',

        // minSize: 生成新 chunk 的最小体积。
        minSize: 20 * 1024,

        // maxSize: 尝试把超过该值的 chunk 再拆小。
        // 这是提示,不是强制精确切割。
        maxSize: 244 * 1024,

        // minChunks: 模块至少被几个 chunk 共享才拆出。
        minChunks: 1,

        // maxInitialRequests: 入口初始并行请求上限。
        maxInitialRequests: 30,

        // maxAsyncRequests: 异步加载并行请求上限。
        maxAsyncRequests: 30,

        // automaticNameDelimiter: 自动生成 chunk 名称的连接符。
        automaticNameDelimiter: '~',

        cacheGroups: {
          // cacheGroups: 拆包分组规则。优先级越高越先匹配。
          framework: {
            test: /[\\/]node_modules[\\/](react|react-dom|vue)[\\/]/,
            name: 'framework',
            priority: 40,
            enforce: true,
          },
          vendors: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            priority: 10,
            reuseExistingChunk: true,
          },
          commons: {
            minChunks: 2,
            name: 'commons',
            priority: 0,
            reuseExistingChunk: true,
          },
        },
      },
    },

    performance: {
      // hints: 资源过大时如何提示。
      // 生产可以设 warning,CI 严格项目可设 error。
      hints: isProd ? 'warning' : false,

      // maxEntrypointSize: 入口总资源体积阈值。
      maxEntrypointSize: 512 * 1024,

      // maxAssetSize: 单个资源体积阈值。
      maxAssetSize: 512 * 1024,
    },

    stats: {
      // preset: 控制命令行构建信息详细程度。
      preset: 'normal',

      // colors: 输出彩色日志。
      colors: true,

      // errors/warnings: 显示错误和警告。
      errors: true,
      warnings: true,

      // assets/chunks/modules: 是否展示资源、chunk、模块信息。
      assets: true,
      chunks: false,
      modules: false,

      // timings: 展示构建耗时。
      timings: true,
    },

    externals: {
      // externals: 不把某些依赖打进 bundle,运行时从外部获取。
      // 适合 CDN 注入的大库,但必须保证 HTML 中已经提供全局变量。
      // lodash: '_',
    },

    experiments: {
      // lazyCompilation: 开发时懒编译动态导入和入口。
      // 大项目可提升启动速度,但首次访问某页面会触发编译等待。
      lazyCompilation: false,

      // futureDefaults: 尝试未来默认配置。
      // 学习和实验可打开,生产项目升级前要谨慎验证。
      futureDefaults: false,
    },

    watchOptions: {
      // ignored: watch 模式忽略目录。
      ignored: /node_modules/,

      // aggregateTimeout: 文件变化后延迟多久再重新构建,用来合并连续保存。
      aggregateTimeout: 200,

      // poll: 轮询监听。Docker、WSL、网络磁盘事件不稳定时可开启。
      // poll: 1000,
    },

    infrastructureLogging: {
      // level: Webpack 内部基础设施日志级别。
      level: 'warn',
    },
  }
}

20.2 Webpack 高级配置的理解框架 ​

resolve 解决“这个 import 到底指向哪个文件”。路径别名、扩展名、省略扩展名、exports 条件、Node polyfill 都属于解析问题。

module.rules 解决“这个文件内容如何变成 Webpack 能处理的模块”。loader 是从右到左、从下到上执行;test/include/exclude/resourceQuery/issuer/oneOf 决定规则是否命中。

plugins 解决“构建流程中要插入什么额外动作”。生成 HTML、抽离 CSS、定义环境变量、类型检查、模块联邦都属于插件能力。

output 解决“最终产物叫什么、放哪里、浏览器从哪里加载”。hash、publicPath、chunkFilename、assetModuleFilename 会直接影响缓存和部署。

optimization 解决“如何压缩、拆包、tree shaking、稳定 hash”。高级阶段要重点观察构建产物,而不是只看配置是否写得复杂。

devServer 只服务开发环境。它的代理、history fallback、HMR、静态目录不会自动进入生产环境。线上要由 Nginx、CDN、网关、后端或部署平台承接。

Babel 解决“源码语法和运行时兼容怎么降级”。它不是压缩器,体积控制重点在 targets、modules: false、polyfill 策略和 runtime helper 复用。

ESLintPlugin 解决“构建时如何把代码规范反馈接入开发体验”。大型项目中它通常和独立 lint 脚本一起使用。

Workbox 解决“Service Worker 如何生成、预缓存哪些资源、运行时缓存哪些请求”。PWA 配置必须在浏览器 Application 面板验证。

20.3 常见高级问题排查顺序 ​

  1. 入口没打进去:检查 context、entry、路径大小写、文件扩展名。
  2. loader 没生效:检查 test、include、exclude、oneOf 顺序、loader 执行方向。
  3. 别名没生效:检查 resolve.alias 是否绝对路径,TS 项目是否同步 tsconfig.paths。
  4. 资源 404:检查 output.publicPath、部署子路径、HTML 注入路径、CDN 前缀。
  5. hash 总变化:检查 runtimeChunk、moduleIds、chunkIds、构建时间戳注入、插件输出。
  6. tree shaking 不生效:检查 ESM、sideEffects、Babel 是否把模块转成 CommonJS、是否有副作用导入。
  7. 构建慢:打开 filesystem cache,缩小 loader include,检查 source map,分析大依赖,必要时拆分类型检查。
  8. devServer 代理正常但线上失败:记住开发代理只在本地存在,生产需要服务端反向代理或后端 CORS。
  9. prefetch/preload 不出现:确认是动态导入、magic comment 写法正确,并在构建产物页面里观察 Network。
  10. ESLint 没生效:检查 context、extensions、ESLint 配置文件类型,以及插件是否只在某个环境启用。
  11. PWA 更新异常:检查 Service Worker 生命周期、skipWaiting、缓存策略和 Application 面板中的 Cache Storage。

21. 核心机制深度补全 ​

本章补全 Webpack 学习中最容易只停留在“配置会写”的部分。真正掌握 Webpack,要能解释一次构建从 entry 到 asset 的过程,知道 loader 和 plugin 分别在什么层面介入,能用构建产物验证缓存、拆包、tree shaking 是否生效。

21.1 一次 Webpack 构建发生了什么 ​

简化流程:

text
读取配置
  -> 创建 compiler
  -> 根据 entry 创建 compilation
  -> 从入口模块开始解析依赖
  -> 对每个模块匹配 loader 并转换源码
  -> 生成模块依赖图 module graph
  -> 根据入口和动态导入生成 chunk graph
  -> 优化 chunk/module/runtime
  -> 生成 assets
  -> 写入 output.path

核心对象:

对象含义
compiler整个 Webpack 实例,代表完整构建环境
compilation一次具体构建,包含模块、chunk、asset、依赖图
module被 Webpack 管理的一个模块
chunk一组模块的集合,最终可能生成一个或多个文件
asset最终输出资源,例如 JS、CSS、图片、字体

理解这几个对象后,loader/plugin 的位置就清楚了:

  • loader 处理单个模块的源码转换。
  • plugin 介入整个构建生命周期。

21.2 entry、module、chunk、bundle 的关系 ​

text
entry
  -> module graph
  -> chunk graph
  -> bundle/assets

示例:

javascript
// main.js
import { createApp } from "vue";
import App from "./App.vue";

createApp(App).mount("#app");

button.addEventListener("click", async () => {
  const { showDialog } = await import("./dialog");
  showDialog();
});

这里:

  • main.js 是入口模块。
  • vue、App.vue、dialog 都是模块。
  • main.js 相关同步依赖进入初始 chunk。
  • import("./dialog") 生成异步 chunk。
  • 最终输出 main.[contenthash].js、dialog.[contenthash].js 等 asset。

21.3 Loader 执行链 ​

loader 的职责是把“Webpack 不认识或需要转换的文件”变成 JavaScript 模块或可被继续处理的内容。

javascript
module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          "style-loader",
          "css-loader",
          "sass-loader",
        ],
      },
    ],
  },
};

执行顺序从右到左:

text
style.scss
  -> sass-loader:Sass 转 CSS
  -> css-loader:解析 CSS import/url,转 JS 模块
  -> style-loader:运行时把 CSS 注入 style 标签

生产环境通常用 MiniCssExtractPlugin.loader 替代 style-loader:

text
style.scss
  -> sass-loader
  -> css-loader
  -> MiniCssExtractPlugin.loader:抽成独立 CSS 文件

21.4 pitch loader ​

loader 有 pitch 阶段和 normal 阶段。大多数业务不需要写 pitch loader,但理解它有助于看懂复杂 loader。

text
pitch:  style-loader -> css-loader -> sass-loader
normal: sass-loader -> css-loader -> style-loader

如果某个 pitch 返回了结果,后续链路可能被截断。这也是某些 loader 能“接管”后续处理的原因。

自定义 loader 简化示例:

javascript
module.exports = function sourceLoader(source) {
  const callback = this.async();

  transformAsync(source)
    .then(result => callback(null, result))
    .catch(error => callback(error));
};

loader 编写原则:

  • 输入输出保持清晰。
  • 不要在 loader 中做全局构建决策。
  • 使用 this.cacheable() 利用缓存。
  • 异步操作使用 this.async()。
  • source map 要正确传递。

21.5 Plugin 与 Tapable 钩子 ​

Webpack 插件基于 Tapable 事件钩子。插件可以在构建生命周期中读取和修改 compilation。

更完整的 compiler.hooks.* 与 compilation.hooks.* 定义、触发时机和选择规则,见同目录 AST与Webpack自定义Loader和Plugin系统学习文档.md 第 9.5 节。

最小插件:

javascript
class BuildInfoPlugin {
  apply(compiler) {
    compiler.hooks.thisCompilation.tap("BuildInfoPlugin", (compilation) => {
      compilation.hooks.processAssets.tap(
        {
          name: "BuildInfoPlugin",
          stage: compilation.constructor.PROCESS_ASSETS_STAGE_ADDITIONS,
        },
        (assets) => {
          compilation.emitAsset(
            "build-info.json",
            new compiler.webpack.sources.RawSource(
              JSON.stringify({ time: new Date().toISOString() }, null, 2)
            )
          );
        }
      );
    });
  }
}

module.exports = BuildInfoPlugin;

使用:

javascript
plugins: [
  new BuildInfoPlugin(),
]

常见插件介入点:

  • HTML 生成。
  • CSS 抽取。
  • 环境变量替换。
  • 类型检查。
  • 资源压缩。
  • 产物分析。
  • Module Federation。
  • PWA Service Worker。

21.5.1 Tapable 钩子类型:选错钩法会直接出错 ​

Webpack 的钩子不是同一种。Tapable 导出 9 个 Hook 类,理解它们的差异才能解释"为什么这个钩子要 tap 而那个要 tapAsync"。

javascript
const {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,
  AsyncParallelHook,
  AsyncParallelBailHook,
  AsyncSeriesHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook,
} = require("tapable");

按两个维度分类:

维度取值含义
同步 / 异步Sync* / Async*同步钩子只能用 tap 注册;异步钩子支持 tap、tapAsync(callback 风格)、tapPromise(返回 Promise)
行为语义Hook / Bail / Waterfall / Loop顺序执行 / 提前熔断 / 流水线传值 / 循环重试

各类型行为:

钩子类型执行方式返回值语义
SyncHook按注册顺序依次执行忽略所有返回值
SyncBailHook顺序执行,某个 tap 返回非 undefined 就停止返回值作为整个钩子的结果(熔断)
SyncWaterfallHook顺序执行,前一个 tap 的返回值传给下一个最终返回最后一个 tap 的返回值
SyncLoopHook某个 tap 返回非 undefined 时,从头重新执行整组全部返回 undefined 才结束
AsyncSeriesHook串行 await 每个 tap忽略返回值
AsyncSeriesBailHook串行执行,提前熔断同 SyncBailHook
AsyncSeriesWaterfallHook串行流水线传值同 SyncWaterfallHook
AsyncParallelHook并发执行所有 tap忽略返回值
AsyncParallelBailHook并发执行,提前熔断同 SyncBailHook

注册时必须传名字,这是硬性约定:

javascript
compiler.hooks.compilation.tap("MyPlugin", (compilation) => {
  // ...
});

compiler.hooks.emit.tapPromise("MyPlugin", async (compilation) => {
  // 异步工作
});

名字用于标识插件来源:错误堆栈、性能分析(profile)和调试输出都靠它定位是谁注册的回调。

对应到 Webpack 里几个高频钩子:

钩子类型为什么是这个类型
compiler.hooks.compilationSyncHook通知型:只需要拿到新建的 compilation,不改变它
compiler.hooks.emitAsyncSeriesHook产物写出前允许异步修改,所有插件按序执行完才继续
compiler.hooks.doneAsyncSeriesHook构建完成后做异步收尾(上传、通知)
compilation.hooks.processAssetsAsyncSeriesHook按 stage 串行处理产物,压缩、注入、统计各占一个阶段

⚠️ 给同步钩子用 tapAsync/tapPromise 注册、或给异步钩子传同步语义的返回值(比如该返回 Promise 时忘了返回),是自定义插件最常见的运行时报错来源。写插件前先确认目标钩子的类型,再看用哪种注册方式。

21.6 resolve:模块解析不是简单拼路径 ​

当你写:

javascript
import Button from "@/components/Button";

Webpack 要判断:

  1. @ 是不是 alias。
  2. 是否补全扩展名:.js、.ts、.vue。
  3. 是否读取 package.json 的 exports、module、main。
  4. 是否匹配条件导出。
  5. 是否跟随 symlink。

配置示例:

javascript
resolve: {
  alias: {
    "@": path.resolve(__dirname, "src"),
  },
  extensions: [".ts", ".tsx", ".js", ".vue", ".json"],
  mainFields: ["browser", "module", "main"],
}

TS 项目必须同步 tsconfig.json:

json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

否则会出现“Webpack 能跑,TypeScript 报错”或“IDE 找不到路径”的问题。

21.7 splitChunks 的思考方式 ​

代码分割目标不是“chunk 越多越好”,而是平衡:

  • 首屏加载。
  • 缓存命中。
  • 请求数量。
  • 复用程度。
  • 变更频率。

典型策略:

javascript
optimization: {
  runtimeChunk: "single",
  splitChunks: {
    chunks: "all",
    cacheGroups: {
      vue: {
        test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
        name: "vue-vendor",
        priority: 30,
      },
      vendors: {
        test: /[\\/]node_modules[\\/]/,
        name: "vendors",
        priority: 10,
      },
    },
  },
}

为什么要拆 runtime:

  • runtime 包含模块加载映射。
  • 业务代码变化可能导致 runtime 变化。
  • 把 runtime 单独拆出,有助于 vendor chunk 长缓存稳定。

验证方式:

  1. 构建两次。
  2. 只改业务代码。
  3. 观察 vendor 文件 hash 是否保持稳定。
  4. 如果 vendor hash 也变,检查 runtime、moduleIds、chunkIds、插件注入内容。

21.8 contenthash、chunkhash、hash ​

占位符含义推荐场景
[hash]整次构建 hash较少用于长期缓存
[chunkhash]chunk 级 hashJS chunk
[contenthash]内容级 hashJS/CSS/资源长期缓存

生产环境推荐:

javascript
output: {
  filename: "js/[name].[contenthash:8].js",
  chunkFilename: "js/[name].[contenthash:8].js",
  assetModuleFilename: "assets/[name].[contenthash:8][ext]",
}

缓存策略:

text
index.html:短缓存或不缓存
*.js/*.css/assets:长缓存 + immutable

原因:index.html 负责引用最新 hash 文件,不能长期缓存;hash 资源内容变了文件名会变,可以长缓存。

21.9 Tree Shaking 为什么不生效 ​

Tree Shaking 依赖静态分析。常见条件:

  1. 使用 ES Module。
  2. 生产模式。
  3. 未被 Babel 转成 CommonJS。
  4. package.json 正确声明 sideEffects。
  5. 代码没有无法分析的动态副作用。

示例:

javascript
// math.js
export function add(a, b) {
  return a + b;
}

export function heavy() {
  console.log("heavy");
}

// main.js
import { add } from "./math";
console.log(add(1, 2));

如果配置正确,heavy 有机会被删除。

副作用示例:

javascript
import "./global.css";
import "./polyfill";

这类导入不能随便删除。package.json 可以这样声明:

json
{
  "sideEffects": [
    "*.css",
    "./src/polyfill.js"
  ]
}

不要盲目写:

json
{
  "sideEffects": false
}

如果项目里有全局样式、polyfill、全局注册组件,错误声明会导致生产环境功能丢失。

21.9.1 sideEffects 与 usedExports 是两套不同机制 ​

官方文档专门澄清:sideEffects 和 usedExports(常被笼统称为 tree shaking)是两个不同的优化,效果层级不同:

机制粒度效果
sideEffects模块级可以跳过整个模块/文件乃至其完整依赖子树(跳过评估)
usedExports语句级标记未使用的导出,依赖 terser 判断语句是否有副作用,无法跳过依赖子树

sideEffects 更有效,因为它允许直接跳过整个模块。usedExports 依赖 terser 去判断每条语句是否有副作用——这在 JavaScript 里是出了名的难题,效果不如显式的 sideEffects 标记;而且按规范,副作用必须被求值,所以它无法跳过依赖子树。导出函数通常没问题,但 React 的高阶组件(HOC)这类场景就比较棘手。

sideEffects 对模块的三种处理方式:

text
include(包含)  :引入该模块、求值、继续分析其依赖。
skip over(越过):不引入、不求值,但继续分析其依赖(用于被 re-export 中转的模块)。
exclude(排除)  :不引入、不求值、也不再分析依赖。

一个典型的 sideEffects: false 误删案例:组件库 Button/index.js 里写了 import "./Button.css",如果包声明了 sideEffects: false,而消费方只 import { Button },Webpack 会认为 CSS 这个导入也没有副作用,于是把 Button.css 一起剔除——组件能渲染,样式却丢了。正确写法是把 CSS 标为有副作用:

json
{
  "name": "awesome-ui",
  "sideEffects": ["**/*.css"]
}

21.9.2 用 /*#__PURE__*/ 帮 terser 判断 ​

sideEffects 是模块级声明,/*#__PURE__*/ 是语句级注释,作用是把某条语句标记为无副作用,让 terser 敢于删除:

javascript
const Button$1 = /* #__PURE__ */ withAppProvider()(Button);

没有这个注释时,terser 面对 withAppProvider()(Button) 这种"调用函数再调用返回值"的写法很难确定有没有副作用(merge、hoistStatics 里会不会改全局?赋值会不会触发 setter?)。多数成熟的库在发布产物里已经由工具自动补上 /*#__PURE__*/,理解它有助于读懂压缩后的产物为何能被删掉。

21.9.3 开发模式验证:usedExports ​

tree shaking 的完整删除主要发生在生产模式,但可以在开发模式提前验证"标记"是否正确。给配置加 optimization.usedExports: true 并用开发模式构建,产物里会出现类似注释:

javascript
/* unused harmony export square */

这表示 Webpack 已经识别出 square 未被使用。注意此时函数体仍在产物里(真正删除交给生产模式的 terser),所以开发模式看到 unused 注释、生产模式体积没降,要去查 sideEffects 声明和是否有隐藏副作用,而不是怀疑标记没生效。

21.10 Source Map 选择 ​

source map 是源码和构建产物之间的映射。

常见选择:

场景推荐
开发调试eval-cheap-module-source-map 或类似快速配置
生产错误定位source-map,上传监控平台后不公开
生产安全敏感关闭或使用 hidden source map
构建速度优先cheap/eval 类

生产环境不要无脑公开 source map,因为它可能暴露源码、接口路径、业务逻辑。

21.11 filesystem cache ​

Webpack 5 支持文件系统缓存:

javascript
cache: {
  type: "filesystem",
  buildDependencies: {
    config: [__filename],
  },
}

优化点:

  • 缩短二次构建时间。
  • 配合 loader 缓存减少重复转换。
  • CI 环境可结合缓存目录持久化。

排查:

  • 配置变化但缓存未失效:检查 buildDependencies。
  • 缓存过大:定期清理。
  • loader 输出依赖外部文件:确保把依赖声明给 Webpack。

21.11.1 缓存默认值与关键配置项 ​

先记住一个容易出错的前提:cache 的默认值取决于 mode。

modecache 默认
productionfalse
development{ type: 'memory' }
nonefalse

也就是说,生产构建默认不缓存,要手动开启 filesystem 才能享受二次构建加速;开发模式默认是内存缓存(进程退出即失效),和文件系统缓存不是一回事。cache: true 是 cache: { type: 'memory' } 的别名。

常用配置项:

配置默认说明
cache.type见上表'memory' 或 'filesystem'
cache.buildDependencieswebpack/lib构建代码依赖(含配置文件本身),其内容变化会使文件系统缓存失效;config: [__filename] 的写法就是把配置文件本身声明进去
cache.cacheDirectorynode_modules/.cache/webpack缓存根目录
cache.cacheLocationcacheDirectory/name实际缓存文件位置
cache.name${config.name}-${config.mode}不同名字对应互不干扰的独立缓存;多份配置需要独立缓存时显式设置
cache.version''手动版本号,不同版本不会复用缓存;配置变化到"无法复用旧缓存"的程度时改它
cache.idleTimeout60000空闲多少毫秒后写盘
cache.idleTimeoutAfterLargeChanges1000检测到大变更后,多快写盘
cache.idleTimeoutForInitialStore5000首次缓存写盘的等待时间
cache.maxAge5184000000(约一个月)未使用的缓存条目保留多久
cache.compressionfalse可选 'gzip'、'brotli'、'zstd',压缩缓存文件
cache.managedPaths['./node_modules']包管理器管理的路径;Webpack 假定其版本唯一,直接用快照而不逐文件校验

cache.version 的典型用途:某次配置改动在语义上已经不能复用旧缓存,但 buildDependencies 的 hash 又没变(例如改了外部工具的行为),这时改版本号强制失效。

21.11.2 CI 中共享文件系统缓存 ​

官方文档给出的两条前提:

  1. CI 平台要支持在多次构建之间共享缓存目录。
  2. CI 任务必须在相同的绝对路径下运行——因为 Webpack 的缓存文件里存的是绝对路径,路径变了缓存就等于作废。

GitHub Actions 的常见做法是用 actions/cache 缓存 node_modules/.cache/webpack,并配置 restore-keys 回退到主分支的缓存:

yaml
- uses: actions/cache@v4
  with:
    path: node_modules/.cache/webpack
    key: ${{ runner.os }}-webpack-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-webpack-

⚠️ 两个实践中会遇到的坑:一是不要在同一个 job 里先跑 npm ci 再依赖缓存目录——npm ci 会删掉并重建 node_modules,把里面的缓存也清掉;二是多份配置如果显式设置了相同的 cache.name,缓存会互相污染,必要时给它们不同的名字(默认名字已包含配置名和 mode)。

21.12 构建性能优化 ​

优先级:

  1. 缩小 loader 命中范围:include: path.resolve(__dirname, "src")。
  2. 开启 filesystem cache。
  3. 减少不必要插件。
  4. 开发环境使用更快 source map。
  5. 类型检查放独立进程或 CI。
  6. 大依赖按需引入。
  7. 使用 bundle analyzer 看真实体积。
  8. 需要时考虑 SWC、esbuild-loader、Rspack 等替代链路。

示例:

javascript
{
  test: /\.[jt]sx?$/,
  include: path.resolve(__dirname, "src"),
  use: {
    loader: "babel-loader",
    options: {
      cacheDirectory: true,
    },
  },
}

21.13 开发环境与生产环境不要混淆 ​

开发环境关注:

  • 启动速度。
  • HMR。
  • source map。
  • 本地代理。
  • 错误提示。

生产环境关注:

  • 体积。
  • 压缩。
  • 长缓存。
  • 代码分割。
  • 安全。
  • 兼容性。

典型拆分:

text
webpack.common.js
webpack.dev.js
webpack.prod.js

配置合并:

javascript
const { merge } = require("webpack-merge");
const common = require("./webpack.common");

module.exports = merge(common, {
  mode: "production",
});

21.14 Webpack 排查方法 ​

遇到问题时,不要只盯着配置文件。按产物和链路排查:

  1. 看终端错误:是解析失败、loader 失败、插件失败还是压缩失败。
  2. 看 dist:文件是否生成,路径是否符合预期。
  3. 看 Network:资源是否 404,publicPath 是否正确。
  4. 看 source map:是否能定位源码。
  5. 看 stats:模块是否被打入预期 chunk。
  6. 看 analyzer:大包来自哪里。
  7. 看缓存:改一行业务代码时 vendor hash 是否变化。
  8. 看环境差异:devServer 正常不代表生产 Nginx 正常。

生成 stats:

bash
npx webpack --profile --json > stats.json

然后用分析工具查看模块、chunk、体积和依赖关系。

21.15 版本基线与源码阅读入口 ​

本文本次补全按 2026-07-09 核对的 npm 版本书写:

包核对版本说明
webpack5.108.4Webpack 主包
tapable2.3.3Webpack 生命周期钩子系统

💡 2026-09-01 复核:npm 上 webpack latest 为 5.110.2,本文新增的钩子类型、模块解析规则、tree shaking 机制与持久化缓存内容均按 5.110.x 官方文档核实。小版本迭代请以官方 changelog 为准。

Webpack 发布包保留了大量 lib/*.js 源码,读原理时可以直接从 node_modules/webpack/lib 下定位。

推荐入口:

文件重点
lib/webpack.jswebpack 函数入口,创建 compiler
lib/Compiler.js总调度器,定义 run/watch/compile/emit/done 等生命周期
lib/Compilation.js一次构建的状态容器,管理 modules/chunks/assets/dependencies
lib/NormalModuleFactory.js普通模块创建,负责 resolve、loader、parser 等链路
lib/NormalModule.js一个普通模块如何 build、run loaders、parse、code generation
lib/JavascriptParser.jsJS 依赖分析,识别 import、require、dynamic import 等
lib/optimize/*splitChunks、module ids、chunk ids、tree shaking 相关优化
lib/cache/*filesystem cache、memory cache
schemas/WebpackOptions.json配置 schema

读源码时先抓住三个对象:

  • Compiler:代表整个 Webpack 编译器和完整构建环境。
  • Compilation:代表一次具体构建,watch 模式下每次变更都会产生新的 compilation。
  • Module:代表被纳入依赖图的模块,可能是 JS、CSS、图片、字体、WASM 等。

21.16 Webpack 完整工作原理 ​

Webpack 的核心不是“把文件拼起来”,而是构建一张依赖图,然后把依赖图切成 chunk,最后把 chunk 渲染成 assets。

完整链路:

text
读取配置
  -> 创建 Compiler
  -> 应用 plugins
  -> run/watch
  -> 创建 Compilation
  -> 根据 entry 创建入口依赖
  -> resolve 模块路径
  -> 匹配 module.rules
  -> 执行 loaders
  -> parse AST,收集依赖
  -> 递归构建 ModuleGraph
  -> seal 阶段冻结模块图
  -> 生成 ChunkGraph
  -> tree shaking、splitChunks、runtime 处理
  -> code generation
  -> render chunk
  -> processAssets
  -> emit 到 output.path
  -> 输出 stats

21.16.1 Compiler 做什么 ​

Compiler 负责“整条流水线的调度”。它持有:

  • 配置对象。
  • 输入输出文件系统。
  • resolver factory。
  • cache。
  • hooks。
  • records。
  • watch 状态。

它本身不代表某一个模块,也不代表某一次构建结果。它更像 Webpack 运行时的主控制器。

源码级简化示例,非逐字复制:

js
class Compiler {
  run(callback) {
    this.hooks.beforeRun.callAsync(this, () => {
      this.hooks.run.callAsync(this, () => {
        this.compile((err, compilation) => {
          if (this.hooks.shouldEmit.call(compilation) === false) {
            return done();
          }

          this.emitAssets(compilation, () => {
            this.emitRecords(() => {
              const stats = new Stats(compilation);
              this.hooks.done.callAsync(stats, callback);
            });
          });
        });
      });
    });
  }
}

对应源码定位:

  • webpack/lib/Compiler.js
  • 关键方法:run、watch、compile、newCompilation、emitAssets

21.16.2 Compilation 做什么 ​

Compilation 负责“一次构建的所有中间状态和产物”。它持有:

  • modules:所有模块。
  • chunks:代码块。
  • assets:最终要输出的文件。
  • moduleGraph:模块之间的依赖关系。
  • chunkGraph:模块和 chunk 的关系。
  • errors / warnings。
  • hooks。

watch 模式下,文件改一次,通常会生成一次新的 Compilation。这也是为什么插件里不能把上一次 compilation 的临时状态无脑复用到下一次。

21.16.3 ModuleGraph 与 ChunkGraph ​

ModuleGraph 回答的是:

  • A 模块 import 了谁?
  • 谁 import 了 A?
  • 某个 export 是否被使用?
  • 模块之间的依赖边是什么?

ChunkGraph 回答的是:

  • 哪些模块被放进哪个 chunk?
  • 某个 chunk 依赖哪些 runtime?
  • 动态导入应该拆成哪个异步 chunk?
  • chunk id 和 hash 怎么生成?

两者关系:

text
源码依赖关系 -> ModuleGraph
构建输出分组 -> ChunkGraph

21.17 Webpack 生命周期:每个周期做什么 ​

Webpack 生命周期由 Tapable hooks 组织。插件本质上就是在合适的 hook 上注册函数。

21.17.1 Compiler 级生命周期 ​

阶段Hook做什么
环境初始化environmentNode 环境、文件系统等初始化后触发
环境初始化后afterEnvironment环境准备完成,可做补充初始化
配置入口entryOption处理 entry 配置
执行前beforeRun单次构建 run 前,适合准备缓存、日志
执行run构建正式开始
watch 执行watchRunwatch 模式每轮构建前
编译参数beforeCompile创建 compilation 参数前后
编译开始compile即将创建 compilation
创建 compilationthisCompilation当前 compilation 创建,适合挂 compilation hooks
compilation 已创建compilation所有插件都可感知当前 compilation
构建依赖图make从 entry 开始构建模块图
make 结束finishMake模块收集基本完成
编译结束前afterCompilecompilation 构建后、emit 前
是否输出shouldEmit可以阻止输出
输出前emitassets 即将写入磁盘,旧插件常用
单文件输出assetEmitted某个 asset 写出后
输出后afterEmit所有 assets 输出后
完成donestats 生成,构建完成
完成后afterDonedone 后同步通知
watch 失效invalid文件变化导致当前构建失效
watch 关闭watchClosewatch 模式关闭

常用选择:

  • 想新增/修改最终文件:优先用 compilation.hooks.processAssets,不要只依赖旧的 compiler.hooks.emit。
  • 想观察构建耗时:用 beforeRun/run/done。
  • 想为每次构建挂内部钩子:用 thisCompilation。
  • 想处理 watch 文件变化:用 invalid 或 watchRun。

21.17.2 Compilation 级生命周期 ​

阶段Hook做什么
模块构建前buildModule某个模块开始 build
模块构建成功succeedModule模块 build 成功
模块构建失败failedModule模块 build 失败
所有模块完成finishModules模块图构建基本完成
seal 开始seal开始冻结依赖图并生成 chunk
优化依赖optimizeDependencies优化依赖关系
优化模块optimizeModules优化 modules
优化 chunksoptimizeChunks优化 chunks
after 优化afterOptimizeChunkschunk 优化结束
产物处理processAssets新增、修改、压缩、分析 assets
seal 结束afterSealseal 阶段完成

processAssets 有多个 stage,适合精细控制插件顺序:

js
class BuildInfoPlugin {
  apply(compiler) {
    compiler.hooks.thisCompilation.tap('BuildInfoPlugin', (compilation) => {
      compilation.hooks.processAssets.tap(
        {
          name: 'BuildInfoPlugin',
          stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
        },
        () => {
          compilation.emitAsset(
            'build-info.json',
            new compiler.webpack.sources.RawSource(
              JSON.stringify({ time: new Date().toISOString() }, null, 2)
            )
          );
        }
      );
    });
  }
}

21.18 Loader 工作原理:为什么从右到左 ​

Loader 的职责是“把某种资源转换成 JavaScript 或 Webpack 能继续处理的模块内容”。

对于:

js
import './style.less';

配置:

js
module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader'],
      },
    ],
  },
};

普通执行阶段从右到左:

text
style.less
  -> less-loader:Less 转 CSS
  -> css-loader:CSS 转 JS module,并处理 @import/url()
  -> style-loader:运行时把 CSS 注入 style 标签

21.18.1 pitch 阶段 ​

Loader 还有 pitch 阶段,方向从左到右:

text
pitch: style-loader -> css-loader -> less-loader
normal: less-loader -> css-loader -> style-loader

如果某个 loader 的 pitch 返回了结果,后面的 pitch 和部分 normal 链路可能被短路。真实业务里你不常写 pitch,但理解它有助于读 style-loader、thread-loader 等源码。

源码级简化示例,非逐字复制:

js
async function runLoaders(resource, loaders) {
  for (const loader of loaders) {
    const pitched = await loader.pitch?.(resource);
    if (pitched) return runNormalLoadersBefore(loader, pitched);
  }

  let content = await readFile(resource);
  for (const loader of loaders.reverse()) {
    content = await loader.normal(content);
  }
  return content;
}

相关源码定位:

  • loader-runner/lib/LoaderRunner.js
  • Webpack 中普通模块构建入口:webpack/lib/NormalModule.js

21.18.2 Loader 上下文 ​

Loader 函数中的 this 不是普通对象,而是 loader context。常见能力:

API作用
this.async()切换为异步 loader
this.callback()返回异步结果
this.getOptions()读取 loader options
this.emitFile()生成额外文件
this.addDependency()声明额外文件依赖,watch 时会监听
this.resourcePath当前资源路径
this.sourceMap是否需要 source map

21.19 Resolve 与 alias 的实现原理 ​

Webpack 的路径解析主要由 enhanced-resolve 承担。它不是简单拼路径,而是一套插件化 resolver。

解析一个请求时会考虑:

  • context:从哪个目录开始解析。
  • resolve.alias:路径别名。
  • resolve.modules:查找模块目录,默认类似 node_modules。
  • resolve.extensions:省略扩展名时尝试的后缀。
  • resolve.mainFields:package 入口字段优先级。
  • resolve.conditionNames:package exports 条件。
  • resolve.symlinks:是否解析符号链接真实路径。
  • resolve.fallback:浏览器环境 polyfill 或替代模块。

alias 示例:

js
const path = require('node:path');

module.exports = {
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      react$: path.resolve(__dirname, 'node_modules/react/index.js'),
    },
    extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
  },
};

解析过程:

text
import Button from "@/components/Button"
  -> alias 命中 @
  -> 替换为 /project/src/components/Button
  -> 尝试 Button.tsx / Button.ts / Button.jsx / Button.js / Button.json
  -> 找到真实文件
  -> 创建 NormalModule

react$ 中的 $ 表示精确匹配,只匹配 react,不匹配 react/jsx-runtime。

21.19.1 官方解析规则:三类路径与文件/目录分支 ​

Webpack 官方把解析器要处理的路径分成三类,理解这个分类能解释大部分"解析失败"报错:

text
1. 绝对路径(/home/me/file、C:\Users\me\file)
   -> 已经是绝对路径,无需进一步解析。

2. 相对路径(../src/file1、./file2)
   -> 以 import/require 所在源文件目录为 context,
      把相对路径拼接成绝对路径。

3. 模块路径(my-module、my-module/lib/file)
   -> 在 resolve.modules 指定的所有目录中查找(默认逐层向上找 node_modules)
   -> 如果包里有 package.json,按 resolve.exportsFields 指定的字段
      (默认 exports)决定包的可导出入口
   -> 也可以先用 resolve.alias 把原模块路径替换成别名路径

拿到绝对路径后,还要判断它指向文件还是目录,两条分支规则不同:

text
指向文件:
  -> 路径已带扩展名:直接打包。
  -> 没有扩展名:按 resolve.extensions 顺序尝试(默认 ['.js', '.json', '.wasm']),
     命中第一个存在的文件。

指向目录:
  -> 目录里有 package.json:按 resolve.mainFields 顺序读取字段
     (web 目标默认 ['browser', 'module', 'main']),取第一个给出有效路径的字段。
  -> 没有 package.json,或 mainFields 都没给出有效路径:
     按 resolve.mainFiles 顺序找目录下的默认文件名(默认 ['index'])。
  -> 最后同样用 resolve.extensions 确定扩展名。

几个与默认行为相关的常见配置项:

配置默认说明
resolve.extensions['.js', '.json', '.wasm']同名多文件时按数组顺序取第一个命中的
resolve.mainFiles['index']目录入口的默认文件名
resolve.exportsFields['exports']读 package.json 中哪些字段作为包导出入口
resolve.conditionNames动态见下
resolve.byDependency按依赖类型微调让 esm、commonjs、url 等不同导入方式用不同解析规则

conditionNames 的默认值不是写死的,而是由 mode 和 target 动态组合:基础条件恒有 "webpack",加上 "production" 或 "development"(mode 为 "none" 时按 "production" 处理);再按 target 追加——web 对应 "browser"、node 对应 "node"、webworker 对应 "worker" 等。所以 target: "web" + mode: "production" 的默认条件大致是 ["webpack", "production", "browser"]。这也解释了为什么同一个包在 dev 和 prod 下可能解析到不同入口:exports 里的 development/production 条件会被分别命中。

💡 别名还有两个容易忽略的官方行为:alias 值设为 false 表示让 Webpack 忽略该模块(常用于把只在 Node 下可用的模块在浏览器构建里替换为空);键名加 $ 是精确匹配,不加 $ 则 react/jsx-runtime 这类子路径也会继续走正常解析。

21.20 Parser、Dependency 与依赖图 ​

Webpack 构建 JS 模块时,会解析 AST,并把不同语法转换为 Dependency 对象。

常见对应关系:

源码语法Dependency 类型方向影响
import x from './a'ESM import dependency静态依赖,参与 tree shaking
export { x } from './a're-export dependency影响 export usage 分析
require('./a')CommonJS require dependency同步依赖,tree shaking 能力较弱
import('./a')dynamic import dependency/block形成异步 chunk
require.context()context dependency生成上下文模块

简化理解:

text
源码字符串
  -> parser 生成 AST
  -> parser hooks 识别 import/require
  -> 创建 Dependency
  -> 加入当前 Module 的 dependencies
  -> ModuleGraph 记录边
  -> 递归构建被依赖模块

这解释了为什么:

  • 静态 import 更容易 tree shaking。
  • 动态 import() 会天然拆包。
  • 变量拼接路径会导致 context module,可能把一批文件都打进去。

21.21 Seal、优化与产物生成 ​

make 阶段重点是“把模块图建出来”。seal 阶段重点是“把模块图变成可输出的 chunk 和 asset”。

简化链路:

text
finishModules
  -> seal
  -> optimizeDependencies
  -> create chunks
  -> optimizeModules
  -> optimizeChunks
  -> assign module ids / chunk ids
  -> codeGeneration
  -> createChunkAssets
  -> processAssets
  -> afterSeal

21.21.1 Tree Shaking 在哪发生 ​

Tree Shaking 不是一个单独按钮,而是多阶段协作:

  1. Parser 识别 ESM import/export。
  2. ModuleGraph 记录 export usage。
  3. package.json 的 sideEffects 帮助判断模块是否可删除。
  4. optimization 阶段标记未使用导出。
  5. minimizer 删除死代码。

所以 tree shaking 不生效时,常见原因是:

  • 使用 CommonJS。
  • 包没有正确声明 sideEffects。
  • 代码有顶层副作用。
  • Babel/TS 把 ESM 提前转成 CommonJS。
  • 动态访问导出,静态分析失败。

21.21.2 splitChunks 在哪发生 ​

splitChunks 发生在 chunk 优化阶段。它不是按文件夹机械拆包,而是根据模块在 chunk 间的复用关系、大小、请求数、cache group 等规则决定。

典型判断:

text
模块是否来自 node_modules?
模块被多少 chunk 共享?
体积是否达到 minSize?
拆出来后请求数是否超过限制?
cacheGroups 优先级谁更高?

21.22 Webpack 源码级生命周期简表 ​

下面是一次普通 webpack --mode production 的高层顺序:

text
webpack(options)
  -> createCompiler(options)
  -> apply plugins
  -> compiler.run()
    -> beforeRun
    -> run
    -> compile()
      -> beforeCompile
      -> compile
      -> newCompilation
      -> thisCompilation / compilation
      -> make
      -> finishMake
      -> compilation.finish()
      -> compilation.seal()
      -> afterCompile
    -> shouldEmit
    -> emit
    -> assetEmitted
    -> afterEmit
    -> emitRecords
    -> done
    -> afterDone

Watch 模式的差异:

text
compiler.watch()
  -> watchRun
  -> compile
  -> done
  -> 文件变化
  -> invalid
  -> 新一轮 watchRun / compile / done

开发服务器的差异:

  • webpack-dev-server 会在内存文件系统中服务构建产物。
  • HMR 需要 HotModuleReplacementPlugin、runtime、dev server websocket/client 协作。
  • 和 Vite 的“浏览器按需请求源码模块”不同,Webpack dev server 仍围绕 bundle/chunk 构建模型工作,只是通过缓存、增量编译、内存输出提升开发体验。

21.23 Webpack 与 Vite 原理对照 ​

维度WebpackVite
开发启动先构建依赖图和 bundle/chunk先启动服务器,浏览器请求哪个模块再转换哪个模块
模块服务dev server 服务 bundle/chunkdev server 服务 ESM URL
HMR 边界依赖图、runtime、HMR pluginmodule graph、import.meta.hot、WebSocket
插件模型Tapable hooksRolldown/Rollup 风格 hooks + Vite server hooks
loader/transformloader 链处理模块源码plugin transform 处理模块源码
路径解析enhanced-resolveVite resolve plugin + Rolldown resolver 能力
生产构建Webpack 自己的 chunk graph 和 runtimeVite 8 使用 Rolldown
适合场景存量项目、复杂定制、成熟生态现代应用、快速开发、轻配置

理解差异后,配置迁移就不会只停留在“这个配置叫什么名字”:

  • Webpack 的 loader 大多迁移到 Vite 插件、框架插件或原生能力。
  • Webpack 的 resolve.alias 对应 Vite 的 resolve.alias,但 TS paths 仍要同步。
  • Webpack 的 DefinePlugin 对应 Vite 的 define,但环境变量推荐用 import.meta.env。
  • Webpack 的 devServer.proxy 对应 Vite 的 server.proxy。
  • Webpack 的 splitChunks 思路对应 Vite/Rolldown 的 advancedChunks(旧 manualChunks 已废弃/移除)或默认拆包策略,但不要机械照搬。

22. 学习资源 ​

学习建议:

  1. 不要只看配置片段,要自己手写最小项目。
  2. 每加一个 loader,都问自己“它把什么转换成了什么”。
  3. 每加一个 plugin,都问自己“它介入了哪个构建流程”。
  4. 每做一个生产优化,都用构建产物验证效果。

基于 VitePress 构建的个人学习文档知识库