前言
对前端工程师而言,前端工程化早已不是 "可选技能",而是应对大型项目开发的 "必备能力"------ 它能解决多人协作的效率问题、代码维护的混乱问题,以及项目从开发到上线的落地问题。今天,我们将聚焦 "多页面前端项目",通过案例拆解工程化的落地实践,我们一起把理论转化为可复用的开发流程。
不过,在进入实践前,建议你先掌握几个核心前置知识(也是工程化的基础):
-
前端工程化的核心价值是什么?它能解决哪些实际开发痛点?
-
Webpack 作为工程化的核心工具,其编译打包的底层原理是怎样的?
-
Webpack 的两大核心能力(Loader 和 Plugin)分别承担什么角色,如何协同工作?
若你对上述内容还不熟悉,可以先阅读以下👇文章补充基础,再继续推进实践部分:
标题 | 连接 |
---|---|
你知道Webpack解决的问题是什么嘛? | juejin.cn/post/754650... |
深入理解:Webpack编译原理 | juejin.cn/post/754655... |
Webpack 核心双引擎:Loader 与 Plugin 详解 | juejin.cn/post/754655... |
不同模式的配置:适配开发与生产的差异化需求
在多页面前端项目的工程化实践中,开发时态 与生产时态的目标差异显著:开发时需追求 "热更新速度""调试便利性",生产时则需侧重 "代码压缩率""资源优化度"。 因此,我们需要按 "基础配置 + 环境差异化配置" 的思路拆分 Webpack 配置,实现不同模式的精准适配。

