这段时间我跟着课程搭建了一套 Webpack 配置。刚开始,我主要想弄明白 entry、module、plugins 和 output 分别怎么写。后来沿着"源代码如何变成可访问的页面"这条线重新阅读,发现这些配置背后有更值得记录的工程化思路。
这篇文章结合几段实际配置,整理我目前的理解。
通过入口扫描,减少重复注册
项目会扫描 app/pages 下符合 entry.*.js 规则的文件,并把它们加入 Webpack 入口:
ini
const pageEntries = {};
const entryList = path.resolve(
process.cwd(),
"./app/pages/**/entry.*.js",
);
glob.sync(entryList).forEach((file) => {
const entryName = path.basename(file, ".js");
pageEntries[entryName] = file;
});
module.exports = {
entry: pageEntries,
};
例如,创建:
bash
app/pages/page1/entry.page1.js
构建程序就能发现这个页面。
如果每新增一个页面,都要到 Webpack 配置里手动添加 entry,页面多了以后,配置文件会持续增长,也容易漏改。这里采用了目录和文件名约定,让页面自己"声明"它是一个入口。
我从这段配置学到的第一个思想是:把重复的人工操作沉淀为统一约定。当然,约定也要足够清楚。文件名如果不符合规则,页面就不会进入构建流程,因此命名规则本身也是项目文档的一部分。
从页面模板反推构建产物
发现入口后,配置会为每个入口创建一个页面模板:
css
new HtmlWebpackPlugin({
filename: path.resolve(
process.cwd(),
"./app/public/dist/",
`${entryName}.tpl`,
),
template: path.resolve(
process.cwd(),
"./app/view/entry.tpl",
),
chunks: [entryName],
});
这段代码建立了页面入口和模板之间的对应关系:
entry.page1.js → entry.page1.tpl
所有页面可以共用 entry.tpl 中的基础 HTML 结构,但每个生成的模板会注入对应入口所需的构建资源。
以前我看到 output 或 filename,会把它们当成"文件放在哪里"的配置。现在我会多问一步:生成的文件接下来由谁读取?
在这个项目里,.tpl 会交给页面服务渲染。因此模板的名称和路径需要与页面服务的查找方式对应。构建产物不是只要生成成功就行,还必须能被运行中的程序正确使用。
通过 Loader 规则控制处理范围
Webpack 需要处理 JavaScript、Vue 组件、Less、图片等不同文件。Loader 规则告诉它遇到某类文件时该怎样转换。
项目中的 JavaScript 规则限定了处理范围:
css
{
test: /.js$/,
include: [
path.resolve(process.cwd(), "./app/pages"),
],
use: {
loader: "babel-loader",
},
}
test 指定文件类型,include 指定目录范围。这里将 Babel 转换集中在页面业务代码中。
我从这段配置认识到,工程化不能只考虑"规则是否生效",还要考虑"规则作用于哪些文件"。处理范围越明确,越容易判断构建过程中发生了什么,也能减少不必要的转换工作。
Less 的规则则让我理解了文件转换的顺序:
bash
{
test: /.less$/,
use: ["style-loader", "css-loader", "less-loader"],
}
Loader 从右向左执行:
Less
→ CSS
→ 解析 CSS 中的依赖
→ 将样式加入页面
这几项配置需要连起来看。前一步的输出是后一步的输入,所以 Loader 的排列顺序会直接影响构建结果。
拆分配置,是因为环境目标不同
项目把 Webpack 配置分成三个文件:
csharp
webpack.base.js
webpack.dev.js
webpack.prod.js
开发和生产配置分别在基础配置上增加自己的内容:
php
const baseConfig = require("./webpack.base.js");
const webpackConfig = merge.smart(baseConfig, {
mode: "development",
devtool: "eval-cheap-module-source-map",
// 其他开发环境配置
});
基础配置保存入口扫描、模块处理等公共规则。开发配置关注源码定位和热更新;生产配置负责资源提取、压缩和输出。
这让我理解了配置拆分的原因:开发和生产使用同一份源代码,但需要不同的构建结果和反馈方式。开发时,我希望修改代码后尽快看到效果,也希望报错能对应到源文件。部署后,浏览器需要加载体积合理、地址正确、能够利用缓存的资源。
把公共规则放在基础配置里,可以避免两份配置逐渐出现不一致;把环境差异分别写出,也方便解释每项配置服务于什么目标。
从模板写盘,看构建工具如何与其他程序协作
开发环境使用 webpack-dev-middleware 提供构建资源,其中有一项配置:
css
devMiddleware(compiler, {
writeToDisk: (filePath) => filePath.endsWith(".tpl"),
publicPath: webpackConfig.output.publicPath,
});
开发时,构建服务可以直接提供资源,不必把每个文件都写入磁盘。但项目中的页面模板需要由另一个服务从文件系统读取,因此配置选择只把 .tpl 写到磁盘。
这段配置让我印象很深。单看 Webpack,可能会觉得"为什么只写出模板";把页面访问过程连起来就容易理解了:
Webpack 生成模板
→ 模板写入磁盘
→ 页面服务读取并渲染模板
→ 浏览器加载前端资源
我从这里学到:构建流程要考虑产物的使用方。当前端构建工具和页面服务协作时,双方怎样找到文件、怎样读取文件,都会影响构建配置。
从热更新,理解开发反馈流程
开发配置会把热更新客户端加入页面入口,并通过中间件建立更新通道:
ini
baseConfig.entry[v] = [
baseConfig.entry[v],
"webpack-hot-middleware/client?...",
];
开发者修改源文件后,Webpack 重新编译,浏览器接收更新消息。
我以前把"提升开发效率"理解成缩短打包耗时。现在觉得还要看修改代码到观察结果之间,有多少步骤需要自己完成。文件监听、重新编译、通知浏览器连成一条流程后,开发者可以更快确认修改效果。
不过,配置中出现热更新插件,不代表所有改动都一定能保留页面状态并局部替换。我还需要在浏览器中实际修改组件,观察它是热替换还是重新加载页面。
从代码拆分,理解浏览器缓存
基础配置使用 splitChunks 提取第三方依赖和多个页面共用的代码:
yaml
splitChunks: {
chunks: "all",
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: "vendor",
priority: 20,
enforce: true,
},
common: {
name: "common",
minChunks: 2,
priority: 10,
},
},
}
输出文件名中还加入了哈希:
ini
filename: "js/[name]_[chunkhash:8].bundle.js"
我对这部分的理解是:第三方依赖和业务页面的变化频率不同。把它们拆成不同资源后,未变化的文件有机会继续使用浏览器缓存;文件内容变化时,新的文件名又能让浏览器获取更新后的资源。
这也是为什么我认为代码拆分不能只停留在"配置了 splitChunks"。还应该检查最终产物:生成了几个文件、各自包含什么、页面实际请求了哪些资源。拆分是否合理,需要结合这些结果判断。
我目前对前端工程化的理解
沿着这份配置看下来,我把构建流程整理成了一条线:
按约定发现入口
→ 将不同源文件转换为模块
→ 生成页面需要的模板和资源
→ 在开发环境提供快速反馈
→ 在生产环境组织和压缩产物
→ 由浏览器加载并运行