“手把手” 优化 Webpack 配置 (一)
“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情”
1、缩小范围
extensions
指定extensions之后在使用export和require的时候,就不用加扩展名,会从extensions配置中依次进行匹配的。
resolve: {
extensions: [".js",".jsx",".json",".css"]
}
alias
配置别名,可以加快webpack查找的速度
- 每当引入bootstrap模块的时候,它会直接引入
bootstrap,而不需要从node_modules文件夹中按模块的查找规则查找
const bootstrap = path.resolve(__dirname,'node_modules/bootstrap/dist/css/bootstrap.css')
resolve: {
// 配置模块别名
alias: {
bootstrap
},
},
mudules
作用: 比如在项目中直接使用import react from 'react' ,这种的其路径查找规则,就是在这里可以进行配置。
如果可以确定项目内所有的第三方依赖模块都是在项目根目录下的 node_modules ,则可以如下配置。
resolve: {
modules: [path.resolve(__dirname, 'node_modules')],
}
如果有其他路径下的模块,也可以直接加入到数组中来
resolve: {
modules: [path.resolve(__dirname, 'xxxx')],
}
mainFields
配置package.json中的文件入口字段。
默认情况下package.json 文件则按照文件中 main 字段的文件名来查找文件
mainFields: ['xxx', 'main'],
mainFiles
作用:当前目录下面没有package.json的时候,会默认加载的文件,可以用这个配置进行修改。
resolve: {
mainFiles: ['index'], // 你可以添加其他默认使用的文件名
}
resolveLoader
resolve.resolveLoader用于配置解析 loader 时的 resolve 配置,默认的配置:
resolveLoader: {
modules: ['node_modules'],
extensions: ['.js', '.json'],
mainFields: ['loader', 'main']
},
2、noParse
作用:module.noParse可以配置那些模块文件不需要进行解析
既,没有依赖的第三方库,可以配置这个字段来提高构建速度。例如下面的lodash。

module: {
noParse: /jquery|lodash/
},
使用 noParse 进行忽略的模块文件中不能使用 import、require、define 等导入机制
3、ignorePlugin
作用:ignorePlugin用于忽略某些特定的模块,让 webpack 不把这些指定的模块打包进去。
这里以moment这个库为例,
index.js
import moment from 'moment';
import 'moment/locale/zh-cn' // 添加忽略配置之后,需要单独引入中文语言包
console.log(moment().format('MMMM Do YYYY, h:mm:ss a'));
plugins: [
new webpack.IgnorePlugin({
contextRegExp: /moment$/, // 模块名
resourceRegExp: /^\.\/locale/ // 模块下面的目录
}),
]
之前:

之后:

4、费时分析
作用:可查看webpack构建过程中的耗时情况。

const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');
const smw = new SpeedMeasureWebpackPlugin();
module.exports = smw.wrap({
// webpack 配置信息
});
5、webpack-bundle-analyzer

yarn add webpack-bundle-analyzer -D
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
6、webpack 打包库
webpack不仅仅可以用来打包项目,还可以用来打包类库。
output.library配置导出库的名称output.libraryExport配置要导出的模块中哪些子模块需要被导出。 它只有在 output.libraryTarget 被设置成 commonjs 或者 commonjs2 时使用才有意义output.libraryTarget配置以何种方式导出库,是字符串的枚举类型,支持以下配置
| libraryTarget | 使用者的引入方式 | 使用者提供给被使用者的模块的方式 |
|---|---|---|
| var | 只能以script标签的形式引入我们的库 | 只能以全局变量的形式提供这些被依赖的模块 |
| commonjs | 只能按照commonjs的规范引入我们的库 | 被依赖模块需要按照commonjs规范引入 |
| commonjs2 | 只能按照commonjs2的规范引入我们的库 | 被依赖模块需要按照commonjs2规范引入 |
| amd | 只能按amd规范引入 | 被依赖的模块需要按照amd规范引入 |
| this | ||
| window | ||
| global | ||
| umd | 可以用script、commonjs、amd引入 | 按对应的方式引入 |
node中的this是当前模块的导出对象module.exports 也等于exports
假设现在我们要使用webpack打包一个库, 采用var的形式。
vue.js
module.exports = {
ref() {
console.log('ref')
},
reactive() {
console.log('reactive')
},
}
配置文件
entry: {
main: './src/vue.js',
},
output: {
path: path.resolve('dist'),
filename: '[name].js',
library: 'Vue', // 导出库的名字
libraryTarget: 'var' // 相当于,全局声明一个变量 calculator
},
打包效果如下

