Webpack 4在2018年2月正式发布,带来了很多令人兴奋的新特性和性能改进。比如零配置启动、mode模式、更快的构建速度、更好的Tree Shaking、全新的SplitChunksPlugin等等。
但是,我发现很多人用Webpack 4,还停留在入门配置的阶段。配置文件里,就是几个loader和plugin,能打包就行,没有充分发挥Webpack 4的强大能力。打包出来的文件很大,构建速度很慢,缓存策略也不合理,性能还有很大的优化空间。
最近,我花了一些时间,深入研究了Webpack 4的配置,整理了一些进阶技巧。这些技巧,有的是Webpack 4的新特性,有的是最佳实践,有的是性能优化的经验。掌握了这些技巧,能让你的Webpack配置更专业、更高效,打包出来的文件更小、更快,构建速度也能提升不少。
今天,我想把这些技巧分享给大家。本文假设你已经有Webpack的基础,知道什么是loader和plugin,能写基本的配置文件。如果你是Webpack新手,建议先看看官方的入门教程,再来读这篇文章。
一、mode模式:不只是设置环境变量
Webpack 4最大的变化之一,就是引入了mode模式。你可以在配置文件里设置mode为development、production或者none,Webpack会根据不同的mode,自动启用一些内置的优化。
很多人用mode,只是简单地设置一下,以为mode只是设置环境变量,没有深入理解不同mode背后的区别。实际上,不同的mode,Webpack会启用完全不同的优化策略,对打包结果有很大的影响。
development模式,会启用以下优化:
- 开启调试工具(devtool: eval),方便调试
- 不压缩代码,保留代码的可读性
- 不启用Tree Shaking,保留所有代码
- 不启用代码分割的优化
- 输出的bundle包含路径信息和模块名称,方便调试
production模式,会启用以下优化:
- 启用代码压缩(TerserPlugin),减小bundle体积
- 启用Tree Shaking,移除未使用的代码
- 启用作用域提升(Module Concatenation),减少函数嵌套,提升运行时性能
- 启用SplitChunksPlugin的默认配置,自动分割公共代码
- 设置process.env.NODE_ENV为production,让React等库自动切换到生产模式
- 不开启调试工具,移除console.log等调试代码
none模式,不启用任何内置优化,完全由你自己配置。
理解了不同mode的区别之后,你就知道,不能在开发环境用production模式,也不能在生产环境用development模式。开发环境用development模式,打包快,调试方便;生产环境用production模式,打包出来的文件小,性能好。
而且,mode还可以和其他配置配合使用。比如,在production模式下,你可以自定义TerserPlugin的配置,调整压缩策略;也可以自定义SplitChunksPlugin的配置,更精细地控制代码分割。
一个小技巧:如果你想在不同的环境下用不同的配置,可以把配置文件拆分成webpack.common.js、webpack.dev.js、webpack.prod.js,用webpack-merge合并。这样,公共配置写在common里,环境相关的配置写在dev和prod里,结构更清晰。
二、SplitChunksPlugin:代码分割的正确姿势
Webpack 4用SplitChunksPlugin替代了原来的CommonsChunkPlugin,用来做代码分割。SplitChunksPlugin比CommonsChunkPlugin更强大,也更灵活,但是配置也更复杂。
很多人用SplitChunksPlugin,只是用默认配置,或者简单地设置一下chunks: 'all',没有充分发挥它的能力。实际上,合理配置SplitChunksPlugin,能大大减小bundle体积,提升加载性能。
先看一下SplitChunksPlugin的默认配置:
splitChunks: {
chunks: 'async', // 只对异步加载的chunk生效
minSize: 30000, // 分割出来的chunk最小30KB
minChunks: 1, // 至少被1个chunk引用
maxAsyncRequests: 5, // 异步加载时最大并行请求数
maxInitialRequests: 3, // 初始加载时最大并行请求数
automaticNameDelimiter: '~', // 文件名分隔符
name: true, // 自动生成文件名
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/, // 匹配node_modules里的模块
priority: -10 // 优先级
},
default: {
minChunks: 2, // 至少被2个chunk引用
priority: -20, // 优先级
reuseExistingChunk: true // 复用已存在的chunk
}
}
}默认配置,只对异步加载的chunk做分割,而且vendors的优先级比较低。对于大多数项目,默认配置可能不够用,需要自定义。
下面是我推荐的一个进阶配置:
splitChunks: {
chunks: 'all', // 对同步和异步的chunk都生效
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
// 第三方库,单独打包
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10 // 优先级高,先匹配
},
// React相关的库,单独打包(因为比较大,而且不常变)
react: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router|redux|react-redux)[\\/]/,
name: 'react',
chunks: 'all',
priority: 20 // 优先级比vendors高,先匹配
},
// 工具类库,单独打包
utils: {
test: /[\\/]node_modules[\\/](lodash|moment|axios)[\\/]/,
name: 'utils',
chunks: 'all',
priority: 20
},
// 公共业务代码,至少被2个chunk引用才分割
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true
}
}
}这个配置的思路是:
- chunks: 'all':对同步和异步的chunk都做分割,最大化代码复用。
- 按库的类型分割:把React相关的库、工具类库、其他第三方库,分别打包成不同的chunk。这样,每个chunk的职责单一,而且不常变的库(比如React)可以单独缓存,不会因为业务代码变化而重新下载。
- 公共业务代码单独打包:被多个chunk引用的公共业务代码,单独打包,避免重复。
- 优先级控制:通过priority控制匹配顺序,更具体的规则优先级更高,先匹配。
这样配置之后,打包出来的文件,会分成vendors.js、react.js、utils.js、common.js、业务代码.js等多个文件。浏览器可以并行下载这些文件,而且不常变的文件可以长期缓存,大大提升加载性能。
一个注意点:不要把chunk分割得太细,否则会导致请求数太多,反而影响性能。maxAsyncRequests和maxInitialRequests就是用来控制最大并行请求数的,要根据你的项目情况合理设置。
三、Tree Shaking:让你的bundle更苗条
Tree Shaking是Webpack 2引入的特性,Webpack 4做了很大的改进,能更有效地移除未使用的代码。但是,很多人用Webpack 4,Tree Shaking并没有生效,或者效果不好,bundle里还是有很多没用的代码。
Tree Shaking的原理,是基于ES6模块的静态结构(import和export),在编译期分析哪些代码被使用了,哪些没有被使用,然后移除未使用的代码。
要让Tree Shaking生效,需要满足几个条件:
1. 使用ES6模块语法
Tree Shaking只能处理ES6模块(import/export),不能处理CommonJS模块(require/module.exports)。如果你的代码里用了CommonJS,或者你依赖的库是CommonJS格式的,Tree Shaking就无法生效。
所以,在写代码的时候,要用ES6模块语法,不要用CommonJS。另外,在配置babel的时候,要设置modules: false,让babel不要把ES6模块转换成CommonJS:
// .babelrc
{
"presets": [
["env", { "modules": false }]
]
}2. 在package.json里设置sideEffects
Webpack 4默认会假设所有模块都有副作用(side effects),不敢随便移除。因为,有些模块,虽然没有导出任何被使用的东西,但是它的执行会产生副作用(比如修改全局变量、注入CSS等),如果移除了,可能会出问题。
所以,你需要在package.json里设置sideEffects字段,告诉Webpack哪些模块是没有副作用的,可以安全地Tree Shaking:
{
"name": "my-project",
"sideEffects": false
}如果你的项目里,有些文件是有副作用的(比如CSS文件、polyfill文件等),可以把它们列出来:
{
"name": "my-project",
"sideEffects": [
"*.css",
"*.scss",
"src/polyfill.js"
]
}设置了sideEffects之后,Webpack就知道哪些模块可以安全地Tree Shaking,哪些不行,Tree Shaking的效果会大大提升。
3. 使用production模式
Tree Shaking只在production模式下才会真正移除未使用的代码。在development模式下,Webpack不会做Tree Shaking,因为要保留代码的可读性,方便调试。
所以,要验证Tree Shaking的效果,要用production模式打包,然后看打包出来的文件里,有没有未使用的代码。
4. 注意副作用函数
有些函数,看起来没有被使用,但是它有副作用(比如修改了全局变量、注册了事件监听器等),Webpack可能无法识别,会保留下来。这种情况,需要你自己检查,确保没有副作用的代码才可以被Tree Shaking移除。
一个小技巧:可以用webpack-bundle-analyzer插件,可视化地分析打包出来的bundle,看看哪些模块比较大,哪些代码没有被Tree Shaking掉,然后针对性地优化。
四、缓存策略:让用户第二次访问更快
缓存,是前端性能优化的重要一环。合理的缓存策略,能让用户第二次访问的时候,直接从浏览器缓存里读取文件,不需要重新下载,大大提升加载速度。
Webpack 4里,实现缓存策略的关键,是文件名的hash。当文件内容变化的时候,hash也会变化,浏览器就会下载新的文件;当文件内容不变的时候,hash也不变,浏览器就会用缓存里的文件。
Webpack支持三种hash:
- hash:整个项目的hash,任何一个文件变化,所有文件的hash都会变。不适合做缓存。
- chunkhash:每个chunk的hash,只有这个chunk的内容变化了,它的hash才会变。适合做JS文件的缓存。
- contenthash:每个文件内容的hash,只有这个文件的内容变化了,它的hash才会变。适合做CSS文件的缓存(因为CSS是从JS里提取出来的,用chunkhash的话,JS变了CSS的hash也会变,用contenthash就不会)。
推荐的缓存配置:
output: {
filename: '[name].[chunkhash:8].js', // JS用chunkhash
chunkFilename: '[name].[chunkhash:8].js', // 异步加载的chunk也用chunkhash
path: path.resolve(__dirname, 'dist')
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css' // CSS用contenthash
})
]这样配置之后,每个文件都有自己独立的hash。业务代码变了,只有业务代码的hash变,第三方库和公共代码的hash不变,浏览器可以继续用缓存。
但是,光有hash还不够,还有一个问题:Webpack的runtime代码,可能会被打包到每个chunk里,导致任何一个chunk变化,其他chunk的hash也会变。为了解决这个问题,需要把runtime代码单独提取出来:
optimization: {
runtimeChunk: {
name: 'runtime' // 把runtime代码单独打包成runtime.js
}
}这样,runtime代码单独打包,其他chunk的hash就不会因为runtime变化而变化了。runtime.js很小,单独下载也很快,而且它不常变,可以长期缓存。
另外,还要配合服务端的缓存策略。在服务端(比如Nginx),设置静态资源的Cache-Control为长期缓存(比如max-age=31536000),让浏览器长期缓存这些文件。因为文件名里有hash,文件内容变了文件名也会变,所以不用担心缓存过期的问题。
五、构建性能优化:让Webpack跑得更快
Webpack的构建速度,是很多人头疼的问题。项目大了之后,每次构建都要等好几分钟,开发体验很差。Webpack 4虽然比之前的版本快了很多,但是还是有优化的空间。
下面是一些常用的构建性能优化技巧:
1. 缩小loader的处理范围
loader处理文件是很耗时的,特别是babel-loader,处理一个JS文件要花不少时间。所以,要尽量缩小loader的处理范围,只处理需要处理的文件。
用include和exclude来指定loader的处理范围:
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
include: path.resolve(__dirname, 'src'), // 只处理src目录下的文件
exclude: /node_modules/ // 不处理node_modules里的文件
}
]
}这样,babel-loader只处理src目录下的JS文件,不会去处理node_modules里的成千上万的文件,构建速度会快很多。
2. 开启缓存
很多loader支持缓存,把处理结果缓存起来,下次构建的时候,如果文件没有变化,就直接用缓存,不需要重新处理。
比如,babel-loader可以开启缓存:
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 开启缓存
}
}
]
}开启缓存之后,第二次构建的速度会快很多。
另外,Webpack 4本身也有缓存,可以用cache-loader,或者在开发环境用webpack-dev-server的缓存。
3. 多线程构建
对于大项目,可以用多线程来加速构建。thread-loader(或者原来的happypack)可以把loader的处理放到多个线程里并行执行,充分利用CPU的多核性能。
const threadLoader = require('thread-loader');
// 预热thread-loader
threadLoader.warmup({}, ['babel-loader']);
module: {
rules: [
{
test: /\.js$/,
use: [
'thread-loader', // 放在babel-loader前面
'babel-loader'
],
include: path.resolve(__dirname, 'src')
}
]
}注意,thread-loader有启动开销,小项目用了反而可能更慢。只有项目比较大、文件比较多的时候,用thread-loader才有明显的加速效果。
4. 合理配置devtool
devtool决定了source map的生成方式,对构建速度有很大影响。不同的devtool选项,构建速度和调试体验是不一样的:
- eval:最快,每个模块用eval包裹,有简单的source map。适合开发环境。
- cheap-eval-source-map:比较快,有更准确的source map。适合开发环境。
- source-map:最慢,生成完整的source map。适合生产环境。
- cheap-module-source-map:比较慢,有较准确的source map。适合生产环境。
开发环境,推荐用eval或者cheap-eval-source-map,构建快,调试也够用。生产环境,推荐用source-map或者cheap-module-source-map,虽然构建慢一点,但是能生成准确的source map,方便线上排查问题。
5. 用DllPlugin预编译第三方库
如果项目里的第三方库很多,而且不常变,可以用DllPlugin把第三方库预编译成一个单独的文件,每次构建的时候,不需要重新编译第三方库,只需要编译业务代码,构建速度会大大提升。
DllPlugin的配置稍微复杂一点,需要两个配置文件:一个是webpack.dll.config.js,用来预编译第三方库;一个是正常的webpack.config.js,引用预编译好的库。
具体的配置方法,可以参考Webpack官方文档。这里就不展开了。
六、环境变量管理:优雅地处理不同环境的配置
前端项目,通常有多个环境:开发环境、测试环境、生产环境。不同的环境,配置不一样,比如API地址、是否开启调试、是否压缩代码等。
很多人处理环境变量,是在代码里硬编码,或者用多个配置文件,手动切换。这样很不优雅,也容易出错。
Webpack 4里,可以用DefinePlugin来优雅地管理环境变量。DefinePlugin可以在编译期,把代码里的某些变量替换成指定的值,实现不同环境的不同配置。
首先,在配置文件里,根据不同的mode,设置不同的环境变量:
const webpack = require('webpack');
module.exports = (env, argv) => {
const isProd = argv.mode === 'production';
return {
// ... 其他配置 ...
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(isProd ? 'production' : 'development'),
'API_BASE_URL': JSON.stringify(isProd ? 'https://api.example.com' : 'http://localhost:3000'),
'ENABLE_DEBUG': JSON.stringify(!isProd)
})
]
};
};然后,在业务代码里,就可以直接用这些变量:
// 业务代码
const apiUrl = API_BASE_URL + '/users';
if (ENABLE_DEBUG) {
console.log('调试模式开启');
}Webpack在编译的时候,会把这些变量替换成具体的值。比如,在生产环境,APIBASEURL会被替换成'https://api.example.com',ENABLEDEBUG会被替换成false。而且,因为ENABLEDEBUG是false,if (ENABLE_DEBUG) { ... }里的代码,会被Tree Shaking移除掉,不会出现在生产环境的bundle里。
这样,就实现了不同环境的不同配置,而且很优雅,不需要手动切换,也不会把测试环境的配置带到生产环境。
一个注意点:DefinePlugin替换的是字面量,所以字符串类型的值,要用JSON.stringify包一下,否则替换出来会是变量名,不是字符串。
七、一些实用的loader和plugin
最后,推荐一些实用的loader和plugin,能让你的Webpack配置更强大。
loader推荐:
- babel-loader:处理JS文件,把ES6+转换成ES5。必备。
- css-loader + style-loader / mini-css-extract-plugin:处理CSS文件。开发环境用style-loader,把CSS注入到JS里;生产环境用mini-css-extract-plugin,把CSS提取成单独的文件。
- postcss-loader:处理CSS的后处理器,可以自动加浏览器前缀、压缩CSS等。配合autoprefixer使用,自动加浏览器前缀。
- sass-loader / less-loader:处理Sass/Less预处理器。
- url-loader / file-loader:处理图片、字体等静态资源。url-loader可以把小图片转成base64,减少请求数;大图片用file-loader,单独打包成文件。
- eslint-loader:在构建的时候检查代码规范,提前发现问题。
- ts-loader:处理TypeScript文件。
plugin推荐:
- html-webpack-plugin:自动生成HTML文件,把打包后的JS和CSS自动注入到HTML里。必备。
- mini-css-extract-plugin:把CSS提取成单独的文件。生产环境必备。
- optimize-css-assets-webpack-plugin:压缩CSS文件。生产环境用。
- terser-webpack-plugin:压缩JS文件。Webpack 4生产环境默认启用,可以自定义配置。
- clean-webpack-plugin:每次构建前清理dist目录,避免旧文件残留。
- copy-webpack-plugin:把静态资源(比如favicon、robots.txt等)复制到dist目录。
- webpack-bundle-analyzer:可视化分析bundle的体积和组成,帮助你发现可以优化的地方。
- webpack-merge:合并多个Webpack配置文件,方便把配置拆分成common、dev、prod。
- case-sensitive-paths-webpack-plugin:强制模块路径大小写敏感,避免在Windows上开发、Linux上部署时出现大小写问题。
- friendly-errors-webpack-plugin:让构建的错误信息更友好、更易读。
八、写在最后
以上,就是我整理的Webpack 4配置的一些进阶技巧。这些技巧,有的是Webpack 4的新特性,有的是最佳实践,有的是性能优化的经验。掌握了这些技巧,能让你的Webpack配置更专业、更高效,打包出来的文件更小、更快,构建速度也能提升不少。
当然,Webpack的配置是一个很灵活的东西,没有标准答案。不同的项目,有不同的需求,配置也不一样。上面的技巧,只是给你一个参考,你需要根据自己的项目情况,选择合适的配置,不断调整和优化。
而且,Webpack还在不断发展,新的特性和优化还在不断出现。要保持学习,关注Webpack的更新和社区的最佳实践,不断提升自己的配置能力。
前端工程化,是一个很大的话题,Webpack只是其中的一部分。但是,把Webpack配置好,能让你的前端开发体验和项目性能提升一个档次。所以,花点时间研究Webpack的配置,是值得的。
最后,用一句话来总结这篇文章:"Webpack配置,入门容易,精通难。但是,只要你愿意花时间去研究,去实践,去优化,你就能让Webpack成为你前端开发的利器,而不是绊脚石。"
愿每一个前端开发者,都能配置出高效、专业的Webpack,让前端开发更愉快,让用户体验更流畅。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录