一、配置文件拆分:三文件分工明确
我们将 Webpack 配置拆分为 3 个核心文件,通过 "基础配置复用 + 环境配置补充" 的逻辑,兼顾效率与灵活性:
-
webpack.base.js
(基础配置) :存放开发与生产环境的公共配置,如多页面入口规则、Loader 基础配置(如 Babel 处理 JS、css-loader 处理样式)、插件公共依赖(如 HtmlWebpackPlugin 生成 HTML),避免重复编写相同逻辑; -
webpack.dev.js
(开发环境配置) :仅包含开发时专属配置,如 HMR 热更新开关、devtool 调试源码映射(如eval-cheap-module-source-map
)、开发服务器(devServer)配置等,保障开发体验; -
webpack.prod.js
(生产环境配置) :聚焦生产环境的优化需求,如代码压缩插件(TerserPlugin 压缩 JS、CssMinimizerPlugin 压缩 CSS)、资源哈希命名(避免缓存问题)、Tree-Shaking 无用代码剔除,happypack提升打包速度等。
这种拆分方式既减少了配置冗余,又能让不同环境的配置逻辑更清晰,后续维护时无需在一套复杂配置中反复切换条件判断。
二、配置融合与启动文件:实现环境切换
拆分后的配置需要 "融合生效"------ 开发时需将 webpack.dev.js
与 webpack.base.js
合并, 生产时则合并 webpack.prod.js
与 webpack.base.js
。因此,我们额外创建两个启动文件,负责配置融合与 Webpack 启动:
dev.js
(开发启动文件) :引入webpack-merge
工具,将webpack.dev.js
的配置合并到webpack.base.js
中,再调用 Webpack 执行编译,同时启动开发服务器;prod.js
(生产启动文件) :同样通过webpack-merge
合并webpack.prod.js
与webpack.base.js
,执行生产环境的编译流程,输出优化后的构建产物(如dist
目录)。
三、脚本命令配置:一键启动不同环境
为了避免每次启动时手动输入冗长的命令,我们在 package.json
的 scripts
字段中配置快捷指令,实现 "一键启动":
js
{
"scripts": {
"build:dev": "node ./app/webpack/dev.js", // 启动开发环境:执行开发启动文件
"build:prod": "node ./app/webpack/prod.js" // 构建生产产物:执行生产启动文件
}
}
后续开发时,只需在终端执行对应命令即可切换环境:
- 执行
npm run build:dev
:启动开发环境,自动开启热更新与调试模式,代码变更后无需手动刷新页面; - 执行
npm run build:prod
:构建生产环境的优化产物,直接用于项目上线部署。
多入口动态配置:告别手动维护,适配多页面场景
在多页面前端项目中,若每个页面都手动配置入口文件和 HTML 模板,不仅会导致配置代码冗余,后续新增或删除页面时还需频繁修改配置 ------ 而 "多入口动态配置" 的核心思路,就是通过自动扫描页面目录、批量生成配置,解决这一痛点,让多页面配置更灵活、易维护。
一、核心逻辑:按约定扫描,自动生成入口与模板
多入口动态配置的关键是 "约定目录结构 + 自动化工具":先约定页面入口文件的存放规则(如所有入口文件命名为 entry.xx.js
并放在 app/pages
目录下),再用 glob
工具扫描该目录,批量生成 Webpack 入口配置(entry
)和 HTML 模板配置(HtmlWebpackPlugin
实例)。
以下是具体实现代码及关键步骤解析:
js
const pageEntries = {};
const HtmlWebpackPluginList = [];
//获取 app/pages 目录下所有入口文件(entry.xx.js)
const entryList = path.resolve(process.cwd(), "./app/pages/**/entry.*.js");
glob.sync(entryList).forEach(file => {
const entryName = path.basename(file, ".js");
//构造entry
pageEntries[entryName] = file;
//构造最终渲染的页面文件
HtmlWebpackPluginList.push(new HtmlWebpackPlugin(
{
//产物(最终模板) 输出路径
filename: path.resolve(process.cwd(), "./app/public/dist/", `${entryName}.tpl`),
// 要指定使用的模板文件
template:path.resolve(process.cwd(), "./app/view/entry.tpl") ,
//要注入的代码块
chunks: [entryName],
}
))
})
js
module.exports = {
//入口配置
entry: pageEntries,
plugins:[
// 构造最终渲染的页面模板
...HtmlWebpackPluginList,
]
}
代码分包:用 SplitChunksPlugin 实现缓存优化与加载效率提升
在多页面前端项目中,随着页面增多和第三方依赖引入,打包后的文件体积会逐渐增大 ------ 若所有代码打包成一个文件,不仅会导致首屏加载变慢,还会浪费浏览器缓存(哪怕只改一行业务代码,用户也需重新下载整个文件)。而 SplitChunksPlugin
(Webpack 内置插件)的核心价值,就是通过 "按策略拆分代码",解决这一问题,实现 "改动频率不同的代码分离存储",最大化利用浏览器缓存。
一、分包核心逻辑:从 "宏观策略" 到 "精准拆分"
SplitChunksPlugin
并非简单按文件大小拆分,而是从实际业务场景 和缓存利用效率 的宏观角度出发,通过配置 "缓存组(cacheGroups)" 定义分包规则。其核心思路可概括为:
将代码按 "改动频率" 和 "复用性" 分别打包成独立文件,让浏览器仅重新下载改动过的部分:
- 第三方依赖(vendor) :如
vue
、lodash
等 node_modules 中的库,基本不会改动(除非升级依赖版本),单独打包后可长期缓存; - 业务公共代码(common) :如多页面复用的组件、工具函数,改动频率低,单独打包后可在多页面间共享缓存;
二、完整配置解析:每一行代码的作用
js
optimization: {
// 代码分割 -- 分包
/**
* 把 js 文件打包成 3种类型
* 1. vendor :第三方 lib 库 , 基本不会改动,除非依赖版本升级
* 2. common : 业务组件的公共部分抽取出来,改动较少
* 3. entry.{page}: 不用页面entry里的业务最近代码的差异部分, 会经常改动
* 目的: 把改动和引用频率不一样的js 区分出来,以达到更好利用浏览器缓存的效果
*/
splitChunks: {
chunks: 'all', // 对同步和异步模块都进行分割
maxAsyncRequests: 10, // 每次异步加载的最大并行请求数
maxInitialRequests: 10, // 入口点最大并行请求数
//缓存组:定义具体的分包规则,按优先级生效
cacheGroups: {
//第三方依赖库
vendor: {
test: /[\\/]node_modules[\\/]/, // 打包node_modules下的第三方库
name: "vendor", //模块名称
priority: 20, //优先级:数字越大的优先级越高
enforce: true, // 是否强制执行
reuseExistingChunk: true, //复用已有的公共chunk
},
//业务组件的公共部分
common: {
name: "common",//模块名称
minChunks: 2, //最少被引用的次数 -> 被两处应用即被归为公共模块
minSize: 1, //模块最小体积 --> 最小分割文件大小(1 byte)
priority: 10, //优先级:数字越大的优先级越高
reuseExistingChunk: true, //复用已有的公共chunk
},
}
},
手搓 HMR 热更新:从核心原理到 Express 实现
在多页面前端项目的开发过程中,"修改代码后手动刷新页面" 是低效且影响体验的操作 ------ 而 HMR(Hot Module Replacement,模块热替换)的核心价值,就是让代码改动后无需刷新整个页面,仅更新变化的模块,既保留页面状态,又提升开发效率。接下来,我们将从原理拆解到代码实现,手把手搭建一套基于 Express 的 HMR 热更新方案。
一、HMR 热更新核心原理:两大核心能力缺一不可
要实现 HMR,本质上需要解决两个关键问题:"如何知道文件改了" 和 "如何通知浏览器更新"。这对应 HMR 的两大核心能力:
-
文件监控能力:实时监听业务文件(如 JS、CSS、页面模板)的改动,一旦文件变化,立即触发后续更新流程;
-
浏览器通讯能力:在文件改动后,主动通知浏览器 "哪些模块变了",并引导浏览器仅加载更新的模块,而非刷新整个页面。
简单来说,HMR 的工作流可概括为:监控文件改动 → 识别变化模块 → 通知浏览器 → 浏览器局部更新,全程无需手动干预,实现 "改完即更" 的开发体验。

二、基于 Express 实现 HMR
实现 HMR 无需从零造轮子,我们可以借助 Webpack 生态中的两个核心中间件:devMiddleware
(负责文件监控与编译)和 hotMiddleware
(负责与浏览器通讯),结合 Express 搭建热更新服务。以下是完整实现代码:
js
const app = express();
const compiler = webpack(webpackConfig);
// 指定静态文件目录
app.use(express.static(path.join(__dirname, '../public/dist')));
// 引用 devMiddleware 中间件 (监控文件改动)
app.use(devMiddleware(compiler, {
//落地文件
writeToDisk: (filePath) => { return filePath.endsWith('.tpl') },
// 资源路径
publicPath: webpackConfig.output.publicPath,
// headers 配置
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization'
},
stats: {
colors:true,
}
}))
// 引用 hotMiddleware 中间件 (HMR 实现热更新通讯)
app.use(hotMiddleware(compiler, {
path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
log:()=>{},
}))
consoler.info('请等待webpack初次构建完成提示....')
const prot = DEV_SERVER_CONFIG.PORT
// 启动服务
app.listen(prot, () => {
console.log(`Server is running on prot:${prot}`);
})