再演示一下采用commonjs的形式进行打包
更改配置
output: {
// .....
libraryTarget: 'commonjs' // 相当于,全局声明一个变量 calculator
},
在dist目录下面新建test.js
const main = require('./main')
main.Vue.ref()
执行效果

7、提取css
yarn add css-loader mini-css-extract-plugin -D
index.js
import moment from 'moment';
import 'moment/locale/zh-cn'
import './a.css'
import './b.css'
a.css
#app {
background: red;
}
b.css
#app {
background: red;
}
webpack.config.js
const miniCssExtractPlugin = require('mini-css-extract-plugin')
output: {
path: path.resolve('dist'),
filename: '[name].js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.css$/,
use: [miniCssExtractPlugin.loader, 'css-loader']
}
]
},
plugins: [
new miniCssExtractPlugin({
filename: '[name].css'
})
]

plugins: [
new miniCssExtractPlugin({
filename: 'css/[name].css' // 这样可以指定目录
})
]
如下:

8、压缩 Js Css Html
css
yarn add -D css-minimizer-webpack-plugin
还是刚才的那两个css文件
a.css
#app {
background: red;
}
b.css
#app {
background: red;
}
.logo {
color: pink;
}
webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /.s?css$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
},
],
},
optimization: {
minimizer: [
// For webpack@5 you can use the `...` syntax to extend existing minimizers (i.e. `terser-webpack-plugin`), uncomment the next line
// `...`,
new CssMinimizerPlugin(),
],
},
plugins: [new MiniCssExtractPlugin()],
};

js
- terser-webpack-plugin是一个优化和压缩JS资源的插件
yarn add -D terser-webpack-plugin
webpack.config.js
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
plugins: [
new htmlWebpackPlugin({
template: './index.html',
minify: {
collapseWhitespace: true,
removeComments: true
}
})
]
};
9、CDN
- qiniu
- CDN 又叫内容分发网络,通过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源,从而加速资源的获取速度。
- public-path
- external-remotes-plugin

缓存策略
HTML文件不缓存,放在自己的服务器上,关闭自己服务器的缓存,静态资源的URL变成指向CDN服务器的地址- 静态的
JavaScript、CSS、图片等文件开启CDN和缓存,并且文件名带上HASH值 (也就是webpack打包产生的hash值) - 为了并行加载不阻塞,把不同的静态资源分配到不同的
CDN服务器上
域名限制
- 同一时刻针对同一个域名的资源并行请求是有限制
- 可以把这些静态资源分散到不同的
CDN服务上去 - 多个域名后会增加域名解析时间
- 可以通过在
HTML HEAD标签中 加入<link rel="dns-prefetch" href="http://img.baidu.cn">去预解析域名,以降低域名解析带来的延迟
文件指纹
- 打包后输出的
文件名和后缀 hash一般是结合CDN缓存来使用,通过webpack构建之后,生成对应文件名自动带上对应的MD5值。如果文件内容改变的话,那么对应文件哈希值也会改变,对应的HTML引用的URL地址也会改变,触发CDN服务器从源服务器上拉取对应数据,进而更新本地缓存。
指纹占位符
| 占位符名称 | 含义 |
|---|---|
| ext | 资源后缀名 |
| name | 文件名称 |
| path | 文件的相对路径 |
| folder | 文件所在的文件夹 |
| hash | 每次webpack构建时生成一个唯一的hash值(常用) |
| chunkhash | 根据chunk生成hash值,来源于同一个chunk,则hash值就一样(常用) |
| contenthash | 根据内容生成hash值,文件内容相同hash值就相同(常用) |
hash
hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值
webpack.config.js
output: {
path: path.resolve('dist'),
filename: '[name].[hash].js',
publicPath: '/'
},
plugins: [
new miniCssExtractPlugin({
filename: 'css/[name].[hash].css'
})
]
打包结果如下:

