近期發現webpack在多臺機器上打包同一份代碼生成的hash不同,查看社區沒有文章深刻說明hash生成策略 ,因此把webpack源碼擼了一遍,定位到是hash生成時包含有項目的絕對路徑致使,最後編寫一個webpack插件解決了該問題,本文主要講解hash的用法和原理以及如何解決多機器hash不一致等坑。javascript
前端同窗衆所周知靜態資源首次被加載後瀏覽器會進行緩存,同一個資源在緩存未過時狀況下通常不會再去請求,那麼當資源有更新時如何通知瀏覽器資源有變化呢?資源文件命名hash化就是解決該問題而生;webpack是如今前端的主流構建工具,因此本文主要是講述webpack構建後文件名的hash策略;webpack分爲hash、chunkhash、contenthash這三種hash,下面咱們依次講述一下三種hash的使用和原理。css
使用webpack構建時hash是使用最多的一種,webpack構建後整個項目的js和css輸出文件的hash都相同;例如一個項目有6個組件,須要把組件一、二、3做爲代碼塊(chunk)輸出一組js和css文件,組件四、5做爲代碼塊(chunk)輸出一組js和css文件,webpack以下配置:html
output: {
path: path.resolve(__dirname, OUTPUT_PATH),
filename: '[name].[hash].js',// 使用hash
publicPath: '/dist/webpack/'
}
複製代碼
經過webpack構建完後輸出的第一組js、css文件的hash相同,而且第二組和第一組的hash也相同,下圖是hash在項目中的效果:前端
因此只要某一個文件被修改,全部輸出文件的hash都會跟着變化;所以它有一個弊端,一旦修改了某一個文件,整個項目的文件緩存都會失效。java
chunkhash相對hash影響範圍比較小,使用chunkhash時,每個代碼塊(chunk)輸出文件對應一個hash,某源文件被修改後,只有該源文件所在代碼塊(chunk)的輸出文件的hash會變化;例如一個項目有6個組件,須要把組件一、二、3做爲代碼塊(chunk)輸出一組js和css文件,組件四、5做爲代碼塊(chunk)輸出一組js和css文件,webpack以下配置:node
output: {
path: path.resolve(__dirname, OUTPUT_PATH),
filename: '[name].[chunkhash].js', // 使用chunkhash
publicPath: '/dist/webpack/'
}
複製代碼
經過webpack打包構建完後輸出的兩組hash不一樣,可是每一組內部js和css的hash相同,下圖是chunkhash在項目中的效果: webpack
當使用mini-css-extract-plugin
插件時還可使用contenthash來獲取文件的hash,contenthash相對於chunkhash影響範圍更小;每個代碼塊(chunk)中的js和css輸出文件都會獨立生成一個hash,當某一個代碼塊(chunk)中的js源文件被修改時,只有該代碼塊(chunk)輸出的js文件的hash會發生變化;例如一個項目有6個組件,須要把組件一、二、3做爲代碼塊(chunk)輸出一組js和css文件,組件四、5做爲代碼塊(chunk)輸出一組js和css文件,webpack以下配置:web
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值 |
chunkhash | chunkhash根據不一樣的入口文件(Entry)進行依賴文件解析、構建對應的代碼塊(chunk),生成對應的哈希值,某文件變化時只有該文件對應代碼塊(chunk)的hash會變化 |
contentHash | 每個代碼塊(chunk)中的js和css輸出文件都會獨立生成一個hash,當某一個代碼塊(chunk)中的js源文件被修改時,只有該代碼塊(chunk)輸出的js文件的hash會發生變化 |
webpack的hash是經過crypto加密和哈希算法實現的,webpack提供了hashDigest(在生成 hash 時使用的編碼方式,默認爲
'hex'
)、hashDigestLength(散列摘要的前綴長度,默認爲20
)、hashFunction(散列算法,默認爲'md5'
)、hashSalt(一個可選的加鹽值)等參數來實現自定義hash;下面依次講述三種hash生成策略。npm
webpack的三種hash生成策略都是根據源碼內容來生成,只是該源碼已經被webpack封裝成能在webpack環境中運行的代碼了,包含每個源文件的絕對路徑;webpack會在build階段根據源碼給對應的模塊(module)生成一個_buildHash(後續根據該值生成模塊的hash),以下圖所示能夠看到源碼中包含絕對路徑。
webpack在seal階段生成三種hash,最後根據output的配置決定使用哪一種hash,webpack經過執行Compilation.createHash
函數來生成hash。
下面主要講一下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生成跟前兩種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的策略: