接手一个老项目,打开 webpack.config.js,两百行配置里一半的字段你都说不上来它在管什么,改一个 publicPath 得先在本地试三遍。这种情况我遇到过不止一次,问题不在配置难,在于大部分教程都是给你一份能跑的模板,没告诉你每个字段作用在构建的哪一步。
这篇是我当年整理 webpack4 配置时的逐字段笔记,按配置文件从上到下的顺序走一遍:每个字段管什么、典型取值是什么、什么场景下必须改。原文写于 webpack4 刚发布不久,现在 webpack 5 已经是主流,所以每一节后面我另起了一小段说明这块在新版本里有没有变化、该怎么迁移,老写法本身完整保留,因为老项目里它们还在跑。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
entry的单文件和多文件两种写法,以及vendors入口的历史用途output里path/filename/chunkFilename/publicPath分别决定什么hash、chunkhash、contenthash三种占位符的区别和选用场景mode这个 webpack4 新增字段到底替代了什么devtool各档位的取舍,以及生产环境的正确选择optimization下minimize/splitChunks/runtimeChunk的作用resolve的extensions/alias/modules三件套module.rules的匹配规则和常见 loader 组合,含原文里一处配错的地方devServer的代理、history 回退、热更新配置- webpack4 删掉了哪些东西,以及这份配置迁到 webpack 5 要动哪几处
# 一、entry,构建的起点
entry 告诉 webpack 从哪个文件开始分析依赖。最常见的是对象形式,key 是 chunk 名字,value 是入口文件路径。
//方式一:单文件写法
entry: {
index: './src/pages/route.js',
//about: './src/pages/about.js',
//other:()=>{...}
}
//方式二:多文件写法
entry: {
/*index:[
'webpack-hot-middleware/client',
'./src/root.js'
],*/
index: ['./src/root.js'],
vendors : ['react','react-dom','redux','react-router','classnames'],
}
上面两段的差别在 value 的类型。单个字符串就是一个入口一个文件;数组形式表示这个 chunk 由多个文件合并而成,webpack 会按数组顺序把它们打进同一个 chunk,最后一个文件的导出作为整个 chunk 的导出。
被注释掉的那段 'webpack-hot-middleware/client' 值得说一句。用 webpack-dev-middleware + webpack-hot-middleware 自己搭 Node 服务时,热更新的客户端脚本得手动塞进入口数组里,dev-server 帮你做了这件事你才感觉不到它。
vendors 这个入口是那个年代的典型写法:把第三方库单独列成一个入口,配合 CommonsChunkPlugin 抽出来做长期缓存。
放到今天这条路已经不用了。webpack4 之后 splitChunks 用 test: /[\\/]node_modules[\\/]/ 就能自动识别第三方依赖,不需要你手写一份依赖清单。手写清单最大的麻烦是维护,装了个新库忘了加进去,它就悄悄跑进业务 chunk 里了。同一时期还有 DllPlugin 那条路子,我在 dll 预编译提高 webpack 打包速度 里单独写过,那篇也说明了为什么现在不推荐再用它。
# 二、output,产物往哪放
output 决定打包结果落到哪、叫什么名字、页面通过什么路径去请求它。
path: 输出文件的目录filename: 输出的文件名,它一般跟你entry配置相对应,如js/[name].js,[name]在这里表示的是index、vendors这些入口名chunkFilename:块,配置了它,非入口entry的模块会被自动拆分文件,也就是大家常说的按需加载,与路由中的require.ensure相互对应publicPath:文件输出的公共路径pathinfo:保留相互依赖的包中的注释信息,这个基本不用主动设置,development模式下默认true,production模式下默认false- 主要的就是这些,还有一些其他的
library、libraryTarget、auxiliaryComment等
output: {
path: path.resolve(__dirname, '../assets'),
filename: 'js/[name].js',
chunkFilename: 'js/[name].[chunkhash:8].js',
publicPath: '/_static_/', //最终访问的路径就是:localhost:3000/_static_/js/*.js
//pathinfo:true,
}
这里最容易出问题的是 publicPath。它不影响文件写到磁盘的哪个目录,只影响运行时资源 URL 的前缀。异步 chunk 加载不出来、字体图片 404,十次里有八次是它配错了。本地开发一般用 /,部署到 CDN 就换成完整域名,部署在子路径下就写 /子路径/。
filename 和 chunkFilename 的分工也常被搞混。前者管有名字的入口 chunk,后者管那些通过动态导入产生的、没人给它起名的 chunk。原文里入口不带 hash、异步 chunk 带 [chunkhash:8],这种搭配在有服务端模板渲染 HTML 的项目里能见到,纯前端项目一般两边都要带 hash。
require.ensure 是 webpack 1 时代的按需加载 API。现在统一用 import('./xxx'),它是 ECMAScript 标准语法,配合 /* webpackChunkName: "foo" */ 魔法注释还能给 chunk 起名字。require.ensure 至今仍能用,但没有理由在新代码里写它。
# 三、hash 占位符怎么选
文件名里的那几个方括号占位符,用错了会直接毁掉浏览器缓存。常用的有三种:
| 模板 | 描述 |
|---|---|
hash |
模块标识符的 hash,一般应用于 filename:'[name].[hash].js' |
chunkhash |
按需加载块内容的 hash,根据 chunk 自身的内容计算而来,'js/[name].[chunkhash:8].js' |
contenthash |
在提取 css 文件时根据内容计算而来的 hash,结合 ExtractTextWebpackPlugin 插件使用 |
hash 长度 |
默认 20,可自定:[hash:8]、[chunkhash:16] |
三者的关键区别在计算范围。[hash] 是整次构建一个值,你改任何一个文件,所有产物的文件名全变,缓存全部作废,所以生产环境几乎不该用它。[chunkhash] 按 chunk 算,改业务代码不影响 vendors chunk 的名字。[contenthash] 按最终文件内容算,粒度最细。
那为什么 JS 和 CSS 打在一个 chunk 里时非要用 [contenthash] 呢?因为一个 chunk 会同时产出 .js 和 .css 两个文件,用 [chunkhash] 的话,你只改了一行 CSS,同 chunk 的 JS 文件名也跟着变,白白让用户重新下一遍 JS。
现在的建议很简单:生产环境所有产物统一用 [contenthash]。原文提到的 ExtractTextWebpackPlugin 在 webpack4 之后已经换成了 mini-css-extract-plugin,它对 [contenthash] 的支持更完整。这块的迁移细节我在 webpack4 升级篇 里展开写了。
# 四、mode,webpack4 省下的一大堆配置
mode 属于 webpack4 才新增的字段,4 之前大家一般用 DefinePlugin 手动设置环境变量,再自己挨个开插件。
mode取值为development、production、nonedevelopment:开发模式,打包的代码不会被压缩,开启代码调试production:生产模式,正好反之
//方法一
webpack --mode development/production
//方法二
……
mode:'development/production'
……
这个字段的价值不在于它本身,而在于它背后自动开了一整套预设。设成 production 之后,webpack 会自动把 optimization.minimize、optimization.usedExports、optimization.concatenateModules、optimization.sideEffects 这些统统打开,同时把 process.env.NODE_ENV 注入成 'production'。设成 development 则相反,走的是不压缩、保留可读性、开 namedModules 那套。
先说结论,绝大多数项目只需要设 mode,不需要再手动配那一堆开关。我见过不少配置文件里 mode: 'production' 和一长串 optimization 手动开关同时存在,其实是重复劳动,还容易改出前后矛盾的组合。
none 这个值是给要做精细控制或者调试 webpack 本身的场景用的,它什么优化都不开,产物最接近原始模块结构。想看清楚 webpack 的运行时代码长什么样,用它跑一次很直观。
webpack 5 这块没变,mode 的语义和默认值都延续下来了。
# 五、devtool,SourceMap 的取舍
devtool 控制是否生成、以及如何生成 source map 文件,开发环境下更有利于定位问题,默认 false。它的开启会影响编译速度,所以生产环境一定要谨慎。
常用的值有 cheap-eval-source-map、eval-source-map、cheap-module-eval-source-map、inline-cheap-module-source-map 等等,更详细的可以去官方文档查看。一般使用 eval-source-map 较多,每个取值都有它不一样的特性。
这些看着眼花的名字其实是几个关键词拼出来的,拆开就好记了:
eval:用eval()包裹模块代码,构建和重建都最快,代价是产物里全是字符串cheap:只映射到行,不映射到列,也不映射 loader 处理前的源码,速度提升明显module:把cheap丢掉的那部分补回来,映射回 loader 处理之前的原始源码inline:map 内容直接以 base64 塞进产物文件,不单独产出.maphidden:生成 map 文件但不在产物末尾写引用注释
这里有个坑要注意,cheap 不带 module 的时候,你在浏览器里看到的是 Babel 转译后的代码,不是你写的那份 JSX 或 TS。调试组件时发现断点位置对不上,多半就是漏了 module。