Webpack

Webpack 是前端工程化里最有代表性的工具之一。它不是一个单纯的“打包命令”,而是一套围绕模块图构建、资源处理、代码转换、运行时加载建立起来的工程系统。

理解 Webpack,不只是为了会配几个 loader,更重要的是理解现代前端项目为什么会需要这样一层中间系统。

一、Webpack 解决的核心问题

今天的前端项目通常包含很多种资源:

  • JavaScript / TypeScript
  • CSS / Less / Sass
  • 图片、字体、SVG
  • 按路由或页面拆分的模块
  • 需要按环境区分的配置

浏览器本身并不天然理解这些工程需求,例如:

  • 如何把 TypeScript 转成浏览器能执行的代码
  • 如何让 CSS 也能通过模块化方式参与依赖图
  • 如何按需加载某一块代码
  • 如何处理第三方包与业务模块之间的关系

Webpack 的答案是:把一切都纳入模块图,然后通过可扩展的转换机制产出浏览器可运行的资源

二、先理解 Webpack 的核心抽象

1. Entry

入口决定了从哪里开始构建依赖图。

module.exports = {
	entry: './src/index.js',
}

这表示:从 src/index.js 出发,递归分析它依赖的所有模块。

2. Module

Webpack 里,模块不只是 js 文件。理论上,任何资源都可以通过适当处理变成模块。

例如:

  • import './index.css'
  • import logo from './logo.png'

3. Loader

loader 的职责是:把某种类型的资源转换成下一步可处理的模块内容

它更像编译流水线中的转换器。

例如:

  • ts-loader:把 TypeScript 转成 JavaScript
  • babel-loader:做语法转换
  • css-loader:让 CSS 能进入依赖图
  • style-loader:把样式注入页面

4. Plugin

如果说 loader 主要处理“单个模块怎么转换”,那么 plugin 更像对整个构建生命周期动手。

例如:

  • 生成 html
  • 提取 CSS
  • 注入环境变量
  • 分析打包结果
  • 压缩资源

5. Output

最终 Webpack 会把依赖图转成浏览器可消费的资源,并输出到指定目录。

三、Webpack 的工作流程

可以把一次构建粗略理解为下面几步:

  1. entry 出发
  2. 解析模块依赖
  3. 遇到不同资源类型时调用对应 loader
  4. 形成完整模块图
  5. 在构建生命周期各阶段执行插件
  6. 生成一个或多个 chunk
  7. 输出最终资源

这个流程就是 Webpack 的骨架。后面很多配置,本质上都只是参与这个过程的不同节点。

四、为什么 Webpack 曾长期成为主流

1. 它足够通用

无论你是做:

  • 单页应用
  • 多页面应用
  • 组件库
  • 老项目升级
  • 复杂企业后台

几乎都能找到一套 Webpack 方案。

2. 它的生态极强

loaderplugin 生态让它几乎能覆盖前端工程中的绝大多数需求。

3. 它形成了一套行业工程范式

例如这些今天看起来很自然的东西,很多都和 Webpack 时代的普及有关:

  • 模块化引入样式
  • 构建时注入环境变量
  • 代码分割与懒加载
  • 静态资源指纹
  • 开发服务器和热更新

五、真正难的不是配置语法,而是理解“模块图”

很多人学 Webpack 卡住,不是因为记不住配置,而是因为没有形成模块图思维。

举个例子:

import './styles/index.css'
import App from './App'

在业务视角里,这只是两行导入;但在 Webpack 视角里,这是:

  • 一个 JS 模块依赖另一个 JS 模块
  • 同时还依赖一个 CSS 模块
  • CSS 模块要经过多级 loader 转换
  • 最终这部分资源会进入不同的 chunk 或运行时注入逻辑

一旦你有了模块图视角,Webpack 的很多行为都会更清楚。

六、一个典型配置应该怎么读

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

module.exports = {
	mode: 'development',
	entry: './src/index.ts',
	output: {
		filename: 'js/[name].[contenthash:8].js',
		path: path.resolve(__dirname, 'dist'),
		clean: true,
	},
	resolve: {
		extensions: ['.ts', '.js'],
		alias: {
			'@': path.resolve(__dirname, 'src'),
		},
	},
	module: {
		rules: [
			{
				test: /\.ts$/,
				use: 'babel-loader',
				exclude: /node_modules/,
			},
			{
				test: /\.css$/,
				use: ['style-loader', 'css-loader'],
			},
		],
	},
	plugins: [
		new HtmlWebpackPlugin({
			template: './public/index.html',
		}),
	],
	devServer: {
		port: 3000,
		hot: true,
	},
}