chunkhash
采用hash计算的话,每一次构建后生成的哈希值都不一样,如果文件内容没有任何变化(则hash值也是不会变的,但是一般重新打包项目下总是会有文件内容发生改变的)。这样子是没办法实现缓存效果,我们需要换另一种哈希值计算方式,即chunkhash。
chunkhash和hash不一样,它根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。我们在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响。
index.js
import moment from 'moment';
import 'moment/locale/zh-cn'
import './a.css'
import './b.css'
console.log(moment().format('MMMM Do YYYY, h:mm:ss a'));
a.css
#app {
background: red;
}
b.css
#app {
background: green;
}
.logo {
color: pink;
}
webpack.config.js
mode: "production",
entry: {
main: './src/index.js',
vender: ['lodash']
},
output: {
path: path.resolve('dist'),
filename: '[name].[chunkhash].js',
publicPath: '/'
},
plugins: [
new miniCssExtractPlugin({
filename: 'css/[name].[chunkhash].css'
})
]
js产出了两个文件,css一个 ,因为css是在入口文件中引入的。
vender被单独打成了一个文件

我们修改入口文件index.js, 之后重新打包再进行观察。
公共库的hash并没有变只是css和js文件的hash发生了变化。这样我们就看出了前两种hash之间的区别。

contenthash
在chunkhash的例子,我们可以看到由于a.css被index.js引用了,所以共用相同的chunkhash值。但是这样子有个问题,如果index.js更改了代码,css文件就算内容没有任何改变,由于是该模块发生了改变,导致css文件会重复构建。缓也会失效。
这个时候,我们可以使用extra-text-webpack-plugin里的contenthash值,保证即使css文件所处的模块里就算其他文件内容改变,只要css文件内容不变,那么不会重复构建。
webpack.config.js
new miniCssExtractPlugin({
filename: 'css/[name].[contenthash].css'
})
结果如下
现在我们尝试修改index.js文件
index.js
import moment from 'moment';
import 'moment/locale/zh-cn'
import './a.css'
import './b.css'
console.log('修改 index.js文件啦啦啦啦啦啦啦') //更改
console.log(moment().format('MMMM Do YYYY, h:mm:ss a'));
发现只有main.js文件的hash值变了。

理解各种hash

这里借用crypto这个库
模拟hash模式
function createHash() {
return require('crypto').createHash('md5');
}
let entry1 = 'require depModule1';//模块entry1
let entry2 = 'require depModule2';//模块entry2
let depModule1 = 'depModule1';//模块depModule1
let depModule2 = 'depModule2';//模块depModule2
//如果都使用hash的话,因为这是工程级别的,即每次修改任何一个文件,所有文件名的hash至都将改变。
//所以一旦修改了任何一个文件,整个项目的文件缓存都将失效
let hash = createHash()
.update(entry1)
.update(entry2)
.update(depModule1)
.update(depModule2)
.digest('hex');
console.log('hash', hash)
结果如下,没改变内容,多次打包hash值是不变的。
假设改变其中的一个文件内容, 最终的hash值会改变。
let entry1 = 'require depModule1---------';//模块entry1
let entry2 = 'require depModule2';//模块entry2
let depModule1 = 'depModule1';//模块depModule1
let depModule2 = 'depModule2';//模块depModule2

