“手把手” 优化 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