1、静态资源首次被加载后浏览器会进行缓存,同一个资源在缓存未过期情况下一般不会再去请求,那么当资源有更新时如何通知浏览器资源有变化呢?资源文件命名 hash
化就是解决该问题而生;
2、对于浏览器来说,一方面期望每次请求页面资源时,获得的都是最新的资源;一方面期望在资源没有发生变化时,能够复用缓存对象。
这个时候,使用【文件名+文件哈希值】的方式,就可以实现只要通过文件名,就可以区分资源是否有更新。
1、webpack 就内置了 hash
计算方法,对生成的文件可以在输出的文件中添加 hash
字段。
2、webpack 内置的 hash
有三种:
hash
:每次构建会生成一个 hash
。和整个项目有关,只要项目有文件更改,就会改变 hash
。contenthash
:和单个文件的内容有关。指定文件的内容发生改变,就会改变 hash
。chunkhash
:和 webpack 打包生成的 chunk
相关。每一个 entry
,都会有不用的 hash
。const path = require('path')
module.exports = {
entry: {
app: path.join(__dirname, 'src/foo.js')
},
output: {
filename: '[name].[chunkhash].js',
path: path.join(__dirname, 'dist')
}
}
src/foo.js
内容如下:
import React from 'react'
console.log(React.toString())
注意这里的 output.filename
你也可以用 [hash]
而不是 [chunkhash]
,但是这两种生成的 hash
码是不一样的。
使用 hash
如下:
app.03700a98484e0f02c914.js 70.4 kB 0 [emitted] app
[6] ./src/foo.js 55 bytes {0} [built]
+ 11 hidden modules
使用 chunkhash
如下:
app.f2f78b37e74027320ebf.js 70.4 kB 0 [emitted] app
[6] ./src/foo.js 55 bytes {0} [built]
+ 11 hidden modules
对于单个 entry
来说用哪个都没有问题,做例子期间使用的是 webpack@3.8.1
版本,这个版本 webpack
对于源码没有改动的情况,已经修复了 hash
串会变的问题。但是在之前的版本有可能会出现对于同一份没有修改的代码进行修改,hash
不一致的问题,所以不管你使用的版本会不会有问题,都建议使用接下去的配置。之后的配置都是用 chunkhash
作为 hash
生成。
因为 webpack
要处理不同模块的依赖关系,所以他内置了一个 js
模板用来处理依赖关系(后面称为 runtime
),这段 js
因此也会被打包到我们最后 bundle
里面。在实际项目中我们常常需要将这部分代码分离出来,比如我们要把类库分开打包的情况,如果不单独给runtime
单独生成一个 js
,那么他会和类库一起打包,而这部分代码会随着业务代码改变而改变,导致类库的 hash
也每次都改变,那么我们分离出类库就没有意义了。所以这里我们需要给 runtime
单独提供一个 js
。
修改配置如下:
module.exports = {
entry: {
app: path.join(__dirname, 'src/foo.js')
},
output: {
filename: '[name].[chunkhash].js',
path: path.join(__dirname, 'dist')
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
]
}
webpack
的文档中说明,如果给 webpack.optimize.CommonsChunkPlugin
的 name
指定一个在entry
中没有声明的名字,那么他会把 runtime
代码打包到这个文件中,所以你这里可以任意指定你喜欢的 name
那么现在打包出来会是神马样的呢?
app.aed80e077eb0a6c42e65.js 68 kB 0 [emitted] app
runtime.ead626e4060b3a0ecb1f.js 5.82 kB 1 [emitted] runtime
[6] ./src/foo.js 55 bytes {0} [built]
+ 11 hidden modules
我们可以看到,app
和 runtime
的 hash
是不一样的。那么如果我们使用 hash
而不是chunkhash
呢?
app.357eff03ae011d688ac3.js 68 kB 0 [emitted] app
runtime.357eff03ae011d688ac3.js 5.81 kB 1 [emitted] runtime
[6] ./src/foo.js 55 bytes {0} [built]
+ 11 hidden modules
从这里就可以看出 hash
和 chunkhash
的区别了,chunkhash
会包含每个 chunk
的区别(chunk
可以理解为每个 entry
),而 hash
则是所有打包出来的文件都是一样的,所以一旦你的打包输出有多个文件,你势必需要使用 chunkhash
。
在一般的项目中,我们的类库文件都不会经常更新,比如 react
,更多的时候我们更新的是业务代码。那么我们肯定希望类库代码能够尽可能长的在浏览器进行缓存,这就需要我们单独给类库文件打包了,怎么做呢?
修改配置文件:
module.exports = {
entry: {
app: path.join(__dirname, 'src/foo.js'),
vendor: ['react'] // 所有类库都可以在这里声明
},
output: {
filename: '[name].[chunkhash].js',
path: path.join(__dirname, 'dist')
},
plugins: [
// 单独打包,app中就不会出现类库代码
// 必须放在runtime之前
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
]
}
然后,我们来执行以下打包:
vendor.72d208b8e74b753cf09c.js 67.7 kB 0 [emitted] vendor
app.fdc2c0fe8694c1690cb3.js 494 bytes 1 [emitted] app
runtime.035d95805255d39272ba.js 5.85 kB 2 [emitted] runtime
[7] ./src/foo.js 55 bytes {1} [built]
[12] multi react 28 bytes {0} [built]
+ 11 hidden modules
vendor
和 app
分开了,而且 hash
都不一样,看上去很美好是不是?高兴太早了年轻人。我们再新建一个文件,叫 bar.js
,代码如下:
import React from 'react'
export default function() {
console.log(React.toString())
}
然后修改 foo.js
如下:
import bar from './bar.js'
console.log(bar())
从这个修改中可以看出,我们并没有修改类库相关的内容,我们的 vendor
中应该依然只有 react
,那么 vendor
的 hash
应该是不会变的,那么结果如我们所愿吗?
vendor.424ef301d6c78a447180.js 67.7 kB 0 [emitted] vendor
app.0dfe0411d4a47ce89c61.js 845 bytes 1 [emitted] app
runtime.e90ad557ba577934a75f.js 5.85 kB 2 [emitted] runtime
[7] ./src/foo.js 45 bytes {1} [built]
[8] ./src/bar.js 88 bytes {1} [built]
[13] multi react 28 bytes {0} [built]
+ 11 hidden modules
很遗憾,webpack
狠狠打了我们的脸╮(╯_╰)╭
这是什么原因呢?这是因为我们多加入了一个文件,对于 webpack
来说就是多了一个模块,默认情况下 webpack
的模块都是以一个有序数列命名的,也就是 [0,1,2....]
,我们中途加了一个模块导致每个模块的顺序变了,vendor
里面的模块的模块 id
变了,所以 hash
也就变了。总结一下:
1、app
变化是因为内容发生了变化
2、vendor
变化时因为他的 module.id
发生了变化
3、runtime
变化时因为它本身就是维护模块依赖关系的
那么怎么解决呢?
这两个 plugin
让 webpack
不再使用数字给我们的模块进行命名,这样每个模块都会有一个独有的名字,也就不会出现增删模块导致模块 id
变化引起最终的 hash
变化了。如何使用?
{
plugins: [
new webpack.NamedModulesPlugin(),
// new webpack.HashedModuleIdsPlugin(),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
]
}
NamedModulePlugin
一般用在开发时,能让我们看到模块的名字,可读性更高,但是性能相对较差。HashedModuleIdsPlugin
更建议在正式环境中使用。
我们来看一下使用这个插件后,两次打包的结果
代码修改前:
vendor.91148d0e2f4041ef2280.js 69 kB 0 [emitted] vendor
app.0228a43edf0a32a59426.js 551 bytes 1 [emitted] app
runtime.8ed369e8c4ff541ad301.js 5.85 kB 2 [emitted] runtime
[./src/foo.js] ./src/foo.js 56 bytes {1} [built]
[0] multi react 28 bytes {0} [built]
+ 11 hidden modules
代码修改后:
vendor.91148d0e2f4041ef2280.js 69 kB 0 [emitted] vendor
app.f64e232e4b6d6a59e617.js 917 bytes 1 [emitted] app
runtime.c12d50e9a1902f12a9f4.js 5.85 kB 2 [emitted] runtime
[./src/bar.js] ./src/bar.js 88 bytes {1} [built]
[0] multi react 28 bytes {0} [built]
[./src/foo.js] ./src/foo.js 43 bytes {1} [built]
+ 11 hidden modules
可以看到 vendor
的 hash
没有变化,HashedModuleIdsPlugin
也是一样的效果。
随着我们的系统变得越来越大,模块变得很多,如果所有模块一次性打包到一起,那么首次加载就会变得很慢。这时候我们会考虑做异步加载,webpack
原生支持异步加载,用起来很方便。
我们再创建一个 js
叫做 async-bar.js
,在 foo.js
中:
import('./async-bar').then(a => console.log(a))
打包:
0.1415eebc42d74a3dc01d.js 131 bytes 0 [emitted]
vendor.19a637337ab59d16fb34.js 69 kB 1 [emitted] vendor
app.f7e5ecde27458097680e.js 1.04 kB 2 [emitted] app
runtime.c4caa7f9859faa94b02e.js 5.88 kB 3 [emitted] runtime
[./src/async-bar.js] ./src/async-bar.js 32 bytes {0} [built]
[./src/bar.js] ./src/bar.js 88 bytes {2} [built]
[0] multi react 28 bytes {1} [built]
[./src/foo.js] ./src/foo.js 92 bytes {2} [built]
+ 11 hidden modules
恩,这时候我们已经看到,我们的 vendor
变了,但是更可怕的还在后头,我们再建了一个模块叫 async-baz.js
,一样的在 foo.js
引用:
import('./async-baz').then(a => console.log(a))
然后再打包:
0.eb2218a5fc67e9cc73e4.js 131 bytes 0 [emitted]
1.61c2f5620a41b50b31eb.js 131 bytes 1 [emitted]
vendor.1eada47dd979599cc3e5.js 69 kB 2 [emitted] vendor
app.1f82033832b8a5dd6e3b.js 1.17 kB 3 [emitted] app
runtime.615d429d080c11c1979f.js 5.9 kB 4 [emitted] runtime
[./src/async-bar.js] ./src/async-bar.js 32 bytes {1} [built]
[./src/async-baz.js] ./src/async-baz.js 32 bytes {0} [built]
[./src/bar.js] ./src/bar.js 88 bytes {3} [built]
[0] multi react 28 bytes {2} [built]
[./src/foo.js] ./src/foo.js 140 bytes {3} [built]
+ 11 hidden modules
每个模块的 hash
都变了。。。
为啥模块又变成数字 ID
了啊?!!
好吧,言归正传,决绝办法还是有的,那就是 NamedChunksPlugin
,之前是用来处理每个 chunk
名字的,似乎在最新的版本中不需要这个也能正常打包普通模块的名字。但是这里我们可以用来处理异步模块的名字,在 webpack
的 plugins
中加入如下代码:
new webpack.NamedChunksPlugin((chunk) => {
if (chunk.name) {
return chunk.name;
}
return chunk.mapModules(m => path.relative(m.context, m.request)).join("_");
})
再执行打包,两次结果如下:
app.5faeebb6da84bedaac0a.js 1.11 kB app [emitted] app
async-bar.js.457b1711c7e8c6b6914c.js 144 bytes async-bar.js [emitted]
runtime.f263e4cd58ad7b17a4bf.js 5.9 kB runtime [emitted] runtime
vendor.05493d3691191b049e65.js 69 kB vendor [emitted] vendor
[./src/async-bar.js] ./src/async-bar.js 32 bytes {async-bar.js} [built]
[./src/bar.js] ./src/bar.js 88 bytes {app} [built]
[0] multi react 28 bytes {vendor} [built]
[./src/foo.js] ./src/foo.js 143 bytes {app} [built]
+ 11 hidden modules
app.55e3f40adacf95864a96.js 1.2 kB app [emitted] app
async-bar.js.457b1711c7e8c6b6914c.js 144 bytes async-bar.js [emitted]
async-baz.js.a85440cf862a8ad3a984.js 144 bytes async-baz.js [emitted]
runtime.deeb657e46f5f7c0da42.js 5.94 kB runtime [emitted] runtime
vendor.05493d3691191b049e65.js 69 kB vendor [emitted] vendor
[./src/async-bar.js] ./src/async-bar.js 32 bytes {async-bar.js} [built]
[./src/async-baz.js] ./src/async-baz.js 32 bytes {async-baz.js} [built]
[./src/bar.js] ./src/bar.js 88 bytes {app} [built]
[0] multi react 28 bytes {vendor} [built]
[./src/foo.js] ./src/foo.js 140 bytes {app} [built]
+ 11 hidden modules
可以看到结果都是用名字而不是 id
了,而且不改改变的地方也都没有改变。
注意生成
chunk
名字的逻辑代码你可以根据自己的需求去改
使用上面的方式会有一些问题,比如使用 .vue
文件开发模式,m.request
是一大串 vue-loader
生成的代码,所以打包会报错。当然大家可以自己找对应的命名方式,在这里我推荐一个 webpack
原生支持的方式,在使用 import
的时候,写如下注释:
import(/* webpackChunkName: "views-home" */ '../views/Home')
然后配置文件只要使用 new NamedChunksPlugin()
就可以了,不需要自己再拼写名字,因为这个时候我们的异步 chunk
已经有名字了。
修改 webpack.config.js
:
{
...
entry: {
app: path.join(__dirname, 'src/foo.js'),
vendor: ['react'],
two: path.join(__dirname, 'src/foo-two.js')
},
...
}
增加的 enrty
如下:
// foo-two.js
import bar from './bar.js'
console.log(bar)
import('./async-bar').then(a => console.log(a))
// import('./async-baz').then(a => console.log(a))
是的跟 foo.js
一模一样,当然你可以改逻辑,只需要记得引用 bar.js
就可以。
然后我们打包,结果。。。
app.77b13a56bbc0579ca35c.js 612 bytes app [emitted] app
async-bar.js.457b1711c7e8c6b6914c.js 144 bytes async-bar.js [emitted]
runtime.bbe8e813f5e886e7134a.js 5.93 kB runtime [emitted] runtime
two.9e4ce5a54b4f73b2ed60.js 620 bytes two [emitted] two
vendor.8ad1e07bfa18dd78ad0f.js 69.5 kB vendor [emitted] vendor
[./src/async-bar.js] ./src/async-bar.js 32 bytes {async-bar.js} [built]
[./src/bar.js] ./src/bar.js 88 bytes {vendor} [built]
[0] multi react 28 bytes {vendor} [built]
[./src/foo-two.js] ./src/foo-two.js 143 bytes {two} [built]
[./src/foo.js] ./src/foo.js 143 bytes {app} [built]
+ 11 hidden modules
怎么所有文件的 hash
都变化了啊?!!!
好吧,原因是 vendor
作为 common chunk
并不只是包含我们在 entry
中声明的部分,他还会包含每个 entry
中引用的公共代码,有些时候你可能希望这样的结果,但在我们这里,这就是我要解决的一个问题啊ლ(゚д゚ლ)
所以这里怎么做呢,在 CommonsChunkPlugin
里面有一个参数,可以用来告诉 webpack
我们的 vendor
真的只想包含我们声明的内容:
{
plugins: [
...
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: Infinity
}),
]
}
这个参数的意思是尽可能少的把公用代码包含到 vendor
里面。于是我们又打包:
app.5faeebb6da84bedaac0a.js 1.13 kB app [emitted] app
async-bar.js.457b1711c7e8c6b6914c.js 144 bytes async-bar.js [emitted]
runtime.b0406822caa4d1898cb8.js 5.93 kB runtime [emitted] runtime
two.9be2d4a28265bfc9d947.js 1.13 kB two [emitted] two
vendor.05493d3691191b049e65.js 69 kB vendor [emitted] vendor
[./src/async-bar.js] ./src/async-bar.js 32 bytes {async-bar.js} [built]
[./src/bar.js] ./src/bar.js 88 bytes {app} {two} [built]
[0] multi react 28 bytes {vendor} [built]
[./src/foo-two.js] ./src/foo-two.js 143 bytes {two} [built]
[./src/foo.js] ./src/foo.js 143 bytes {app} [built]
+ 11 hidden modules
恩,熟悉的味道。
到这里我们跟 webpack
的 hash
变化之战算是告一段落,大部分 webpack
打包出现问题的原因是模块命名的问题,所以解决办法其实也就是给每个模块一个固定的名字。
最后我们的配置如下:
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
app: path.join(__dirname, 'src/foo.js'),
vendor: ['react'],
two: path.join(__dirname, 'src/foo-two.js')
},
externals: {
jquery: 'jQuery'
},
output: {
filename: '[name].[chunkhash].js',
path: path.join(__dirname, 'dist')
},
plugins: [
new webpack.NamedChunksPlugin((chunk) => {
if (chunk.name) {
return chunk.name;
}
return chunk.mapModules(m => path.relative(m.context, m.request)).join("_");
}),
new webpack.NamedModulesPlugin(),
// new webpack.HashedModuleIdsPlugin(),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: Infinity
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
]
}
webpack
的 hash
是通过 crypto
加密和哈希算法实现的,webpack
提供了 hashDigest
(在生成 hash
时使用的编码方式,默认为 'hex'
)、hashDigestLength
(散列摘要的前缀长度,默认为 20
)、hashFunction
(散列算法,默认为 'md5'
)、hashSalt
(一个可选的加盐值)等参数来实现自定义hash;
相信大家如果听说过 webpack4
的更新,最大的感触应该就是去除了 CommonsChunkPlugin
,毕竟官方 change log
写的篇幅最多的就是这个。
CommonsChunkPlugin
删除之后,改成使用 optimization.splitChunks
进行模块划分,有兴趣的可以去看以下官方的详细文档。
官方的说法是默认设置已经对大部分用户来说非常棒了,但是需要注意一个问题,默认配置只会对异步请求的模块进行提取拆分,如果要对 entry
进行拆分,需要设置optimization.splitChunks.chunks = 'all'
。其他的内容大家就自己研究吧。
对应之前我们拆分 runtime
的情况,现在也有一个配置 optimization.runtimeChunk
,设置为 true
就会自动拆分 runtime
文件。
现在也不需要使用这个 plugin
了,只需要使用 optimization.minimize
为 true
就行,production mode
下面自动为 true
。
optimization.minimizer
可以配置你自己的压缩程序。
—————————— 【正文完】——————————
前端学习交流群,想进来面基的,可以加群: 832485817,685486827;
写在最后: 约定优于配置 —— 软件开发的简约原则
——————————【完】——————————
我的:
个人网站: https://neveryu.github.io/neveryu/
Github: https://github.com/Neveryu
新浪微博: https://weibo.com/Neveryu
微信: miracle421354532
更多学习资源请关注我的新浪微博…好吗