读这类配置时,不要把它看成“很多零碎选项”,而要理解它在描述:

  • 从哪里开始构建
  • 遇到不同资源怎么处理
  • 如何生成最终产物
  • 开发环境如何运行

七、Loader 链为什么容易让人迷糊

因为 loader 经常是串联执行的。

例如:

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

你需要理解两个关键点:

  • 它们是链式转换
  • 执行顺序通常是从右到左

所以这里实际含义更接近:

  1. postcss-loader 先处理 CSS
  2. css-loader 把 CSS 变成可被 JS 引用的模块
  3. style-loader 再把样式注入页面

八、Plugin 为什么更体现 Webpack 的深度

Webpack 真正强大的地方,很多时候不在 loader,而在插件系统。

因为插件可以介入构建生命周期,例如:

  • 编译开始前
  • 模块解析阶段
  • 资源生成阶段
  • 输出文件前后

这使它非常适合复杂工程需求,但也意味着:

  • 配置理解成本高
  • 构建链排查难度高
  • 插件之间可能相互影响

九、代码分割与运行时

Webpack 最重要的能力之一,是把一个大型应用拆成多个 chunk。

例如:

const Detail = () => import('./pages/detail')

这意味着:

  • detail 页面代码不会进初始主包
  • 只有真正访问时才异步加载
  • Webpack runtime 会负责在浏览器里加载对应 chunk

这就是现代前端懒加载的基础之一。

为什么理解 runtime 很重要

很多构建问题并不发生在“打包阶段”,而发生在“浏览器如何加载打出的 chunk”阶段,例如:

  • 资源 publicPath 错误
  • 动态 chunk 404
  • CDN 缓存导致主包和子包版本不一致

十、性能优化不能只背结论

很多人记住:

  • 开启缓存
  • 开启分包
  • 压缩代码
  • 提取公共依赖

但优化真正重要的是知道你在优化哪一层

1. 构建性能

关注:

  • loader 是否过多
  • 是否重复转译 node_modules
  • Source Map 是否过重
  • 插件是否过多串行执行

2. 产物性能

关注:

  • 首屏包体积
  • 公共 chunk 是否合理
  • Tree Shaking 是否有效
  • 资源缓存策略是否合理

3. 运行时性能

关注:

  • 懒加载边界是否合理
  • 是否产生太多细碎请求
  • chunk 加载时机是否影响用户体验

十一、Webpack 最常见的几个坑

1. 别名只配了一处

导致:

  • 构建能过
  • TypeScript 报错
  • 测试跑不通

因为不同工具的模块解析配置通常彼此独立。

2. Source Map 过重

在大型项目中,过于详细的 Source Map 会拖慢开发构建速度。

3. Loader 配置顺序错误

这类错误很隐蔽,常表现为:

  • CSS 没生效
  • TS 能编译但运行异常
  • 某些资源路径不对

4. 环境变量注入理解错误

很多环境变量并不是“运行时从 process.env 读取”,而是在构建阶段被替换进去的。

十二、Webpack 与 Vite、Rollup 的关系

Webpack 和 Vite

  • Webpack 偏向统一处理开发与构建全过程
  • Vite 更强调开发态的轻量和现代浏览器能力

Webpack 和 Rollup

  • Webpack 更适合复杂应用工程
  • Rollup 通常更适合库打包和更纯粹的 ESM 输出

十三、核心源码解读:Webpack 到底是怎么跑起来的

如果你想读 Webpack 源码,最容易迷路的地方在于:类很多、钩子很多、阶段很多。

正确方法不是一开始就记所有类名,而是先建立下面这条主线:

  • Compiler 负责整个构建生命周期
  • Compilation 负责一次具体编译产物的构建现场
  • NormalModuleFactory 负责把请求变成模块
  • Parser 负责分析模块里的依赖
  • ChunkGraph 负责模块和 chunk 的组织关系

1. 总入口:webpack(options)

从源码心智模型上看,顶层入口做的事情并不神秘:

  1. 标准化配置
  2. 创建 Compiler
  3. 挂载内置插件和用户插件
  4. 根据模式决定是 run 还是 watch

一个极简化伪代码可以理解成:

function webpack(options) {
	const compiler = new Compiler(options.context)
	new NodeEnvironmentPlugin().apply(compiler)
	applyWebpackOptions(compiler, options)
	compiler.hooks.initialize.call()
	return compiler
}

这里要抓住的重点是:Webpack 很多能力并不是硬编码在一个大函数里,而是通过插件在 Compiler 生命周期上层层装配进去的。

2. Compiler:全局调度中心

