1. 内容整体设计与思路拆解
1.1 为什么这个阶段还要聊webpack5
先说个现象。我这几年面试前端,简历上十有八九都写着“熟悉webpack配置”,可真让对方讲讲loader和plugin的区别、说说tree shaking的原理,能讲清楚的不超过两成。大部分人是被Vite惯坏了——Dev server一敲,页面出来了,至于打包环节发生了什么,基本是一本糊涂账。
但现实是,生产环境里存量项目绝大多数还是webpack的天下。特别是那些需要深度定制、多页面、复杂CDN部署、微前端拆分的场景,Vite的上限就在那儿摆着,最终大家都得回到webpack的语境里解决问题。所以说,webpack5不是过时,而是被低估了。
另外webpack5本身解决了webpack4时代几个非常痛点的问题:持久化缓存让二次构建速度提升了一个量级、module federation让微前端有了全新的落地思路、内置的静态资源模块让file-loader和url-loader直接退出了历史舞台。这些东西如果你不去亲手搭一遍工程,光看文档是体会不到它的好的。
这篇博文就是用webpack5完整走一遍前端工程化搭建的流程,包含开发环境、生产构建、性能优化、多环境区分、代码规范集成这几个核心模块。我不会只贴配置就完事,每个关键配置都会拆开讲清楚为什么这么写、底层在做什么、坑在哪里。目标读者是那种已经写过一些业务代码、想真正搞懂前端构建链路的人,不管是准备跳槽还是准备接手公司老项目,这篇都能让你少走弯路。
1.2 搭建方案的选型考量:为什么不用脚手架
很多人会问,现在create-react-app、vue-cli、甚至Vite一步到位,为什么还要手动搭一遍webpack?
我的看法是:脚手架帮你做了80%的事情,但也藏了80%的细节。CRA你只能改react-scripts暴露出来的那点配置,哪天你想加个Sass全局变量、做多页面入口、自定义CDN路径,就卡住了。而手动搭建webpack,本质上是在搭建你自己的构建平台,一切都在掌控之中。
这次我选型的原则有三条:
- 不追求最新,追求最稳:webpack就用5.x长期维护版本,loader和plugin都选社区活跃度高、维护频率正常的。
- 不堆砌配置:能用webpack5内置能力解决的,绝不多装一个包。比如静态资源处理用内置的asset module,开发服务器用webpack-dev-server,不再额外接一堆第三方中间件。
- 开发体验和生产构建分开:开发环境核心是速度和热更新,生产环境核心是体积和性能。两套配置要按场景去优化,不能一套配置走天下。
用一句话概括这次的搭建思路:用webpack5把开发构建、资源处理、代码质量、性能优化全部整合成一条标准化流水线,搭建出来的工程可以直接用于商业项目,也可以作为团队内部统一的前端构建基线。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与基础骨架搭建
2.1 初始化项目和安装核心依赖
先准备一个项目目录,我用的是frontend-webpack-boilerplate,你也可以随意命名。
bash复制mkdir frontend-webpack-boilerplate
cd frontend-webpack-boilerplate
npm init -y
然后安装webpack核心依赖。这里有一个容易踩的坑:webpack4和webpack5的版本差异很大,webpack4时代安装webpack-cli是为了命令行工具,webpack5同样需要,但两者的兼容性更好。安装命令:
bash复制npm install webpack webpack-cli --save-dev
装完之后建议用npx webpack --version验证一下版本。如果输出是5.x.x,那就没问题。有个概念要先搞清楚:webpack是核心打包库,webpack-cli是负责在命令行里调用webpack的工具,两者缺一不可。
接下来安装开发服务器和插件:
bash复制npm install webpack-dev-server html-webpack-plugin --save-dev
html-webpack-plugin是工程化搭建里几乎绕不开的插件。它的作用是根据模板生成最终要用的HTML文件,并且自动把打包出来的js、css文件通过script和link标签注入到HTML里。如果没有这个插件,你每次构建完都要手动去改HTML里的资源引用路径,这在多入口工程里基本就是噩梦。
2.2 基础目录结构和三个关键文件
一个合理的目录结构是工程化的地基。我建议按源码和构建逻辑分开组织:
code复制frontend-webpack-boilerplate/
├── src/
│ ├── assets/
│ │ ├── images/ # 图片资源
│ │ └── styles/ # 全局样式
│ ├── js/
│ │ └── index.js # 入口文件
│ ├── views/
│ │ └── index.html # HTML模板
├── config/
│ ├── webpack.common.js # 公共配置
│ ├── webpack.dev.js # 开发环境配置
│ └── webpack.prod.js # 生产环境配置
├── dist/ # 构建输出目录
├── package.json
这里我把webpack配置拆分成了三个文件。webpack.common.js写开发和构建都要用的公共配置,webpack.dev.js和webpack.prod.js分别写各自环境独有的配置,再通过webpack-merge把公共配置和环境配置合并起来。
bash复制npm install webpack-merge --save-dev
这种做法的好处是显而易见的:公共逻辑只写一遍,避免开发和生产配置里重复维护入口、loader这些内容。如果你只在webpack.config.js里通过环境变量判断,配置一长就变得不可维护,特别是在loader和plugin多起来之后。
2.3 入口与出口的配置逻辑
先看webpack.common.js里的基础配置:
javascript复制const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/js/index.js',
},
output: {
filename: 'js/[name].[contenthash:8].js',
path: path.resolve(__dirname, '../dist'),
clean: true,
publicPath: '/',
},
plugins: [
new HtmlWebpackPlugin({
template: './src/views/index.html',
filename: 'index.html',
inject: 'body',
}),
],
};
入口的写法我用了对象形式而不是字符串形式。单入口工程差别不大,但一旦以后要加第二个页面,直接加一个键值对就行。[contenthash:8]这个占位符就是根据文件内容生成的哈希值,取前8位。这么做的目的只有一个:浏览器缓存。
这里解释一下为什么用contenthash而不是hash。hash是每一次构建都会变的全局哈希,哪怕你只改了一个标点,所有文件名都会变,等于缓存全部失效。contenthash则只跟文件内容挂钩,文件没变哈希就不会变,浏览器就会沿用缓存,大大减少每次发版后用户需要重新下载的资源量。
2.4 模式的设置:开发和生产为什么要分家
mode这个配置很多人会忽视,但它直接影响最终的打包结果。在webpack.common.js里我一般不写死mode,而是交给环境配置文件去覆盖:
javascript复制// webpack.dev.js
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
// ...
});
// webpack.prod.js
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
// ...
});
mode设为development时,webpack会自动开启NamedModulesPlugin和HotModuleReplacementPlugin,方便开发时调试。设为production时,webpack会默认启用压缩插件、tree shaking、作用域提升等一系列优化手段。所以你不手动写优化配置,其实webpack也已经帮你做了很多。但为了更好的效果,生产环境我们还需要手动加一些东西,这个后面细说。
3. 核心配置逐块拆解与实操要点
3.1 处理JavaScript:babel-loader和浏览器兼容性
现代前端代码写的都是ES6+语法,可用户的浏览器不一定支持。babel-loader的作用就是把ES6+语法降级成ES5。安装依赖:
bash复制npm install babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime --save-dev
npm install @babel/runtime-corejs3 core-js --save
babel-loader是webpack和babel之间的桥梁,@babel/core是babel的核心库,@babel/preset-env负责语法转换规则的集合,@babel/plugin-transform-runtime和@babel/runtime-corejs3负责抽取公共的辅助代码和按需注入polyfill。
然后配置:
javascript复制module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
},
},
],
},
cacheDirectory: true是容易被人忽略的性能优化点。babel的转译过程是很耗时的,开了这个选项后,babel会把转译结果缓存到node_modules/.cache/babel-loader目录里,源文件没变就直接用缓存,开发时二次构建速度快非常多。
对应的babel.config.js:
javascript复制module.exports = {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: 3,
targets: {
browsers: ['> 1%', 'last 2 versions', 'not dead'],
},
},
],
],
plugins: [
[
'@babel/plugin-transform-runtime',
{
corejs: 3,
},
],
],
};
useBuiltIns: 'usage'这个配置值得花点篇幅讲。如果你把它设为'entry',你需要在入口文件里手动引入core-js,babel会把所有polyfill全量打进来,体积大得吓人。设为'usage'后,babel会扫描代码里实际用到的API,只看你用到了什么ES6+特性,按需引入对应的polyfill。比如你的代码里用了Promise,那就只引入Promise相关的polyfill。这直接关系到首屏加载体积。
3.2 处理样式:从Sass到CSS Module
样式处理在工程化里是重头戏,因为涉及到的loader链条又多又容易配错。先安装:
bash复制npm install sass sass-loader postcss-loader autoprefixer css-loader style-loader mini-css-extract-plugin --save-dev
基础规则配置:
javascript复制{
test: /\.s[ac]ss$/i,
use: [
'style-loader',
'css-loader',
'postcss-loader',
'sass-loader',
],
},
这个loader的执行顺序是从下到上的:sass-loader先把Sass编译成CSS,postcss-loader再对CSS做自动加浏览器前缀等处理,css-loader再把CSS转成JS模块,最后style-loader把样式以<style>标签的形式注入到HTML里。
这里有一个开发和生产环境需要差异化的点。开发环境用style-loader没问题,热更新快。但生产环境如果用style-loader,样式就是通过JS动态写入的,在JS加载完成之前页面就是白板,这体验太差了。所以生产环境需要用mini-css-extract-plugin把CSS抽成独立文件,用<link>标签加载:
javascript复制// webpack.prod.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// 在rules里把style-loader替换成MiniCssExtractPlugin.loader
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader',
],
// 插件配置
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
}),
postcss-loader的配置单独放在postcss.config.js:
javascript复制module.exports = {
plugins: [
require('autoprefixer'),
],
};
autoprefixer会根据你在babel targets里配置的浏览器范围,自动给CSS属性加前缀。比如你写display: flex,它会自动补上-webkit-前缀。前提是你加了browserslist配置,可以放在package.json里,也可以放在.browserslistrc文件里。这个配置babel和autoprefixer都共用,所以要注意两边的一致性。
3.3 处理静态资源:webpack5的asset module
webpack4时代处理图片、字体要装file-loader、url-loader、raw-loader三件套,还得为不同类型文件配不同的rule。webpack5直接用asset module统一搞定了。
javascript复制{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
generator: {
filename: 'images/[name].[contenthash:8][ext]',
},
parser: {
dataUrlCondition: {
maxSize: 4 * 1024, // 4KB
},
},
},
{
test: /\.(woff2?|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[contenthash:8][ext]',
},
},
type: 'asset'是个自动选择的模块类型,webpack会根据文件大小决定走哪条路:小于maxSize的文件转成base64字符串直接内嵌到JS里,减少一次HTTP请求;大于maxSize的文件打包成独立文件,按路径引用。这个阈值设成4KB是比较稳妥的选择,内嵌太大会让JS文件急剧膨胀,反而拖慢了加载速度。
type: 'asset/resource'就直接把文件打到输出目录,适合字体这种不会特别小的文件。字体文件如果不转独立文件而是内嵌base64,体积会膨胀三分之一左右,对大字体文件来说完全不可接受。
3.4 路径别名和模块解析:让import不再痛苦
在这之前,每次写import都得算相对路径,../../../components/Button这种代码相信大家都写过。配置一个@别名指向src目录:
javascript复制const path = require('path');
resolve: {
alias: {
'@': path.resolve(__dirname, '../src'),
},
extensions: ['.js', '.json', '.scss'],
},
alias是路径别名,extensions是自动解析的扩展名。配置了extensions之后,import './Button'就能自动找到Button.js、Button.json或Button.scss,不用写后缀。
这里有个性能细节要注意:extensions列表越短越好,因为每多一个扩展名,webpack在解析模块时就要多做一次文件系统的探测,过多扩展名会拖慢构建速度。尽量只保留工程里实际用到的扩展名。
4. 开发服务器的实战配置与优化
4.1 webpack-dev-server的完整配置
开发服务器是整个开发体验的核心。配置不好,每次保存都等个三五秒甚至触发整个页面刷新,那开发效率就别提了。
javascript复制// webpack.dev.js
devServer: {
static: {
directory: path.resolve(__dirname, '../dist'),
},
port: 8080,
open: true,
hot: true,
historyApiFallback: true,
compress: true,
client: {
overlay: true,
},
},
一个个说下关键配置项:
hot: true:开启热模块替换。这是纯前端开发的基本体验,改样式和组件只替换变更的模块,不刷新整个页面。但要真正生效,还涉及到模块热替换的代码,通常配合react-refresh或vue-loader这些框架相关的方案来实现。historyApiFallback: true:配合前端路由使用。开发时你访问/about这个路径,服务器上并没有这个文件,但historyApiFallback会帮你把请求回退到index.html,让前端路由接管解析。client.overlay:编译出错时是否在浏览器上展示蒙层报错。开启后,编译错误会直接显示在页面上,不需要切到终端去看日志,排查问题效率高很多。compress: true:开启gzip压缩。本地开发可能感受不明显,但能模拟线上传输环境。
4.2 模块热替换到底是怎么工作的
热更新的原理,往深了说可以写一本书,这里用通俗的方式拆一下。
webpack在构建时会对每个模块做标记。你在开发模式下启用hot: true后,webpack会在打包产物里注入一个websocket客户端,连接上开发服务器。当你改动一个文件并保存时,webpack会重新编译这个模块,并通过websocket推送一条消息给浏览器。浏览器收到消息后,会调用对应模块记录的module.hot.accept回调,用新的模块替换掉旧的模块,而不触发页面刷新。
问题来了,如果你的业务代码没有写module.hot.accept,热替换是失效的,最终还是会退化成一个整页刷新。这就是为什么很多框架都要配对应的热更新插件:React需要react-refresh,Vue的vue-loader本身就集成了。单独用webpack做原生JS开发时,你需要在入口文件里写一段环境判断代码:
javascript复制if (module.hot) {
module.hot.accept();
}
意思是这个入口模块所在的范围如果更新了,就接受更新并重新渲染。不过实际开发中如果走的原生DOM操作流程,热更新往往做不到完美的状态保持,直接用整页刷新反而更省心。所以我的建议是:样式和框架模块交给热更新,业务模块别强求。
4.3 代理配置:解决开发环境的跨域问题
做前端开发几乎绕不开跨域。开发时前端跑在8080端口,后端接口跑在3000端口,直接fetch必然被同源策略拦住。webpack-dev-server提供了一个解决方案——代理。
javascript复制devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: { '^/api': '' },
changeOrigin: true,
},
},
},
这段配置的意思是把请求/api开头的接口全部转发到http://localhost:3000,并把路径里的/api去掉。比如前端请求/api/users,实际上后端收到的请求是/users。
changeOrigin: true看着不起眼但必须设。它的作用是让代理服务器以target的域名去发起请求,避免后端根据host头做校验时直接拒绝请求。很多团队Debug跨域问题搞了半天,最后发现就是少了这一行。
5. 生产构建的性能优化实战
5.1 代码分割:从entry到splitChunks
生产环境最忌讳的就是把整个应用打进一个JS文件。一个几兆的JS文件加载耗时极长,用户首屏体验会非常差。webpack5提供了成熟的分包方案——splitChunks。
javascript复制// 生产环境配置
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10,
},
common: {
name: 'common',
minChunks: 2,
priority: 5,
},
},
},
},
chunks: 'all'代表不管是同步加载还是异步加载的模块,都要参与代码分割。对于异步加载,webpack会为动态import的模块单独打包,这样用户点击某个路由时才去下载对应代码。
cacheGroups是分组策略。我把node_modules里的第三方依赖独立成vendor包,因为第三方依赖的更新频率很低,抽出来之后可以长期利用浏览器缓存。common组放的是被多个入口或页面共享的业务代码,至少被引用2次才抽出来,避免分包太碎。
这里有个经验之谈:第三方依赖的包不要拆得太碎。有时候你把lodash、axios、react全部分成单独的包,会导致HTTP请求暴增,反而变慢。一般一个vendor包就够,如果工程确实大再考虑按功能拆分。
5.2 持久化缓存:webpack5最大的惊喜
webpack5最让我满意的更新,不是module federation,而是持久化缓存。webpack4时代的开发启动花费几秒甚至十几秒是常态,因为每次启动都要重新编译全部模块。webpack5把编译结果缓存到了磁盘上,二次启动直接复用缓存,启动速度提升了不是一点半点。
javascript复制// webpack.common.js
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename],
},
},
type: 'filesystem'开启磁盘缓存。buildDependencies.config的意思是,当配置文件本身发生变化时,缓存失效并重新构建。这是必不可少的设置,否则你改配置后发现构建结果没变,排查半天都找不到原因。
开启之后,你会发现之前冷启动要8秒的项目,现在可能只花2到3秒。特别是当你只改了代码里的一个字符串,webpack能精准定位到受影响的模块,做增量编译。这种效率提升体感非常明显,是webpack5非常值得升级的理由。
5.3 CSS压缩和JS压缩
先看CSS压缩
webpack5默认不压缩CSS,需要css-minimizer-webpack-plugin:
bash复制npm install css-minimizer-webpack-plugin --save-dev
javascript复制const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
optimization: {
minimizer: [
new CssMinimizerPlugin(),
'...',
],
},
'...'这个写法说的是保留webpack默认的压缩配置(也就是JS压缩),在它前面再加一个CSS压缩。如果你直接写minimizer: [new CssMinimizerPlugin()],默认的JS压缩会被覆盖掉,JS就完全不压缩了,这种坑我建议你避开。
再看JS压缩
webpack5生产模式下默认用的是terser-webpack-plugin。如果对默认配置不满意,可以显式安装并根据项目情况调整:
javascript复制const TerserPlugin = require('terser-webpack-plugin');
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
}),
],
},
drop_console: true会把代码里的console.log全部删掉。上生产环境肯定不希望用户端还打印一堆调试日志,既暴露细节又影响性能。但这个配置也要考虑团队情况,如果你还需要在线上排查问题,建议只删掉console.log,保留console.warn和console.error:
javascript复制compress: {
drop_console: true,
pure_funcs: ['console.info'],
},
5.4 tree shaking:让未使用的代码自动消失
tree shaking这个概念在webpack4就有,webpack5进一步改进了对ES module分析的能力。它做的就是一件事——去掉按引号没被用到的代码。
但tree shaking有前提条件,必须满足:
- 模块必须是ES Module语法,即
import和export。CommonJS的require无法被静态分析,也就无法tree shaking。 - 不能有副作用。如果一个模块在导入时就立即执行了全局操作,比如给window挂属性,那么webpack就不敢动它。
在package.json里加一个字段:
json复制{
"sideEffects": false
}
这个字段是给webpack一个声明:项目里的所有模块都是纯模块,没有副作用,可以放心做tree shaking。但如果你的项目里引入了全局样式文件,比如import './global.scss',这个声明就出问题了——webpack会认为全局样式是没用的代码被删掉。正确的做法是:
json复制{
"sideEffects": [
"*.scss",
"*.css"
]
}
意思是仅样式文件属于有副作用的模块,其他模块都可以被安全地删减。
5.5 资源内联:减少首屏请求数
开发环境下我们期望资源尽量拆分,方便调试。生产环境下,一些体积小但请求频繁的资源,内联反而是更好的选择。比如一些关键的SVG图标、首屏所需的CSS、一些平台能力检测脚本,完全可以直接内联到HTML里。
webpack5可以这样处理:
javascript复制// 小体积SVG直接转base64内联
{
test: /\.svg$/,
type: 'asset/inline',
},
另外html-webpack-plugin本身也有一个实用能力,可以把某个JS或CSS文件的内容直接以<script>或<style>标签的形式内联到HTML里。配合inline-chunk-html-plugin之类的插件,可以把webpack运行时这个几十KB的小文件直接塞进HTML,省掉一次HTTP请求。
不过内联要克制,过度的内联会让HTML文件变大,进而影响首屏解析速度。一般情况下,内联的文件控制在200KB以内比较合适。
6. 常见问题排查与实用心得
6.1 常见问题速查表
我整理了一张排查表,列出来的都是实际开发中遇到过最多的问题:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
构建报Module not found |
路径拼写错误或别名未生效 | 检查resolve.alias配置和extensions列表 |
| 页面样式不生效 | loader顺序写错 | 记住loader从右到左执行,顺序应为sass-loader → postcss-loader → css-loader → style-loader |
| 热更新失效,总是整页刷新 | 没有写module.hot.accept或框架插件未配置 |
React工程配置react-refresh,Vue工程确认vue-loader版本 |
| 打包体积过大 | 没有做代码分割或source-map未关闭 | 配置splitChunks,生产环境用devtool: 'source-map'或hidden-source-map |
| 修改文件后缓存不更新 | 文件名没有使用contenthash | 输出文件名用[contenthash:8] |
| 浏览器兼容性报错 | babel targets和polyfill配置不对 | 确认useBuiltIns: 'usage',并根据需要配core-js |
| 图片不显示/路径404 | publicPath配置错误 | 部署时搞清楚publicPath是根路径还是CDN路径 |
| CSS中的url资源路径不对 | 缺少publicPath配置 |
比较典型的,字体或图片资源的url需要在output里做路径适配 |
6.2 构建速度优化的几个小技巧
除了前面提到过的缓存和babel缓存,还有几个我用得很顺手的小技巧可以分享:
减少loader的作用范围。用include和exclude让loader只处理它该处理的目录,而不是扫描全项目:
javascript复制{
test: /\.m?js$/,
exclude: /node_modules/,
use: ['babel-loader'],
}
node_modules里的代码已经是ES5的,babel再去转一遍纯粹浪费时间。同理,图片、字体这种loader也可以限定在src目录里。
关闭source-map或用更轻量的模式。开发环境用eval-cheap-module-source-map,生产环境其实可以用hidden-source-map,把source map文件单独上传监控平台,不暴露给用户端。
关于并行压缩的取舍。webpack5内置了parallel参数控制多进程压缩。但对于中小型项目,开启多进程的启动开销可能比压缩本身更耗时。对这种工程,没必要强行上thread-loader或并行压缩,反而会增加配置的复杂度。
6.3 从webpack4迁移到webpack5的注意事项
如果你的手头有一个webpack4的老项目,想升级到webpack5,有几个点一定要提前注意:
第一,node版本要够。webpack5要求Node.js的版本在10.13.0以上,建议直接上14或16,否则会安装失败或者运行报错。
第二,loaders和plugins的兼容性。必须检查一下你用的loader是否兼容webpack5。最常见的是url-loader和file-loader,webpack5里直接用asset module替代了它们。win-loader如果强制使用,可能在构建时直接报错。
第三,配置兼容的问题。webpack4里的optimization.namedModules已经移除,直接用optimization.moduleIds: 'deterministic'即可。webpack4里过时的ExtractTextWebpackPlugin到webpack5已经彻底不再支持,需要用MiniCssExtractPlugin替代。
6.4 关于工程化的一些个人体会
搭一套webpack工程化配置很容易,照着文档抄一遍就完了。但理解这份配置背后的原理,具备在它出问题时快速排错的能力,这才是工程化这件事真正的价值所在。
我见过不少团队,配置越来越复杂,却说不清每一条配置是干嘛的;也见过有的工程,构建配置从webpack3升级到webpack5,反而变慢了——因为类似loader重复扫描、过度分包导致请求数激增这些问题,比配置本身更难察觉。
所以在搭建过程中,我的原则是:每引入一个loader或插件就问自己三个问题——它解决什么问题?它带来的性能开销是多少?有没有更简单的替代方案?如果三个问题都回答得上来,这条配置留在工程里就是有理由的。如果只是为了追新、或者看网上教程说一定要装,就值得停下来想想了。
webpack5的工程化搭建这件事,本质上是培养一种能力:理解前端资源的流转、缓存、加载、性能。配置只是表象,背后涉及的是对浏览器机制、模块系统、编译原理的综合理解。这种能力,不管前端工具链怎么变,都不会过时。
