近期发现webpack在多台机器上打包同一份代码生成的hash不一样,查看社区没有文章深入说明hash生成策略 ,所以把webpack源码撸了一遍,定位到是hash生成时包含有项目的绝对路径导致,最后编写一个webpack插件解决了该问题,本文主要讲解hash的用法和原理以及如何解决多机器hash不一致等坑。
webpack的hash策略
前端同学众所周知静态资源首次被加载后浏览器会进行缓存,同一个资源在缓存未过期情况下一般不会再去请求,那么当资源有更新时如何通知浏览器资源有变化呢?资源文件命名hash化就是解决该问题而生;webpack是现在前端的主流构建工具,所以本文主要是讲述webpack构建后文件名的hash策略;webpack分为hash、chunkhash、contenthash这三种hash,下面我们依次讲述一下三种hash的使用和原理。
hash
使用webpack构建时hash是使用最多的一种,webpack构建后整个项目的js和css输出文件的hash都相同;例如一个项目有6个组件,需要把组件1、2、3作为代码块(chunk)输出一组js和css文件,组件4、5作为代码块(chunk)输出一组js和css文件,webpack如下配置:
output: {
path: path.resolve(__dirname, OUTPUT_PATH),
filename: '[name].[hash].js',// 使用hash
publicPath: '/dist/webpack/'
}
通过webpack构建完后输出的第一组js、css文件的hash相同,并且第二组和第一组的hash也相同,下图是hash在项目中的效果:
所以只要某一个文件被修改,所有输出文件的hash都会跟着变化;因此它有一个弊端,一旦修改了某一个文件,整个项目的文件缓存都会失效。
chunkhash
chunkhash相对hash影响范围比较小,使用chunkhash时,每一个代码块(chunk)输出文件对应一个hash,某源文件被修改后,只有该源文件所在代码块(chunk)的输出文件的hash会变化;例如一个项目有6个组件,需要把组件1、2、3作为代码块(chunk)输出一组js和css文件,组件4、5作为代码块(chunk)输出一组js和css文件,webpack如下配置:
output: {
path: path.resolve(__dirname, OUTPUT_PATH),
filename: '[name].[chunkhash].js', // 使用chunkhash
publicPath: '/dist/webpack/'
}
通过webpack打包构建完后输出的两组hash不同,但是每一组内部js和css的hash相同,下图是chunkhash在项目中的效果:
contenthash
当使用mini-css-extract-plugin
插件时还可以使用contenthash来获取文件的hash,contenthash相对于chunkhash影响范围更小;每一个代码块(chunk)中的js和css输出文件都会独立生成一个hash,当某一个代码块(chunk)中的js源文件被修改时,只有该代码块(chunk)输出的js文件的hash会发生变化;例如一个项目有6个组件,需要把组件1、2、3作为代码块(chunk)输出一组js和css文件,组件4、5作为代码块(chunk)输出一组js和css文件,webpack如下配置:
output: {
path: path.resolve(__dirname, OUTPUT_PATH),
filename: '[name].[contenthash].js', // 使用contenthash
publicPath: '/dist/webpack/'
}
通过webpack打包构建完后输出的两组hash不同,而且每一组内部js和css的hash也不同,下图是contenthash在项目中的效果:
三种hash的区别
hash类型 | 区别 |
---|---|
hash | hash是根据整个项目构建,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值 |
chunkhash | chunkhash根据不同的入口文件(Entry)进行依赖文件解析、构建对应的代码块(chunk),生成对应的哈希值,某文件变化时只有该文件对应代码块(chunk)的hash会变化 |
contentHash | 每一个代码块(chunk)中的js和css输出文件都会独立生成一个hash,当某一个代码块(chunk)中的js源文件被修改时,只有该代码块(chunk)输出的js文件的hash会发生变化 |
webpack的hash原理
webpack的hash是通过crypto加密和哈希算法实现的,webpack提供了hashDigest(在生成 hash 时使用的编码方式,默认为
'hex'
)、hashDigestLength(散列摘要的前缀长度,默认为20
)、hashFunction(散列算法,默认为'md5'
)、hashSalt(一个可选的加盐值)等参数来实现自定义hash;下面依次讲述三种hash生成策略。
webpack的三种hash生成策略都是根据源码内容来生成,只是该源码已经被webpack封装成能在webpack环境中运行的代码了,包含每一个源文件的绝对路径;webpack会在build阶段根据源码给对应的模块(module)生成一个_buildHash(后续根据该值生成模块的hash),如下图所示可以看到源码中包含绝对路径。
webpack在seal阶段生成三种hash,最后根据output的配置决定使用哪种hash,webpack通过执行Compilation.createHash
函数来生成hash。
hash和chunkhash的生成过程
下面主要讲一下hash的生成过程,其中chunkhash的生成过程包含在其中。webpack生成hash的第一步是获取Compilation下面的所有modules,把所有的module在build阶段生成的_buildHash作为内容生成一个新的hash值;然后获取到所有的代码块(chunks),分别把代码块(chunk)中包含的module的hash作为内容生成代码块(chunk)的hash,该hash就是配置chunkhash时需要使用的hash值;最后把所有代码块(chunks)的hash作为内容生成一个hash就是最终的hash,如下源码所示。
// 非源码,代码有删减
createHash() {
// 把所有的module根据在build阶段生成_buildHash来生成一个新的hash值
const modules = this.modules;
for (let i = 0; i < modules.length; i++) {
const module = modules[i];
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash);
}
// clone needed as sort below is inplace mutation
const chunks = this.chunks.slice();
// 给所有的chunks分别生成一个hash
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const chunkHash = createHash(hashFunction);
try {
chunk.updateHash(chunkHash);
// chunk中包含的所有module的hash作为内容生成一个hash值
template.updateHashForChunk(
chunkHash,
chunk,
this.moduleTemplates.javascript,
this.dependencyTemplates
);
chunk.hash = chunkHash.digest(hashDigest);
// 把所有的chunks的hash作为内容
hash.update(chunk.hash);
// 生成contentHash
this.hooks.contentHash.call(chunk);
} catch (err) {}
}
// 生成hash
this.fullHash = hash.digest(hashDigest);
this.hash = this.fullHash.substr(0, hashDigestLength);
}
contenthash生成过程
contenthash生成跟前两种hash生成不一样,它是通过mini-css-extract-plugin
和JavascriptModulesPlugin
插件生成的hash;mini-css-extract-plugin
是webpack打包构建时把css类型的module单独分类出来的插件,使用该插件时会为css类型的文件单独生成hash;它会把代码块(chunk)中所有类型为css/mini-extract
的module的hash作为内容生成chunkhash。
// mini-css-extract-plugin插件的css文件hash生成的钩子函数
compilation.hooks.contentHash.tap(pluginName, chunk => {
const { outputOptions } = compilation;
const { hashFunction, hashDigest, hashDigestLength } = outputOptions;
const hash = createHash(hashFunction);
// 把chunk中所有类型为`css/mini-extract`的module的hash作为内容生成hash
for (const m of chunk.modulesIterable) {
if (m.type === MODULE_TYPE) {
m.updateHash(hash);
}
}
const { contentHash } = chunk;
// 把生成的内容放入chunk对象的contentHash中
contentHash[MODULE_TYPE] = hash.digest(hashDigest).substring(0, hashDigestLength);
});
contentHash钩子触发时会调用JavascriptModulesPlugin插件注册的contentHash事件,把代码块(chunk)中所有类型为函数的module的hash作为内容生成hash。
// JavascriptModulesPlugin插件为js生成contentHash的钩子函数
compilation.hooks.contentHash.tap("JavascriptModulesPlugin", chunk => {
// ...此处有删减
for (const m of chunk.modulesIterable) {
if (typeof m.source === "function") {
hash.update(m.hash);
}
}
chunk.contentHash.javascript = hash
.digest(hashDigest)
.substr(0, hashDigestLength);
});
多机器构建方案
webpack的hash虽然给我们带来了极大的方便,但是也存在一些弊端;webpack的三种hash策略都依赖module的_buildHash,而_buildHash值又依赖module的源文件内容和绝对路径,所以同一份源码在不同的机器上构建出来的hash值不一定一样,除非两台机器上的项目路径完全相同;若线上存在多机器构建部署同一个项目时,可能hash值不同而导致访问js或者css时出现404现像。
若想多机器部署hash一样,下面是解决多机器构建生成hash的策略: