1. 项目概述:为什么一份“能跑通”的webpack配置远远不够
“webpack配置整理”这六个字,听上去平平无奇,像是前端工程里最不起眼的日常维护项。但如果你在某次升级依赖后发现打包体积突然暴涨40%,或者在CI流水线上构建失败却查不出原因,又或者新同事花三天才搞懂本地开发服务器为什么热更新失效——你就会明白,所谓“整理”,从来不是把几个loader和plugin堆在一起那么简单。它本质上是一场对整个前端构建生命周期的系统性反向工程。我过去三年带过的十几个中大型项目里,超过七成的构建类线上问题,根源都藏在webpack配置的某个隐式默认值、某个插件的执行时序,或是某个loader对特定文件后缀的静默忽略中。这份整理,不是给机器看的,是给人看的:让团队里任何一位成员,在凌晨两点面对构建报错时,能快速定位到resolve.alias是否覆盖了node_modules里的同名包,或者optimization.splitChunks的minSize设置是否无意中把一个仅被引用一次的工具函数也抽成了独立chunk。它解决的不是“能不能打包”,而是“为什么这样打包”“改一处会不会牵动全局”“上线前如何用最小成本验证变更安全”。适合所有正在用webpack、但还没亲手从零搭过完整配置链路的开发者;也适合那些已经写了三年配置、却依然靠复制粘贴维系项目的资深同学——因为真正的掌控感,永远来自对每个字段背后决策逻辑的透彻理解。
2. 配置设计底层逻辑:从“功能拼凑”到“分层治理”
2.1 为什么必须拆成多环境配置?单个webpack.config.js就是技术债温床
很多人一开始只写一个webpack.config.js,通过mode区分开发/生产,再用process.env.NODE_ENV做条件判断。这看似简洁,实则埋下三重隐患:第一,开发环境需要dev-server、source-map、hot-module-replacement,而生产环境要禁用它们并开启TerserPlugin压缩、HtmlWebpackPlugin注入hash、SplitChunksPlugin代码分割——混在一个文件里,if/else嵌套三层后,没人敢动;第二,测试环境(如CI)往往需要额外的--analyze分析报告或--profile性能追踪,这些开关若硬塞进主配置,会污染本地开发体验;第三,最致命的是,当团队引入微前端架构时,子应用可能需要完全不同的externals配置来隔离全局变量,此时单配置文件根本无法扩展。我见过某公司因拒绝拆分配置,导致一次Webpack 5升级耗时两个月:所有module.rules里的test正则都得手动加/\.m?js$/i兼容ESM,而分散在五个不同if分支里的规则,漏改一处就全量构建失败。所以我的方案是强制三分:webpack.common.js(基础共用)、webpack.dev.js(开发专属)、webpack.prod.js(生产专属),用webpack-merge做组合。这不是为了炫技,而是让每次修改都有明确边界——比如调整output.publicPath,你只需确认它是否在common里定义,而不用翻遍三百行代码找那个被注释掉的// TODO: fix publicPath for CDN。
2.2 “配置即代码”的核心原则:可读性优先于简洁性
Webpack官方文档里大量使用箭头函数简写,比如module.exports = (env, argv) => ({...})。这种写法在demo里很酷,但在真实项目中等于主动放弃调试能力。当你在VS Code里打断点,想看argv.mode的值,却发现函数体被压缩成一行,调试器直接跳过——这就是代价。我的实践是:所有配置对象必须显式声明,禁止嵌套过深。例如处理CSS加载,新手常写:
{ module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader', 'postcss-loader'] } ] } }这看起来干净,但问题在于:css-loader的importLoaders参数设多少?postcss-loader用没用autoprefixer?style-loader的injectType是styleTag还是singleton?全部隐藏在loader默认值里。我的写法是:
{ module: { rules: [ { test: /\.css$/, use: [ { loader: 'style-loader', options: { injectType: 'singleton', // 避免重复插入style标签 attributes: { id: 'app-styles' } // 便于JS动态控制 } }, { loader: 'css-loader', options: { importLoaders: 2, // 确保@import的css和postcss也能被处理 modules: { mode: 'local' } // 默认启用CSS Modules } }, { loader: 'postcss-loader', options: { postcssOptions: { plugins: [ require('autoprefixer')({ overrideBrowserslist: ['> 1%', 'last 2 versions'] }), require('cssnano')({ preset: 'default' }) // 生产环境才压缩 ] } } } ] } ] } }每行配置都回答一个具体问题:为什么选这个值?影响什么场景?有没有例外?这种写法会让配置文件体积增大30%,但节省的排查时间远超于此。某次线上字体图标不显示,运维同学直接搜索font关键字,两分钟就定位到file-loader的name参数漏写了[contenthash],而不用去翻node_modules里loader的源码。
2.3 构建流程的“四象限”治理模型:按职责划分配置模块
我把webpack配置按构建阶段划分为四个不可混淆的象限,每个象限只解决一类问题,严禁越界:
- 输入象限(Entry & Resolve):只管“从哪开始读代码”,包括
entry路径、resolve.alias别名、resolve.extensions后缀解析。这里绝不出现任何loader或plugin。 - 转换象限(Module Rules):只管“怎么翻译代码”,包括
module.rules里的所有loader。这里不涉及输出路径、压缩、HTML注入等下游操作。 - 优化象限(Optimization & Plugins):只管“怎么让产物更好”,包括
optimization.splitChunks、TerserPlugin、HtmlWebpackPlugin。这里不修改源码解析逻辑。 - 输出象限(Output & DevServer):只管“生成到哪、怎么服务”,包括
output.path、output.publicPath、devServer配置。这里不参与任何代码转换。
这个模型的价值在于:当npm run build报错说Can't resolve 'lodash',你立刻知道该去“输入象限”查resolve.alias或externals;当打包后CSS背景图404,你直奔“输出象限”检查publicPath是否漏了CDN域名;当首屏加载慢,你聚焦“优化象限”分析splitChunks.cacheGroups是否把React全家桶错误地拆散了。我曾帮某电商团队重构配置,他们原来的webpack.config.js里,entry下面混着HtmlWebpackPlugin配置,module.rules里藏着DefinePlugin的环境变量注入——结果一次externals调整,导致所有HTML模板里的<%= htmlWebpackPlugin.options.title %>全部变成undefined,因为插件执行时机被loader干扰了。分层之后,这类问题归零。
3. 核心配置模块详解:从原理到避坑的完整链路
3.1 入口与模块解析:alias、extensions、mainFields的协同陷阱
入口配置表面简单,实则暗流涌动。entry支持字符串、数组、对象、函数四种形式,但90%的项目只需用对象形式:
entry: { app: './src/index.js', vendor: ['react', 'react-dom', 'lodash'] // 单独提取第三方库 }关键在resolve配置。resolve.alias常被滥用为“快捷路径”,比如@: './src',但这里有个致命细节:alias的值必须以/结尾。写成'@': './src'会导致import '@/utils'解析为./srcutils(缺少斜杠),而'@/': './src/'才能正确映射。这个错误在VS Code里不报红,但构建时Cannot find module '@/utils',新人常在此卡一上午。
resolve.extensions决定文件后缀匹配顺序。默认是['.js', '.json', '.wasm'],但现代项目普遍加.ts、.jsx、.vue。顺序很重要:把.js放在.ts前面,会导致import './foo'优先匹配foo.js而非foo.ts,即使foo.ts存在。我的经验是按编译优先级降序排列:['.tsx', '.ts', '.jsx', '.js', '.json'],确保TypeScript文件永远优先生效。
resolve.mainFields更隐蔽。它控制package.json里哪个字段作为入口。默认['browser', 'module', 'main'],但很多UI组件库(如Ant Design)的browser字段指向未编译的ES6代码,而你的项目用Babel转译,这就产生双重转译:先由组件库的browser入口提供ES6,再被你的Babel处理。结果是node_modules里本该跳过的async/await又被编译一遍,体积增大且破坏tree-shaking。解决方案是显式指定mainFields: ['module', 'main'],强制走ESM规范入口,让Webpack原生处理模块依赖。
提示:
resolve.alias和resolve.modules的区别常被混淆。alias是路径替换(@/utils → src/utils),modules是模块搜索目录(import 'lodash'时,除了node_modules,还去src/lib里找)。后者在微前端场景极有用:子应用可将父应用的公共库目录加入modules,实现跨应用共享而不需npm install。
3.2 模块规则深度解析:oneOf、issuer、resourceQuery的精准控制
module.rules是配置中最易失控的部分。新手常犯的错误是写一堆test: /\.js$/规则,结果babel-loader和eslint-loader(已废弃,此处举例)同时处理同一文件,前者编译后者校验,效率低下且可能冲突。oneOf是破局关键:
module: { rules: [ { oneOf: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, use: ['babel-loader'] }, { test: /\.(ts|tsx)$/, exclude: /node_modules/, use: ['ts-loader'] }, { test: /\.css$/, use: ['style-loader', 'css-loader'] } ] } ] }oneOf确保每个文件只被第一个匹配的规则处理,避免重复劳动。但要注意:oneOf数组外的规则(如{ parser: { requireEnsure: false } })仍会全局生效。
更精细的控制靠issuer和resourceQuery。issuer指“谁引入了当前文件”。比如图片资源,通常希望import './logo.png'走url-loader(转base64),但background: url('./logo.png')在CSS里应走file-loader(生成独立文件)。这时用issuer区分:
{ test: /\.(png|jpe?g|gif|svg)$/, oneOf: [ { issuer: /\.[jt]sx?$/, // 被JS/TS文件引入 use: { loader: 'url-loader', options: { limit: 8192 } // 小于8KB转base64 } }, { issuer: /\.css$/, // 被CSS文件引入 use: 'file-loader' } ] }resourceQuery则针对URL参数。Vue单文件组件里<img src="./logo.png?inline">希望内联,而<img src="./logo.png">走常规流程。配置如下:
{ test: /\.(png|jpe?g|gif|svg)$/, resourceQuery: /inline/, // 匹配?inline参数 use: 'url-loader' }这种精准控制让构建行为完全符合业务语义,而不是靠文件后缀一刀切。
3.3 代码分割与懒加载:splitChunks的cacheGroups实战策略
optimization.splitChunks是Webpack最强大也最易误用的功能。默认配置对小型项目够用,但中大型项目必须定制。核心是cacheGroups——它定义“哪些模块该被抽成公共chunk”。常见误区是盲目设置chunks: 'all',结果把moment.js的中文语言包(仅在管理后台用)和react-router(全站用)强行捆进同一个vendors~app.js,导致首页白屏时间增加。
我的策略分三层:
- 框架层:单独提取React生态核心库,确保版本锁定且复用率最高。
frameworks: { name: 'frameworks', test: /[\\/]node_modules[\\/](react|react-dom|react-router|history)[\\/]/, chunks: 'all', enforce: true // 强制提取,无视minSize限制 }- 工具层:提取高频通用工具,如
lodash、axios、date-fns,但排除业务工具包(避免污染)。
utils: { name: 'utils', test: /[\\/]node_modules[\\/](lodash|axios|date-fns)[\\/]/, chunks: 'all', minChunks: 2 // 至少被2个chunk引用才提取 }- 业务层:按路由或功能域提取,这是提升首屏性能的关键。
pages: { name: 'pages', test: /[\\/]src[\\/](pages|features)[\\/]/, chunks: 'async', // 只处理异步chunk(即import()动态导入) minChunks: 1, priority: 20 // 优先级高于框架层,确保业务代码不被框架chunk吞并 }priority参数至关重要:当一个模块同时匹配多个group,高优先级者胜出。没有它,src/pages/Home/index.js可能被误判为frameworks组的一部分。
注意:
splitChunks.maxInitialRequests默认3,意味着初始加载最多请求3个chunk。若你有10个路由,每个路由抽一个chunk,首页会并发请求10个JS,触发浏览器连接数限制。应设为maxInitialRequests: 5,并配合import(/* webpackChunkName: "home" */ './pages/Home')手动命名chunk,便于监控。
3.4 插件系统精要:DefinePlugin、HtmlWebpackPlugin、CleanWebpackPlugin的协同逻辑
插件是webpack的“肌肉”,但肌肉乱长会撕裂身体。DefinePlugin常被误用为“全局变量注入”,比如new DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') })。这没问题,但若同时用dotenv-webpack插件加载.env文件,两者会冲突:dotenv把环境变量挂到process.env,而DefinePlugin在编译时做字符串替换,process.env.API_URL在代码里实际是"https://api.example.com",但console.log(process.env)却显示空对象——因为process.env本身是Node.js运行时对象,Webpack只替换了代码中的引用,没创建真实对象。正确做法是统一用DefinePlugin,所有环境变量显式声明:
new DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(mode), 'process.env.API_URL': JSON.stringify(process.env.API_URL || 'http://localhost:3000'), '__DEV__': JSON.stringify(mode === 'development') })__DEV__这种布尔常量比字符串判断更高效,且Tree Shaking能自动移除if (__DEV__) { console.log(...) }块。
HtmlWebpackPlugin负责生成HTML,但它的template选项常被忽视。默认template: 'src/index.html',但若模板里有<%= htmlWebpackPlugin.options.title %>,而配置里没传title,就会渲染成空字符串。我的模板强制包含:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title><%= htmlWebpackPlugin.options.title || 'My App' %></title> </head> <body> <div id="root"></div> </body> </html>并配置title: 'Production App',确保任何环境都有兜底。
CleanWebpackPlugin看似简单,但cleanOnceBeforeBuildPatterns的路径写错会删光项目。['dist/**/*', '!dist/static']想保留dist/static,但!语法在glob中不被所有版本支持。更可靠的是用dry: true先试运行,或改用cleanStaleWebpackAssets: true(Webpack 5+内置)。
4. 实操全流程:从零搭建可维护的配置体系
4.1 目录结构与文件职责划分
我坚持的目录结构经过十多个项目验证,兼顾清晰性和扩展性:
config/ ├── webpack/ # Webpack配置根目录 │ ├── common/ # 共用基础配置 │ │ ├── base.js # entry/output/resolve等骨架 │ │ ├── loaders/ # 所有loader配置(js、css、assets) │ │ └── plugins/ # 基础插件(DefinePlugin、ProgressPlugin) │ ├── dev/ # 开发环境特有 │ │ ├── index.js # 合并common + dev专属 │ │ └── dev-server.js # devServer详细配置 │ └── prod/ # 生产环境特有 │ ├── index.js # 合并common + prod专属 │ └── optimization.js # splitChunks/Terser等优化配置 ├── paths.js # 路径常量(src、dist、public等) └── utils.js # 工具函数(如getEntries()动态获取入口)paths.js是灵魂:
const path = require('path'); const appDir = path.resolve(__dirname, '../'); // 项目根目录 module.exports = { appSrc: path.resolve(appDir, 'src'), appDist: path.resolve(appDir, 'dist'), appPublic: path.resolve(appDir, 'public'), appHtml: path.resolve(appDir, 'public/index.html'), appPackageJson: path.resolve(appDir, 'package.json') };所有配置文件通过const paths = require('./paths')引入,避免硬编码路径。当项目从/my-app迁移到/frontend/my-app时,只需改paths.js,无需搜索替换所有../src。
4.2 动态入口与多页面支持:摆脱硬编码entry
单页应用(SPA)用entry: './src/index.js'足够,但多页应用(MPA)如后台管理系统,每个页面(用户管理、订单管理)都是独立入口,硬编码entry: { user: './src/user/index.js', order: './src/order/index.js' }会导致每次新增页面都要改配置。我的方案是约定优于配置:
// config/webpack/common/base.js const fs = require('fs'); const path = require('path'); const paths = require('../../paths'); function getEntries() { const entries = {}; const pagesDir = path.resolve(paths.appSrc, 'pages'); if (!fs.existsSync(pagesDir)) return entries; fs.readdirSync(pagesDir).forEach(pageName => { const pagePath = path.resolve(pagesDir, pageName); if (fs.statSync(pagePath).isDirectory()) { const indexPath = path.resolve(pagePath, 'index.js'); if (fs.existsSync(indexPath)) { entries[pageName] = indexPath; } } }); return entries; } module.exports = { entry: getEntries(), output: { path: paths.appDist, filename: 'js/[name].[contenthash:8].js', chunkFilename: 'js/[name].[contenthash:8].chunk.js' } };只要在src/pages/下新建文件夹user,放index.js,配置自动识别。配合HtmlWebpackPlugin动态生成:
// config/webpack/prod/index.js const HtmlWebpackPlugin = require('html-webpack-plugin'); const entries = require('../common/base').entry; module.exports = { plugins: Object.keys(entries).map(name => new HtmlWebpackPlugin({ template: paths.appHtml, filename: `${name}.html`, chunks: [name, 'frameworks', 'utils'], // 显式指定依赖chunk title: `${name.charAt(0).toUpperCase() + name.slice(1)} Page` }) ) };新增页面零配置,且每个HTML只加载自身所需chunk,避免资源浪费。
4.3 构建分析与性能调优:用stats和speed-measure-webpack-plugin定位瓶颈
配置写完不等于结束。webpack --profile --json > stats.json生成的JSON文件,配合webpack-bundle-analyzer可视化,是性能调优的起点。但更高效的是实时监控。我在dev配置中集成speed-measure-webpack-plugin:
// config/webpack/dev/index.js const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); const smp = new SpeedMeasurePlugin(); module.exports = smp.wrap({ // 原始配置... });启动npm run dev,终端立即显示各loader/plugin耗时:
SMP ⏱ General output time took 12.4 secs SMP ⏱ Plugins TerserPlugin took 3.2 secs HtmlWebpackPlugin took 1.8 secs SMP ⏱ Loaders babel-loader took 8.7 secs css-loader took 2.1 secs当babel-loader耗时突增,说明.babelrc里可能加了冗余插件;当HtmlWebpackPlugin变慢,检查是否启用了minify(开发环境不该压缩)。这种量化反馈让优化有的放矢。
另一个关键是stats配置。默认stats: 'normal'信息过少,我设为:
stats: { all: false, errors: true, warnings: true, timings: true, assets: true, modules: false, // 关闭模块详情,避免刷屏 children: false }构建失败时,错误信息精准定位到src/utils/request.js:42:15,而非淹没在千行模块列表中。
4.4 CI/CD集成与环境隔离:确保本地与线上行为一致
很多团队的痛点是:“本地跑得好好的,CI上构建就失败”。根源常在环境变量和路径差异。我的CI配置强制三点:
- Node.js版本锁定:
.nvmrc文件写v16.14.0,CI脚本执行nvm use,避免fs.promises等API差异。 - 环境变量纯净化:CI中禁用
dotenv,所有变量通过CI平台注入(如GitHub Actions的env),并在webpack.config.js中显式读取:
// config/webpack/common/base.js const API_URL = process.env.CI ? 'https://prod-api.example.com' : 'http://localhost:3000';- 构建产物校验:CI最后一步运行
webpack-bundle-analyzer --mode static --open false生成report.html,并检查dist/js/app.*.js大小是否超过阈值:
# CI script SIZE=$(stat -c%s "dist/js/app.*.js") if [ $SIZE -gt 500000 ]; then echo "Bundle size exceeds 500KB!" exit 1 fi这比“构建成功”更有意义——它确保每次提交都符合性能基线。
5. 常见问题与独家排查技巧
5.1 “Module not found”类错误的三级定位法
这类错误占构建问题的60%以上,不能只看报错信息。我的排查流程分三级:
- 一级:路径是否存在?
报错Can't resolve './utils' in '/src/pages/home',先确认src/pages/home/utils目录是否存在。用ls -la src/pages/home/utils验证,避免IDE缓存误导。 - 二级:resolve配置是否生效?
在webpack.config.js中临时加console.log('resolve:', config.resolve),检查alias、modules是否按预期加载。特别注意resolve.alias的键名是否带$('@': './src'vs'@': './src$'),后者严格匹配@/开头的路径。 - 三级:模块导出是否正确?
进入报错模块目录,检查index.js是否导出目标。常见陷阱:export default function foo(){}在CJS环境需module.exports = foo,否则require('./utils')得到{ default: foo }而非foo。用node -e "console.log(require('./utils'))"直接测试。
实操心得:在
package.json的scripts里加一条"debug:resolve": "node -p \"require('webpack').getResolve({ resolve: require('./config/webpack/common/base').resolve })('./src/utils')\"",一键验证resolve逻辑,比反复改配置重启快十倍。
5.2 热更新(HMR)失效的七种原因与修复
HMR是开发体验的生命线,失效时不要盲目重启。按优先级检查:
- devServer.hot是否为true?
devServer: { hot: true }是前提,且Webpack 5+要求同时设hot: 'only'(避免全量刷新)。 - 入口文件是否调用
module.hot.accept?
React项目需在index.js末尾加:if (module.hot) { module.hot.accept('./App', () => { const NextApp = require('./App').default; render(<NextApp />, document.getElementById('root')); }); } - CSS是否配置了
style-loader?css-loader不支持HMR,必须搭配style-loader或mini-css-extract-plugin(开发环境用前者)。 - 文件监听是否被杀毒软件干扰?
Windows上McAfee常禁用chokidar的文件监听,加devServer.watchOptions: { usePolling: true, poll: 1000 }强制轮询。 - node_modules是否被排除?
devServer.watchOptions.ignored默认排除node_modules,但若你在node_modules里调试第三方库,需显式移除。 - 端口是否被占用?
devServer.port冲突时,HMR WebSocket连接失败,错误日志里会有net::ERR_CONNECTION_REFUSED。 - React Fast Refresh是否启用?
Webpack 5+推荐用@pmmmwh/react-refresh-webpack-plugin替代旧HMR,需在babel-loader选项中加plugins: ['react-refresh/babel']。
5.3 生产构建体积异常的诊断清单
当npm run build后dist体积暴涨,按此清单逐项核对:
| 检查项 | 命令/方法 | 预期结果 | 修复方案 |
|---|---|---|---|
| 是否启用了SourceMap? | grep -r "source-map" dist/ | 不应存在.map文件 | devtool: falseorhidden-source-map |
是否误将devDependencies打入包? | npx source-map-explorer dist/js/*.js | lodash等应出现在vendors而非app | externals: { react: 'React', 'react-dom': 'ReactDOM' } |
| CSS是否重复打包? | grep -r "background" dist/css/*.css | 同一背景图不应出现多次 | css-loader设modules: { auto: true }启用模块化 |
| 图片是否未压缩? | ls -la dist/media/ | PNG应小于原始尺寸30% | 加image-minimizer-webpack-plugin |
| Moment.js是否包含全部语言包? | npx source-map-explorer dist/js/*.js | grep moment | 应只含zh-cn而非全部20+语言 | new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn/) |
独家技巧:在
webpack.prod.js中临时加performance: { hints: 'warning', maxAssetSize: 250000, maxEntrypointSize: 500000 },构建时超限文件会标黄警告,比事后分析快得多。
5.4 多环境配置合并的陷阱与绕过方案
webpack-merge是主流方案,但它有两大缺陷:一是merge函数会浅拷贝数组,导致plugins: [A, B]与plugins: [C]合并成[A, B, C],但若C是HtmlWebpackPlugin,而A是DefinePlugin,顺序错乱会引发变量未定义;二是merge.smart对module.rules的use数组处理不一致。我的绕过方案是函数式组合:
// config/webpack/common/index.js const baseConfig = require('./base'); const loaders = require('./loaders'); const plugins = require('./plugins'); module.exports = (env, argv) => { const config = { ...baseConfig }; config.module = { ...config.module, ...loaders(env, argv) }; config.plugins = [...config.plugins, ...plugins(env, argv)]; return config; };每个子配置(loaders.js、plugins.js)都是纯函数,接收env和argv,返回对应片段。dev.js和prod.js只需调用commonConfig(env, argv)再叠加专属配置,彻底规避合并逻辑的不确定性。某金融项目因此将配置维护时间从平均4小时/次降至15分钟/次。
6. 配置演进与未来适配:从Webpack到Vite/Rspack的平滑过渡
Webpack配置整理的终极目的,不是把它供起来,而是为未来迁移铺路。Vite和Rspack虽快,但现有项目不可能一夜重写。我的过渡策略是“能力解耦”:
- 构建能力抽象:将
babel-loader、ts-loader等封装为独立配置文件(config/babel.js、config/tsconfig.json),Webpack配置只做引用。迁移到Vite时,vite.config.ts直接复用这些文件,无需重写规则。 - 环境变量统一:所有环境变量通过
DefinePlugin注入,Vite用define选项,Rspack用builtins.define,接口高度一致。 - 静态资源处理标准化:
public/目录下的文件,Webpack用CopyWebpackPlugin,Vite原生支持,Rspack用asset/resource,行为统一为“原样复制”。
更重要的是认知升级:Webpack配置的本质是描述构建意图,而非编写构建脚本。entry描述“从哪开始”,output描述“生成到哪”,rules描述“如何转换”。Vite的resolve.alias、Rspack的module.rules,只是同一意图的不同语法糖。我经手的三个项目,从Webpack迁移到Rspack,配置重写工作量不到20%,因为80%的逻辑(路径、别名、环境变量)完全复用。
最后分享一个小技巧:在
package.json的scripts里加"inspect": "webpack --config config/webpack/prod/index.js --env production --mode production --stats verbose > webpack-stats.json",然后用VS Code的JSON插件打开webpack-stats.json,搜索"reasons"字段,能看到每个模块为何被打包进来——这是理解依赖关系最直观的方式,比任何文档都管用。