Compiler 更像“总导演”,它负责:

  • 管理生命周期钩子
  • 创建 Compilation
  • 驱动构建、监听、输出
  • 协调文件系统、缓存、日志

你可以把它想象成“构建系统级别的控制器”。

很多常见钩子都挂在这里,例如:

  • beforeRun
  • run
  • compile
  • make
  • emit
  • done

3. Compilation:一次编译的工作现场

如果说 Compiler 是总导演,那么 Compilation 就是某一场具体拍摄。

它主要负责:

  • 收集模块
  • 维护依赖关系
  • 创建 chunk
  • 组织资源产物
  • 触发优化流程

很多“为什么这次构建结果是这样”的问题,最后都要落回 Compilation

4. 模块是怎么被创建出来的

Webpack 从入口开始分析依赖时,不是直接把源码塞进图里,而是经历大致这样的流程:

  1. 解析请求字符串
  2. 通过 Resolver 找到真实文件
  3. 交给 NormalModuleFactory 创建模块对象
  4. 调用 loader 链转换源码
  5. 交给 Parser 继续分析新的依赖

它的核心思想是:

每个模块在进入图之前,先要被“解析、转换、再分析”。

5. loader-runner 的位置要理解清楚

很多人把 loader 当成 Webpack 本体,但其实更准确地说:

  • Webpack 负责调度模块生命周期
  • loader-runner 负责执行具体的 loader 链

这也是为什么 loader 机制看起来像“编译流水线中的一段子系统”。

一个非常简化的理解模型可以写成:

const result = runLoaders({
	resource: moduleResource,
	loaders,
})

执行结束后,返回的结果会再交回模块系统继续解析。

6. Parser 为什么是模块图扩张的关键

真正让依赖图不断长大的,不是入口本身,而是 Parser 对每个模块源码的分析。

例如当它看到:

import foo from './foo'
import('./bar')
require('./baz')

它会把这些语法转成不同类型的依赖对象,再把这些依赖挂回当前模块上。

这一步很关键,因为:

  • 同步依赖会进入当前构建链
  • 动态导入会影响异步 chunk 划分
  • 不同依赖类型会触发不同模板和运行时代码生成策略

7. 从 ModuleGraphChunkGraph

这是 Webpack 很核心但也很容易混淆的一层:

  • ModuleGraph 关心“模块依赖谁”
  • ChunkGraph 关心“哪些模块被放进哪个 chunk”

也就是说:

  • 依赖关系是一张图
  • 输出组织又是另一张图

理解这一点后,你就更容易明白为什么:

  • 模块能存在,但不一定进初始 chunk
  • 一个模块可能被多个 chunk 共享
  • splitChunks 本质是在改写 chunk 组织策略

8. seal 阶段为什么重要

Compilation 中,一个很值得关注的阶段是 seal

可以把它理解成:

  • 模块收集大体完成
  • 开始冻结结构并进行 chunk 组织
  • 进入优化、代码生成、资源生成阶段

很多 chunk 划分、运行时代码生成、资源产物组织,都在这之后逐步成形。

9. 运行时代码是怎么来的

很多人以为打包产物只是“把源码拼起来”,其实不是。

Webpack 会生成相当一部分运行时代码来负责:

  • 模块缓存
  • 模块执行
  • 异步 chunk 加载
  • publicPath 处理
  • 热更新接口

这也是为什么理解 RuntimeModule、模板生成、chunk loading 很重要——它们直接决定浏览器如何执行构建产物。

十四、源码阅读顺序建议

如果你准备真的看源码,推荐顺序是:

  1. 顶层 webpack(options)Compiler
  2. run/watch/compile/make 主链路
  3. Compilation 如何收集模块
  4. NormalModuleFactoryloader-runner
  5. Parser 如何产出依赖
  6. ModuleGraph / ChunkGraph
  7. seal、代码生成、emit

这个顺序比直接扑进插件源码高效得多。

十五、什么时候你真正算学会了 Webpack

不是能抄一个配置,而是你能解释清楚下面这些问题:

  • 为什么某个资源会走这条 loader 链
  • 为什么某个 chunk 会被单独拆出去
  • 为什么热更新会失效
  • 为什么构建快慢受哪些环节影响
  • 为什么线上动态资源会 404

总结

Webpack 的本质,不只是一个打包工具,而是前端工程的模块化编排系统。

Webpack 的关键,不在于死记配置,而在于理解:

  • 入口如何形成依赖图
  • 资源如何通过 loader 转换
  • 生命周期如何被 plugin 扩展
  • 产物如何通过 runtime 在浏览器中运行

当你理解这些后,很多看起来复杂的配置都会变得有章可循。

上次更新:
贡献者: Joe, joe