Skip to content
从一份配置说起
js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/',
},
module: {
rules: [
{ test: /\.js$/, use: 'babel-loader' },
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
],
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
],
mode: 'development',
};一份配置对应一个完整的打包流程。entry、output、module、plugins、mode,这五个字段各自管辖不同的环节——它们之间的协作方式,直接决定最终产物从哪来、长什么样、落在哪里。
Entry:依赖图起点
webpack 从 entry 开始构建依赖图。它以 entry 指定的文件为入口,递归解析所有 import / require,得到一棵完整的模块树。最终生成的 bundle 包含所有可访问到的模块。
三种写法
字符串
单入口最简单的情形:
js
entry: './src/index.js'等价于 entry: { main: './src/index.js' }。未显式命名时,webpack 使用默认 chunk 名 main。
数组
js
entry: ['./src/app.js', './src/polyfill.js']数组的作用是把多个文件合并进同一个入口。webpack 会将数组中的所有文件视为该入口的组成部分,它们的依赖会被完整解析,最终输出到同一个 chunk。常见用法是在不修改业务源码的前提下,将 polyfill 与业务代码一起打包。
数组仍然是单入口——只生成一个 chunk,共享同一棵依赖图。
对象
多入口场景使用对象写法:
js
entry: {
app: './src/app.js',
admin: './src/admin.js',
}对象的每个 key 是一个独立入口,会生成独立的 chunk。每个 key 对应的值可以是字符串或数组。webpack 为每个入口分别构建一棵依赖图,最终输出对应该入口的 bundle。
多入口时的依赖图分支
对象写法的每个属性都对应一棵独立依赖图。当多个入口引用了同一个模块时,该模块默认会被打包进各自的 bundle,造成代码冗余。这时需要配合 splitChunks 或 dependOn 做公共模块提取。
数组写法仅生成一棵依赖图。这就是 entry 数组和多入口对象的核心差异:数组共享一个 chunk,对象生成多个独立 chunk。
Output:产物的落地点
output 决定构建后的文件写到哪里、以什么规则命名,以及运行时资源路径如何拼接。
filename 与 path
path:输出目录的绝对路径。filename:输出文件的命名规则,支持占位符。
js
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
}[name] 会被替换为 chunk 名称(来自 entry 的 key),[contenthash:8] 取文件内容哈希的前 8 位。文件名随内容变化而变化,浏览器在内容更新后会自动请求新文件,无需手动清缓存。
publicPath
publicPath 指定的是输出资源在运行时的引用路径前缀。它与文件实际写入磁盘的位置无关,只影响 HTML、CSS 等文件中对资源的 URL 拼接。
js
output: {
publicPath: '/assets/'
}若首页部署在 https://example.com/,以上配置会使 JS 的引用路径变为 /assets/bundle.js。部署到 CDN 时,publicPath 通常指向 CDN 地址,如 https://cdn.example.com/assets/。
多入口时的输出占位符
多入口可使用 [name] 区分不同入口对应的文件:
js
entry: { app: './src/app.js', admin: './src/admin.js' },
output: {
filename: 'js/[name].bundle.js'
}
// 输出:js/app.bundle.js、js/admin.bundle.js如果不同入口需要不同的文件名格式,也可使用函数:
js
output: {
filename: (pathData) => {
return pathData.chunk.name === 'admin'
? 'js/admin/[name].js'
: 'js/[name].js';
}
}Module:最小构建单元
在 webpack 的设计中,每个能被依赖图解析的文件都是一个 module——可以是 .js、.css、.png 或 .svg 等。
未经任何配置时,webpack 只识别 JavaScript 模块。经过 loader 处理后,任何类型的文件都可以作为模块参与到依赖图中。例如 import './style.css' 之所以生效,是因为 css-loader 将 CSS 内容转换成了 webpack 能处理的 JavaScript 模块。
源码中的每个文件,以及通过 loader 转换后生成的中间代码,都属于 module 范畴。它们正是 webpack 依赖追踪与打包的基本粒度。
Loader:把非 JS 资源变成模块
默认情况下 webpack 只理解 JavaScript 和 JSON。要让图片、CSS、TypeScript 等也能被 import,需要 loader 来转换。
module.rules 的结构
module.rules 是一个规则数组,每条规则包含 test 和 use 两个核心字段:
test:匹配文件路径的正则或函数。use:声明要应用的 loader,可以是字符串、对象或数组。
js
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
]
}以上配置的行为:当模块路径匹配 .css 结尾时,先交给 css-loader 处理,再把结果传给 style-loader。每个 loader 返回处理后的内容,作为下一个 loader 的输入。
链式调用与执行顺序
use 数组中的 loader 是从右到左执行的。css-loader 负责将 CSS 内容解析为字符串或 CSS Module 对象,style-loader 负责把前者返回的代码以 <style> 标签的方式插入页面。
这种顺序本质上对应函数组合:style(css(rawContent))。如果写成对象形式,还可以通过 enforce: 'pre' 或 enforce: 'post' 控制执行阶段,但基础的 use 数组直接体现了链式调用语义。
所有 loader 组成一条处理流水线,每个 loader 只处理自己关心的转换,出错时流程中断。
Plugin:在构建生命周期里插入逻辑
Loader 负责单个文件的转换,Plugin 则作用于整个构建流程的各个阶段。
与 Loader 的定位差异
| Loader | Plugin | |
|---|---|---|
| 作用对象 | 单文件内容 | 整个编译过程 |
| 介入方式 | 函数组合链 | 事件钩子(Tapable) |
| 典型任务 | 转译、预处理 | 生成 HTML、抽离 CSS、环境变量注入 |
Loader 本质是对内容做转换的函数;Plugin 本质是带有 apply 方法的对象,接收 compiler 实例,通过监听 compiler 上的事件来介入打包生命周期。
常见 Plugin 作用方式
HtmlWebpackPlugin 会在输出目录生成 HTML 文件,并自动注入打包后的 JS、CSS 引用:
js
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
}),
];template 指定源 HTML 文件,webpack 将生成的 bundle 以 <script> 标签的方式注入,省去手动维护引用的工作。
其他 plugin(如 MiniCssExtractPlugin)会在 module 处理阶段后将 CSS 从 JS bundle 中抽离成独立文件。这需要 loader 与 plugin 协同:loader 负责提取 CSS 内容,plugin 在资源生成阶段控制输出形态。
Mode:内置策略集
mode 是 webpack 4 引入的顶层配置,可选值:development、production、none。不同的 mode 会启用对应的内置优化和默认配置。
mode 通过 webpack 内置的 DefinePlugin 为 process.env.NODE_ENV 设置对应的值,这是许多第三方库判断运行环境的主要依据。
development
process.env.NODE_ENV的值被设为"development"- 启用模块路径命名(
[path][name])和NamedModulesPlugin等价能力,便于调试 - devtool 默认值为
eval,重编译速度快 - 不开启压缩
production
process.env.NODE_ENV的值被设为"production"- 启用
TerserPlugin等压缩插件 - 默认开启 scope hoisting
- 模块名使用短 id 以减小体积
- 开启 tree shaking、副作用标记等优化
none
关闭所有内置优化,适合需要完全手动控制内部行为的场景。
同份配置仅切换 mode,输出产物的体积与结构就会完全不同——这是内置策略直接生效的结果。
Chunk 与 Bundle:中间产物与最终文件
chunk 是构建阶段的中间代码块,bundle 是最终输出的静态资源文件。
Chunk 的生成
模块按依赖关系合并成 chunk,主要来自三类来源:
- 入口 chunk:由 entry 直接生成
- 异步导入 chunk:通过
import()动态引入的模块会形成独立 chunk
js
// 这段代码会生成一个独立的异步 chunk
import('./utils').then(module => {
module.doSomething();
});- 公共 chunk:通过
splitChunks提取的共享模块 chunk
js
optimization: {
splitChunks: {
chunks: 'all',
}
}以上配置会将所有入口共享的模块自动提取为一个独立 chunk,避免重复打包。
chunk 的生成发生在编译的 optimization 阶段,它决定哪些模块合并为一个代码块,此时尚未产生实际文件。
Bundle 的落地
生成 chunk 之后,webpack 进入 emit 阶段,将 chunk 转换为实际文件写入磁盘。一个 chunk 通常对应一个 bundle,但启用 source map 时,每个 chunk 会同时生成 .js 和 .map 两个文件。更准确的说法是:一个 chunk 可以对应多个 output asset。
区分意义
理解 chunk 与 bundle 的差异,有助于分析打包结果和排查代码分割是否正确。在 webpack 构建日志或分析工具(如 webpack-bundle-analyzer)中看到的节点是 chunk,而最终返回到浏览器的文件是 bundle。
阅读一份 webpack 配置
回到开头那份配置,可以按上述概念拆解:
entry: './src/index.js'→ 单入口,默认 chunk 名mainoutput.path和filename→ 输出到dist/bundle.jspublicPath: '/'→ 运行时资源路径以根目录为准module.rules→ 对.js和.css分别配置 loader;CSS 的use数组表明先执行css-loader,再执行style-loaderplugins→ 使用HtmlWebpackPlugin生成 HTML 并自动引用产物mode: 'development'→ 开启开发模式的内置行为
理解这份配置的运转逻辑之后,再遇到更复杂的配置(多 entry、代码分割、环境变量注入等)也可以沿着同样的思路拆解。
注意点
- entry 数组是单入口:它只是将多个文件合并到同一个 chunk,不是多个入口各对应一棵依赖图。
- output.publicPath 不影响文件落盘路径:它只影响 HTML/CSS 等文件中引用资源时拼接的 URL 前缀,部署时需要与实际服务器路由对齐。
- loader 顺序由函数组合决定:
use数组中写在后面的 loader 先执行。 - plugin 依赖 Tapable 事件:plugin 在 compiler 和 compilation 生命周期注册钩子。代码中出现的
compilation.hooks.processAssets等调用本质上就是事件订阅。 - mode 的默认行为可被显式覆盖:通过
optimization或devtool等字段可以覆盖 mode 的内置预设,手动配置的优先级更高。 - Chunk 与 Bundle 并非严格一一对应:简单模式下是一个 chunk 对应一个 bundle,但启用 source map 或某些 plugin 时,一个 chunk 可能生成多个文件。准确地说,一个 chunk 可以对应多个 output asset。
