Webpack这个东西,我刚入行那会儿总觉得它是个黑盒子。配置文件一抄一大把,能跑就行,至于里面到底发生了什么,一概不知。后来换了几家公司,处理过几回线上故障,回头再看,发现真正卡住人的不是Webpack的API有多少,而是对整个构建流程的理解。这篇文章就把我这些年折腾Webpack的经验做个梳理,从核心机制聊到实际配置,从优化手段聊到跟Vite的对比,每一块都是踩过坑之后验证过的东西。
1. 为什么Webpack会出现在我们的工具链里
1.1 没有打包器之前的散装时代
你如果翻过早期的前端项目,应该见过那种把所有JS文件用<script>标签挨个引用的页面。文件少还好,文件一多问题就全来了:全局变量互相污染、加载顺序稍有不慎就报错、代码没法复用、线上缓存也没法做细粒度更新。我当时维护过一个老系统,一个页面挂了20多个script标签,每次改动都得小心翼翼,生怕哪个依赖被后加载的文件覆盖了。
这个问题本质上不是“文件太多”,而是JS语言本身在早期没有官方的模块化方案。Node.js那边有CommonJS,但浏览器不认识require。浏览器端的模块化规范(ES Modules)直到ES6才正式落地,而且就算语法支持了,浏览器的加载效率也扛不住一个页面请求几百个JS文件。
1.2 模块化需求驱动打包器出现
Webpack的核心思路很简单:把浏览器不认识的各种模块语法,统一转换成浏览器能识别的普通JS代码,同时把这些散落的模块合并成一个或几个经过优化的文件,减少HTTP请求数。但这只是表面,真正让它流行起来的原因在于,它把“模块化”这个概念从JS延伸到了整个前端资源体系。
什么意思呢?CSS、图片、字体这些资源,在Webpack里也会被当作模块来处理。你在JS里import './style.css'是合法的,Webpack会把它解析成一段把样式注入页面的JS代码。图片可以用import img from './logo.png'引入,小图直接转成base64内联,大图则在构建时复制到输出目录并自动生成哈希文件名。这种“一切皆模块”的设计,让前端工程化有了统一的基础设施,后续的按需加载、公共代码抽取、内容哈希缓存,全都建立在这套模块系统之上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Webpack的核心运行机制:从入口到产物
2.1 一份配置的骨架:entry/output/module/plugins
先看一段最基础的配置,读懂了它,Webpack的大半机制就清楚了:
javascript复制// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js',
clean: true
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
})
]
};
entry是构建起点。Webpack会从这个文件出发,递归解析里面所有import和require的依赖,形成一棵模块依赖树。output定义构建结果输出到哪里、文件怎么命名,contenthash是根据文件内容生成的哈希,内容不变哈希就不变,这是做长缓存的关键。module.rules里配置的是loader规则,plugins用于扩展Webpack在构建生命周期各个阶段的能力。
2.2 Loader与Plugin的分工
这两个概念是新手最容易混淆的地方。我打个比方:Loader是做“翻译”的,Plugin是做“整修”的。翻译解决的是格式问题——Webpack只认识JS代码,遇到CSS、TypeScript、图片这些它搞不定的格式,就需要Loader把它们翻译成JS能处理的形态。整修解决的是流程问题——产物压缩、HTML模板生成、环境变量注入、文件复制,这些跟“翻译”无关但又是构建必需的事情,由Plugin在Webpack构建的不同生命周期节点上介入处理。
常用的Loader有这几个:
babel-loader:把ES6+语法转译成目标环境兼容的ES5。css-loader+style-loader:前者解析CSS里的import和url(),后者把样式以<style>标签形式注入页面。sass-loader/less-loader:编译预处理样式语言。vue-loader/ts-loader:处理Vue单文件组件和TypeScript。url-loader/asset-modules:处理图片字体等资源文件。
Plugin最常接触的是HtmlWebpackPlugin(自动生成HTML并注入script标签)、MiniCssExtractPlugin(把CSS从JS里抽取成独立文件)、CopyWebpackPlugin(复制静态资源)、DefinePlugin(注入全局变量)。
Loader的执行顺序有讲究,是从下到下、从右到左的。比如['style-loader', 'css-loader'],实际执行顺序是css-loader先处理CSS文件,拿到解析后的CSS内容,再交给style-loader注入页面。这个顺序如果写反了会直接报错。
2.3 构建流程中的几个关键阶段
Webpack的构建流程大致分为三步:
第一步是初始化。读取配置文件,合并默认配置,实例化所有Plugin,每个Plugin的apply方法会在这里被调用。不要把重要的逻辑放在apply执行时去做,因为这时候构建还没开始,拿不到任何模块信息。
第二步是编译。从entry出发,通过loader将每个文件转换成标准模块,然后解析模块内的依赖,逐层递归直到所有依赖都被处理完。这个阶段是构建中最耗时的部分,特别是大型项目里几千个模块的转换、解析和依赖收集,耗时的瓶颈大多出在这里。
第三步是输出。把所有模块组合成chunk,经过优化(压缩、treeshaking、代码分割),最终写入output.path指定的目录。
理解这个流程最大的好处是排查问题有方向。比如构建卡住不动,大概率是模块解析阶段某个loader在死循环;产物文件在但内容不对,就要看输出阶段的插件是不是把东西搞乱了。有方向就比瞎猜快得多。
3. 一份能上生产环境的配置,要怎么写
3.1 开发环境和生产环境必须分开
我见过不少项目只维护一份webpack.config.js,用process.env.NODE_ENV在代码里做判断,这个做法本身没错,但当配置膨胀到一定程度,可读性会很差。更推荐的做法是拆三份:
webpack.base.conf.js:公共配置,entry、resolve、module.rules等两边共用的部分。webpack.dev.conf.js:开发环境配置,重点在构建速度和调试体验。webpack.prod.conf.js:生产环境配置,重点在产物体积、缓存策略和兼容性。
用webpack-merge来合并公共配置:
javascript复制// webpack.prod.conf.js
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.conf');
module.exports = merge(baseConfig, {
mode: 'production',
// 生产环境特有配置
});
这样拆分之后,每次构建跑哪个文件一目了然,也不会出现改一个配置影响另一个环境的情况。
3.2 开发服务器的调试体验配置
webpack-dev-server是本地开发的核心工具。除了基本的路由配置,有几个参数非常影响体验:
javascript复制devServer: {
port: 8080,
open: false,
hot: true,
compress: true,
historyApiFallback: true,
client: {
overlay: {
errors: true,
warnings: false
}
}
}
historyApiFallback在做SPA应用时必须要开,否则你要手动把路由都重写到index.html,不然刷新一个子路由页面直接404。hot: true开启模块热替换,改CSS和Vue单文件组件时页面不用整刷,这个体验提升不是一点半点。
有一点要注意:devServer的hot: true配合vue-loader或者react-refresh,需要你在项目里做额外配置才能生效。Vue CLI和Create React App这类脚手架把这些都封装好了,但如果你是自己手搭的Webpack,Hot Module Replacement的插件配置是要自己加的。
3.3 生产构建的优化配置
刚说mode: 'production'会自动开启代码压缩(使用terser-webpack-plugin),但自动的配置离“好”还有距离。我一般会手动覆写压缩配置:
javascript复制const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
extractComments: false,
terserOptions: {
compress: {
drop_console: false, // 生产环境建议改成true
drop_debugger: true
}
}
}),
new CssMinimizerPlugin()
]
}
parallel: true开启多进程压缩,在大型项目里压缩阶段能节省不少时间。drop_console要不要开得看团队的线上日志策略,如果产品有采集console日志的需求,这个就不能盲目关掉。我的建议是保留warn和error级别的日志,把log和info的干掉,用pure_funcs: ['console.info', 'console.log']正则匹配会更精准。
Source Map在发布到生产环境时要不要开?我的意见是:有条件的话线上一定要开。不开source map,线上报错堆栈就是压缩过的一团乱码,排查问题全靠瞎猜。现在云上都有source map上传服务,上传后只在内部系统可见,不会暴露给普通用户。配置项用devtool: 'hidden-source-map',这样浏览器侧不会加载.map文件,但你能拿到对应的source map进行线上错误还原。
4. 构建性能优化:先会定位瓶颈,再谈优化手段
4.1 分析工具先行
优化之前必须先看数据,别靠感觉。第一板斧是看构建日志里各阶段的耗时,第二板斧是用webpack-bundle-analyzer看产物体积分布:
bash复制npm install --save-dev webpack-bundle-analyzer
配置里加一个插件,然后跑构建完会自动打开一个可视化页面,把所有模块按体积大小排列出来。这里一眼就能看到哪些第三方库是体积大户,比如旧版的moment.js会把所有语言的locale都打包进去,体积极其感人,但你实际只用到英文。这种问题不分析根本发现不了。
同样是体积分析,我建议生产构建和开发构建各看一次。生产构建的产物经过了压缩和tree-shaking,它反映的是“实际上线资源”的状况;开发构建没压缩,反映的是“模块编译”的状况。两者的瓶颈点往往不一样,前者要看JS/CSS的体积分布,后者要看编译耗时集中在哪些loader上。
4.2 缓存机制和编译提速
Webpack5内置了cache配置,默认在mode: 'development'下开启内存缓存,在mode: 'production'下默认开启文件缓存(放在node_modules/.cache目录下)。想要减少二次构建时间,建议显式配置:
javascript复制cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
buildDependencies这个配置的作用是告诉Webpack当配置文件本身发生变化时,缓存自动失效。我遇到过不知道这个配置导致的问题:改了webpack配置但构建没重新跑,半天都是旧逻辑在做事,排查了很久才发现是缓存没失效。
loader层面的缓存可以开启cache-loader或者直接写在具体的loader配置里,但要注意别加重复。比如babel-loader自己有cacheDirectory选项,vue-loader也有自己的缓存机制,如果再套一层cache-loader反而损耗性能。我现在的做法是只配置babel-loader的cacheDirectory: true,其余的依赖Webpack5的filesystem缓存已经能覆盖大部分场景。
多进程编译方面,thread-loader的收益是需要在多核机器上才明显,而且它只对耗时的loader(比如babel和ts-loader)有效。在babel-loader后面挂thread-loader,预热带来的开销有时候比收益还大。我自己实测下来,项目少于1000个模块的话,thread-loader带来的提升不明显,反而因为进程通信开销拖慢速度。3000个模块以上的大项目,收益才真正体现出来。
4.3 Tree Shaking:原理和边界
Tree Shaking是Webpack配合ES Modules做“按需移除”的机制。它能在编译阶段检测出哪些导出没有被任何模块引用,然后把这些“死代码”标记掉,最后由压缩工具(terser)执行删除。
这个机制的前提是模块语法必须用ES Modules(import/export),不能用CommonJS(require/module.exports),因为CJS是动态导入,静态分析无法确定到底引用了哪些导出,自然没法shaking。这就是为什么很多库会同时提供module字段指向ESM版本,main字段指向CJS版本,就是为了让打包器能用上tree shaking。
需要注意几个边界情况:
-
有副作用的模块不会被移除。即使
import './polyfill'没有用到任何导出,Webpack也不会删掉这个文件,因为模块顶层代码可能执行了全局修改。如果你的代码里有纯声明式的模块不想被打包,需要在package.json里标记"sideEffects": false,或者用数组精确列出有副作用的文件。 -
动态require无法被静态分析。
require(variable)这种写法的依赖关系在编译期是未知的,Webpack会尽可能把可能的模块都打包进去,导致体积膨胀。尽量避免这种写法。 -
CJS依赖的库,tree shaking效果会大打折扣。如果第三方库没有提供ESM版本(比如老版本的lodash),就只能用
lodash-es作为替代,或者老老实实用全量引入。
4.4 拆包策略:别把公共代码和业务代码混在一起
最基础的拆包用splitChunks:
javascript复制splitChunks: {
chunks: 'all',
cacheGroups: {
vue: {
test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
name: 'vendor-vue',
priority: 20,
chunks: 'all'
},
lodash: {
test: /[\\/]node_modules[\\/]lodash-es[\\/]/,
name: 'vendor-lodash',
priority: 20,
chunks: 'all'
},
common: {
name: 'common',
minChunks: 2,
priority: 5,
chunks: 'all'
}
}
}
这里我的经验是:把特别大的第三方库单独拆出来做成一个vendor包,而不是把所有node_modules都丢一个vendor.js里。几百个第三方依赖打包成一个vendor文件,虽然缓存利用率高,但是首屏加载这个vendor包可能就几百KB,体验反而下降。
拆包的粒度取决于你的业务场景。市面上常见的方案有四种:整个node_modules打一个包,按依赖关系手工分组,按路由懒加载自动分包,以及小而多(每个npm包单独成包)。前两种简单但粒度粗糙,最后一个包太多HTTP请求又不划算。我一般用的是按第三方库重要性分两个vendor + 按页面级懒加载分包,这个是投入产出比比较高的方案。
5. Webpack和Vite的行与不行
5.1 两者构建方式差异的本质
Webpack做的是“预构建”,启动时把所有模块收集、编译、打包成bundles,不管页面是否用到这些模块,构建阶段就要全部处理。这种方式的优点是运行时更快、依赖关系很明确、工具链成熟稳定;缺点是项目一大,启动就要几十秒甚至几分钟,开发时改一行代码热更新也要等好几秒。
Vite走的是esbuild预构建 + 浏览器原生ESModule的开发模式。开发时服务器启动只需要轻量地准备HTML入口,浏览器发起请求到哪个模块,Vite才即时转换那个模块返回给浏览器。本地开发体验几乎无上限——启动秒开,热更新也是毫秒级的。
这两个方案的差异本质上是“预编译”和“按需编译”的取舍。Webpack牺牲了启动速度换来了运行时的确定性;Vite牺牲了一点浏览器的首次加载时间(首次进页面要发的请求数较多),换来了开发期的极致反馈速度。
5.2 生产构建速度差距和转译精度问题
需要注意一个事实:vite build在生产构建时用的还是Rollup,不是开发阶段的esbuild。用esbuild做生产构建之所以没有直接套用,是因为esbuild的转译不做type checking,也不做babel那种完整的语法兼容处理,特别是一些需要polyfill的ES2015+语法和装饰器等实验性语法,esbuild的输出精度和Babel还有差距。所以Vite团队选择用Rollup做生产打包,搭配@rollup/plugin-babel等插件才能和Webpack体系对齐。
这就导致一个结果:Vite开发环境和生产环境的构建链路不同,理论上存在“开发环境能跑但生产环境产物有问题”的情况。这个风险在Webpack上不存在,因为开发和生产跑的是同一套模块系统。
5.3 迁移到Vite的代价和收益
如果你在维护一个老项目,要从Webpack迁到Vite,建议先想清楚这些代价:
- 所有CJS依赖需要Vite的
optimizeDeps预构建,遇到不兼容的库需要额外配置。 - 基于Webpack的专属插件需要找Vite对应的替代方案(比如
stylelint-webpack-plugin在Vite里得换成对应的vite插件)。 - 老旧的
require.context语法、自己写的WebpackPlugin、基于Webpack的目录约定路由,迁移的时候都要改。 - Node版本要求Vite更高,如果你的线上CI跑在老的Node版本上,升级Node本身可能又牵扯出一堆其他问题。
我的判断标准是这样的:老项目稳定运行、团队没有明确的新需求要动构建流程——这种就别动,Webpack的稳定性比开发体验更值钱。新项目、小项目、以纯前端页面为主不需要复杂后端集成——直接上Vite,效率上是质的飞跃。两种工具没有绝对的优劣,只有你是否用对了场景。
6. 常见问题排查实录
6.1 打包后图片路径404了,是publicPath在作祟
我踩过最典型的一次坑:开发环境一切正常,打包部署后所有图片路径变成了绝对路径/static/img/xxx.png,但项目部署在子路径下,服务器上根本没有这个路径的直接映射,图片全部404。问题出在output.publicPath。
publicPath决定了产物中引用资源的公共URL前缀。开发环境devserver默认的publicPath是/,部署到根路径没问题;一旦部署在子路径下,就要改成相对路径或者动态获取:
javascript复制// 方案一:相对路径(简单但有个坑,后面说)
output: {
publicPath: './'
}
// 方案二:CDN或子路径部署时显式指定
output: {
publicPath: process.env.CDN_BASE_URL || '/subpath/'
}
方案一看着省事,但路由懒加载的场景会出问题——动态import的chunk加载请求会变成./dist/js/xxx.js这种相对路径,如果当前页面URL带参数或者路由是hash模式,相对路径计算容易错。我后来统一用方案二,部署到哪个路径,构建时通过环境变量注入。
HtmlWebpackPlugin里如果也配置了自定义的assets前缀,要注意和publicPath保持一致性。
6.2 构建时内存溢出
code复制FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
这个报错我印象很深的项目是组件库构建的时候,Webpack的Node进程因为处理大量模块导致堆内存溢出。最简单的临时解决办法是给Node增大内存:
bash复制NODE_OPTIONS=--max-old-space-size=4096 npm run build
但根治要回头看为什么内存占用这么大。有几件事可以排查:
source-map的构建策略设置成cheap-module-source-map,能显著降低内存占用。- 有没有把所有文件都交给同一个loader处理,比如babel-loader没有排除
node_modules,会让Webpack白白编译一遍第三方库。 - 看看是不是CSS的@import嵌套层级太深,导致CSS处理阶段内存激增。
- 检查是否有死循环依赖,两个模块互相引入会导致Webpack递归解析,直到内存耗尽。
javascript复制module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'), // 明确范围
use: ['babel-loader']
}
]
}
6.3 改了代码但页面不更新,热更新失效了
Webpack的dev server开着,代码改了,但页面不刷。这个通常有两个原因:
第一个是devServer.hot没开,或者热更新组件逻辑没配对。Vue2项目装了vue-hot-reload-api但版本和vue-loader版本不匹配,热更新会静默失败。React项目没有配置react-refresh-webpack-plugin的话,热更新只能降级成整页刷新。
第二个是文件系统监听的坑。在Docker容器或虚拟机里挂载的宿主机目录做开发,Node的文件watcher(用的是系统层面的inotify)有时候监听不到文件变化。这时候要配置devServer的watchOptions:
javascript复制devServer: {
watchOptions: {
poll: 1000, // 每秒轮询一次
aggregateTimeout: 600
}
}
poll轮询会吃CPU,但至少在容器环境下开发体验还能保住。这里也提一下,如果代码改动后存在缓存导致重新构建不生效,大概率是上面的filesystem缓存出了问题,把node_modules/.cache删掉重启devserver,八成能解决。
6.4 使用worker上传大文件时,Webpack的产物路径处理
用worker上传大文件时,worker脚本的加载路径特别容易踩坑。new Worker(new URL('./upload.worker.js', import.meta.url), { type: 'module' })这种写法现在是浏览器原生支持的,但Webpack5自身对worker的打包方式需要在output里配workerChunkLoading和workerPublicPath。尤其部署在CDN场景下,worker脚本的加载路径如果和主bundle的路径不一致,会直接加载失败。
我建议优先用import.meta.url+new URL()写法,因为contenthash自带缓存你也不用管。真遇到兼容问题,再退回用raw-loader把worker代码以字符串方式引入,用Blob URL创建实例。这个方法兼容性最稳,就是代码体积大一点。
6.5 配置文件改动不生效
很多人改完webpack.config.js发现构建行为没变化,第一反应就是配置写错了,翻半天查不出来。这个是Webpack5的filesystem缓存机制在捣鬼。配置文件本身的变化默认不触发缓存失效。好一点的构建设置里会有buildDependencies.config: [__filename]来把这个文件本身加入缓存依赖,我前前后后把这段代码加到我所有项目里:
javascript复制cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
第一次遇到这个问题的时候以为是自己的配置写错了,翻来覆去改了半天,后来突然反应过来内置缓存没失效。所以我在这里专门写出来,排查方向可能根本不是配置本身,而是缓存没有失效。
7. 我在实际项目中积累的Webpack配置一口气分享
最后分享一套我自己整理的生产配置模版,算是我实践下来和团队协作都比较顺手的版本。它兼顾了“开箱即用”和“可定制”,你拿到手按自己的目录结构微调一下就可以用。
javascript复制// webpack.base.conf.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'assets/js/[name].[contenthash:8].js',
chunkFilename: 'assets/js/[name].[contenthash:8].chunk.js',
assetModuleFilename: 'assets/[hash][ext][query]',
publicPath: '/',
clean: true
},
resolve: {
extensions: ['.js', '.vue', '.ts', '.jsx', '.tsx', '.json'],
alias: {
'@': path.resolve(__dirname, 'src')
}
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
include: path.resolve(__dirname, 'src'),
use: 'babel-loader'
},
{
test: /\.(png|jpe?g|gif|svg|webp|avif)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 4 * 1024
}
}
},
{
test: /\.(woff2?|eot|ttf|otf)$/i,
type: 'asset/resource'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
favicon: './public/favicon.ico',
minify: process.env.NODE_ENV === 'production' ? {
removeComments: true,
collapseWhitespace: true,
minifyCSS: true,
minifyJS: true
} : undefined
})
]
};
开发环境配置,重点是模块热替换和更快的sourceMap:
javascript复制// webpack.dev.conf.js
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.conf');
module.exports = merge(baseConfig, {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
devServer: {
port: 8080,
hot: true,
compress: true,
historyApiFallback: true,
client: {
overlay: {
errors: true,
warnings: false
}
}
},
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
});
eval-cheap-module-source-map在开发阶段的性价比很高,编译速度比source-map快非常多,而且报错能定位到源码的具体行列,已经满足开发调试需求了。生产环境我会用hidden-source-map,既能保留错误还原能力,又不暴露源码。
生产环境配置:
javascript复制// webpack.prod.conf.js
const { merge } = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const baseConfig = require('./webpack.base.conf');
module.exports = merge(baseConfig, {
mode: 'production',
devtool: 'hidden-source-map',
output: {
publicPath: process.env.CDN_BASE_URL || '/'
},
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
}
]
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
name(module) {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
return `vendor-${packageName.replace('@', '')}`;
},
test: /[\\/]node_modules[\\/]/,
priority: 10
},
common: {
name: 'common',
minChunks: 2,
priority: 0
}
}
},
minimizer: [
new TerserPlugin({
parallel: true,
extractComments: false
}),
new CssMinimizerPlugin()
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'assets/css/[name].[contenthash:8].css',
chunkFilename: 'assets/css/[name].[contenthash:8].chunk.css'
})
]
});
这套配置里的几个细节再解释一下:
vendor拆分用了函数的方式为不同包生成不同的名称,这样就不会把几百个包压成一个巨大的vendor文件,而是每个包独立成一个chunk,粒度合适且缓存利用率高。postcss-loader在CSS链里加上,配合autoprefixer自动处理浏览器兼容前缀。这个在现在主流浏览器已经够先进的情况下,很多人会忽略,但遇到老的安卓WebView项目,这就是救命的。- 生产环境的公共路径用环境变量注入,这样你可以方便地切到CDN域名构建。
Webpack这个工具,你日常写业务代码其实很少碰它,但每次构建出问题、打包体积暴增、线上加载卡顿,追根究底都会回到这里。前端面试的时候Webpack也几乎成了必问项,特别是构建原理、loader和plugin的区别、性能优化手段这几个方向。把这一篇的内容吃透,不管是用Webpack还是迁移Vite,你都能有自己的判断,而不是被人牵着走。