chunkhash, 假设模块2 是公共库的入口。
let entry1 = 'require depModule1';//模块entry1
let entry2 = 'require depModule2';//模块entry2 假设模块2 是公共库的入口
let depModule1 = 'depModule1';//模块depModule1
let depModule2 = 'depModule2';//模块depModule2
//chunkhash根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。
//在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,
//接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响
let entry1ChunkHash = createHash()
.update(entry1)
.update(depModule1).digest('hex');;
console.log('entry1ChunkHash', entry1ChunkHash);
let entry2ChunkHash = createHash()
.update(entry2)
.update(depModule2).digest('hex');;
console.log('entry2ChunkHash', entry2ChunkHash);

下面我们修改entry1里面的内容
let entry1 = 'require depModule1%%%%%%%%%%%%%%';//模块entry1 // 改变
let entry2 = 'require depModule2';//模块entry2 假设模块2 是公共库的入口
let depModule1 = 'depModule1';//模块depModule1
let depModule2 = 'depModule2';//模块depModule2
可以看出entry2ChunkHash的值并没有发生变化。

contenthash 实验
let entry1File = entry1 + depModule1;
let entry1ContentHash = createHash()
.update(entry1File).digest('hex');;
console.log('entry1ContentHash', entry1ContentHash);
let entry2File = entry2 + depModule2;
let entry2ContentHash = createHash()
.update(entry2File).digest('hex');;
console.log('entry2ContentHash', entry2ContentHash);

改变entry1里面的内容之后也是只影响entry1ContentHash的结果。
最后在假设这两个entry里面的内容都是一样的,会输出怎样的结果呢?
let entry1 = 'require';//模块entry1
let entry2 = 'require';//模块entry2 假设模块2 是公共库的入口
let depModule1 = 'depModule';//模块depModule1
let depModule2 = 'depModule';//模块depModule2

10、moduleIds & chunkIds的优化
- module: 每一个文件其实都可以看成一个
module - chunk: webpack打包最终生成的代码块,代码块会生成文件,
一个文件对应一个chunk - 在webpack5之前,没有从
entry打包的chunk文件,都会以1、2、3...的文件命名方式输出,删除某些些文件可能会导致缓存失效 - 在生产模式下,默认启用这些功能
chunkIds: "deterministic",moduleIds: "deterministic",此算法采用确定性的方式将短数字 ID(3 或 4 个字符)短hash值分配给 modules 和 chunks - chunkId设置为
deterministic,则output中chunkFilename里的[name]会被替换成确定性短数字ID - 虽然chunkId不变(不管值是
deterministic|natural|named),但更改chunk内容,chunkhash还是会改变的
| 可选值 | 含义 | 示例 |
|---|---|---|
| natural | 按使用顺序的数字ID | 1 |
| named | 方便调试的高可读性id | src_two_js.js |
| deterministic | 根据模块名称生成简短的hash值 | 915 |
| size | 根据模块大小生成的数字id | 0 |
index.js
import moment from 'moment';
import 'moment/locale/zh-cn'
import './a.css'
import './b.css'
console.log('修改 index.js文件啦啦啦啦啦啦啦')
console.log(moment().format('MMMM Do YYYY, h:mm:ss a'));
import('./a')
import('./b')
import('./c')
a.js
console.log('a')
b.js
console.log('b')
c.js
console.log('c')
webpack.config.js
optimization: {
moduleIds: 'natural',
chunkIds: 'natural'
},
删除前

在上面的基础上我们删除import('./b') 之后在进行打包。
import('./a')
// import('./b')
import('./c')
删除后

看结果,这样就会让原来4.05eac48c15ada5d0fcc0.js文件的缓存失效了,本来只是减少了一个chunk,但是因为少了一个chunk,所以会使最终的文件名发生变化。
我们再继续将其设置成deterministic,还是进行上面的两个操作,并进行对比
webpack.config.js
optimization: {
moduleIds: 'deterministic',
chunkIds: 'deterministic'
},
删除前
删除后

这样即使删除了一个chunk但是它不会让原来的chunk名字发生改变,所以使用这种配置,会提高缓存的命中率。

转载自:https://juejin.cn/post/7140663872347865095