详解多页应用 Webpack4 配置优化与踩坑记录
本文详细解析 Webpack4 在多页应用中的配置优化方案。重点讲解如何利用 optimization 配置实现代码分割、DLL 库抽离及 Polyfill 管理,通过 runtimeChunk、moduleIds 和 namedChunks 提升缓存命中率。同时提供使用 Node.js 动态生成入口文件以简化多页项目结构的实用技巧,并指出 CSS Hot Loader 在生产环境使用的潜在风险。
前言
前言
近期启动了一个多页项目,此前未曾使用过 Webpack4,因此决定进行实践探索。本文主要介绍相关配置细节,旨在为计划迁移至 Webpack4 的开发者提供参考。
相较于 Webpack2 和 3,Webpack4 发生了显著变化,其核心改进在于大量配置已内置,使得 Webpack 具备“开箱即用”的能力。尽管这无法覆盖所有场景,但许多以往必需的配置已不再需要。例如,过去压缩混淆代码需引入 Uglify 插件,作用域提升需使用 ModuleConcatenationPlugin,而在 Webpack4 中,仅需将 mode 设置为 production 即可实现相同效果。当然,若强行保留旧插件也不会报错。
因此,建议迁移至 Webpack4 时,从零开始构建配置,参考历史经验重新添加所需功能,而非在旧配置基础上删减升级。这样能使 Webpack4 的配置更加精简。
打包优化
打包优化
打包优化的核心在于多页应用构建时,对页面依赖进行合理打包。目前业界已有诸多实践,包括 Webpack4 的相关文章。此处补充几个易被忽视的细节。部分概念若不熟悉 Webpack 配置可能难以理解,建议搜索相关关键词查阅详细资料。
构建多页应用时,通常会抽离以下 chunk 包:
- common:将多个页面共同引用的依赖打包至 common chunk。多数教程建议引入两次即打入 common,建议根据页面数量调整。在我的工程中,仅当引入次数超过页面总数的 1/3 时,才将其打入 common 包。
- dll:将每个页面均引用且基本不变的依赖(如 react/react-dom)抽离,防止其他模块变化污染 dll 库的 hash 缓存。
- manifest:Webpack 运行时代码。依赖变化时运行时代码也会改变,若不抽离,可能增加 common 包 hash 值变化的概率。
- 页面入口文件对应的 page.js
随后,为 chunk 包名注入 contentHash,以实现最大缓存效果。分 chunk 的关键思想是每次迭代发布时尽量减少 chunk hash 值的改变。业界已有大量实践,例如参考文章:https://github.com/pigcan/blog/issues/9
在 Webpack4 中,无需添加过多插件,一个 optimization 配置即可解决。
先展示我的 Webpack optimization 配置,随后进行介绍以加深印象。
const commonOptions = {
chunks: 'all',
reuseExistingChunk: true
}
export default {
namedChunks: true,
moduleIds: 'hashed',
runtimeChunk: {
name: 'manifest'
},
splitChunks: {
maxInitialRequests: 5,
cacheGroups: {
polyfill: {
test: /[\\/]node_modules[\\/](core-js|raf|@babel|babel)[\\/]/,
name: 'polyfill',
priority: 2,
...commonOptions
},
dll: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'dll',
priority: 1,
...commonOptions
},
commons: {
name: 'commons',
minChunks: Math.ceil(pages.length / 3), // 至少被1/3页面的引入才打入common包
...commonOptions
}
}
}
}
runtimeChunk
runtimeChunk
Webpack4 之前,抽离 manifest 需使用 CommonsChunkPlugin 并指定 name 为 'manifest'。Webpack4 中,无需手动引入插件,配置 runtimeChunk 即可。
splitChunks
splitChunks
该配置允许按规则抽离所需包。若需抽离多个包(如 vendor + common),splitChunks 提供 cacheGroups 字段,每增加一个 key 即代表一条抽包规则。
许多教程中,dll 需单独配置 webpack,使用 DllPlugin 构建 dll 库,再通过 DllReferencePlugin 映射。虽能加快构建速度,但过程繁琐。
我倾向于避免繁琐,宁愿在 Webpack4 中利用 splitChunks 配置规则并抽离 dll 包。读者可根据实际情况选择方案。
除 dll 与 common 外,我还添加了 polyfill。因使用某些新库或 ES6+语法(如 async/await)需 runtime 垫片。例如工程中使用了 react16,需增加 Map/Set/requestAnimationFrame(参考 https://reactjs.org/docs/javascript-environment-requirements.html)。必须在 dll 库加载前增加 polyfill,因此将 core-js 与 babel 引入的包打入 polyfill,确保后续 chunk 能执行。priority 字段用于配置 chunk 引入优先级,一般项目通常为 polyfill > dll > common > page。
splitChunks 中 maxInitialRequests 表示一个入口中最大初始请求 chunk 数(不含按需加载,即 DOM 中 script 引入的 chunk),默认值为 3。当前 cacheGroups 已有三项,加上 runtimeChunk 产生的 manifest,共 4 个 chunk 包,超出默认值,故需重新配置。
moduleIds
moduleIds
了解 Webpack 运行机制的开发者知道,项目工程中加载的 module 会被分配 moduleId 以映射模块。若工程中模块增删或顺序变化,moduleId 随之改变,进而影响所有 chunk 的 content hash 值。仅因 moduleId 变化导致缓存失效并非所愿。
Webpack4 之前,通过 HashedModuleIdsPlugin 插件,将模块路径映射为 hash 值替代 moduleId。因模块路径基本不变,hash 值也基本稳定。
Webpack4 中,只需在 optimization 配置项中设置 moduleIds 为 hashed 即可。
namedChunks
namedChunks
除 moduleId 外,分离出的 chunk 也有 chunkId。同样,chunkId 变化可能导致缓存失效。因 manifest 与 chunk 包中包含 chunkId 相关数据,若“增删页面”等操作导致 chunkId 变化,可能影响大量 chunk 缓存。
Webpack4 之前,通过 NamedChunksPlugin 使用 chunkName 替换 chunkId 以固化 chunkId。Webpack4 中,只需在 optimization 配置项中设置 namedChunks 为 true 即可。
CSS 相关
css 相关
Webpack4 之前,使用 extract-text-webpack-plugin 将 css 从 js 包中分离单独打包。Webpack4 中需替换为 MiniCssExtractPlugin。生产环境或需 HMR(模块热替换)时,需用 MiniCssExtractPlugin.loader 替换 style-loader。
注意此处有一个坑。开发环境配置热更新时,css 热更新目前 MiniCssExtractPlugin.loader 尚待支持,故需增加 css-hot-loader。切记,css-hot-loader 绝不能在生产环境使用。否则每次构建过程中所有 js chunk 包的 contentHash 值均不一致,导致所有 js 缓存失效。因生产环境增加此配置不会报错且页面能正常构建,易被忽视。
简化多页应用的入口文件
简化多页应用的入口文件
使用 react/vue 等框架时,通常需要一个入口 index.js,例如:
import React from 'react'
import ReactDOM from 'react-dom'
import App from './app'
ReactDOM.render(<App />, document.getElementById('root'))
若还需使用 dva 或为所有 react 页面增加 layout 功能,可能变为:
import React from 'react'
import dva from 'dva'
import Model from './model'
import Layout from '~@/layout'
import App from './app'
const app = dva()
app.router(() => (
<Layout>
<App />
</Layout>
))
app.model(Model)
app.start(document.getElementById('root'))
若每个页面均如此,略显繁琐,因程序员最忌重复劳动。但必须存在,无法抽离为单独文件。因这是入口文件,多页工程中每个页面必须有独立入口文件,即使内容相同。于是,资源目录结构如下:
- src
- layout.js
- pages
- pageA
- index.js
- app.js
- model.js
- pageB
- index.js
- app.js
- model.js
因所有 index 相同,理想中页面入口文件仅需 app.js,例如:
- src
- layout.js
- pages
- pageA
- app.js
- model.js
- pageB
- app.js
- model.js
作为前端开发工程师,Node 应是我们熟练运用的工具,而非仅使用他人封装好的各类工具。
针对此问题,可在 Webpack 构建前,通过 Node 的文件系统(File System),对应每个页面,利用同一入口文件模板创建临时入口文件:
- src
- .entires
- pageA.js
- pageB.js
- layout.js
- pages
将这些临时文件作为 Webpack 的 entry 配置。代码如下:
const path = require('path')
const fs = require('fs')
const glob = require('glob')
const rimraf = require('rimraf')
const entriesDir = path.resolve(process.cwd(), './src/.entries')
const srcDir = path.resolve(process.cwd(), './src')
// 返回webpack entry配置
module.exports = function() {
if (fs.existsSync(entriesDir)) {
rimraf.sync(entriesDir)
}
fs.mkdirSync(entriesDir)
return buildEntries(srcDir)
}
function buildEntries(srcDir) {
return getPages(srcDir).reduce((acc, current) => {
acc[current.pageName] = buildEntry(current)
return acc
}, {})
}
// 获取页面数据,只考虑一级目录
function getPages(srcDir) {
const pagesDir = `${srcDir}/pages`
const pages = glob.sync(`${pagesDir}/**/app.js`)
return pages.map(pagePath => {
return {
pageName: path.relative(pagesDir, p).replace('/app.js', ''), // 取出page文件夹名
pagePath: pagePath
}
})
}
// 构建临时入口文件
function buildEntry({ pageName, pagePath }) {
const fileContent = buildFileContent(pagePath)
const entryPath = `${entriesDir}/${pageName}.js`
fs.writeFileSync(entryPath, fileContent)
return entryPath
}
// 替换模板中的 App 模块地址,返回临时入口文件内容
function buildFileContent(pagePath) {
return `
import React from 'react'
import dva from 'dva'
import Model from './model'
import Layout from '~@/layout'
import App from 'PAGE_APP_PATH'
const app = dva()
app.router(() => (
<Layout>
<App />
</Layout>
))
app.model(Model)
app.start(document.getElementById('root'))
`.replace(PAGE_APP_PATH, pagePath)
}
如此,我们简单去除了重复入口文件,并增加了 layout 功能。此代码较简单,实际项目可能涉及多级目录、多个 model 等,需自行定制。
Webpack4 发布已有一段时间,本文撰写稍显滞后,故许多我认为大家已知的细节未详述。如有疑问,欢迎评论。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
怎么使用 JavaScript import() 实现模块的条件加载
2026-11-05 14:44:45
前端防御XSS攻击:怎么防止恶意JS注入与CSP策略配置
2026-11-04 08:51:38
怎么解决 Turf.js 处理大规模 GeoJSON 数据的性能瓶颈
2026-11-03 23:22:19
为什么不建议在JS中手动拼接JSON字符串?
2026-11-03 22:48:27
代码混淆对JS执行性能的影响测试与分析
2026-11-03 22:25:37
