檔案的hash指紋通常作為前端靜态資源實作增量更新的方案之一,Webpack是目前最流行的開源編譯工具之一,其強大的功能也帶來很多坑(當然,大部分麻煩其實都可以在官方文檔中找到答案)。
比如,在Webpack編譯輸出檔案的配置過程中,如果需要為檔案加入hash指紋,Webpack提供了兩個配置項可供使用:
hash
和 chunkhash
。那麼兩者有何差別呢?其各自典型的應用場景又是什麼?本文結合筆者工作中遇到的問題,簡單記錄一下以上問題的解決方案。 1. hash與chunkhash
首先我們先看一下官方文檔對于兩者的定義:
[hash] is replaced by the hash of the compilation.
hash
代表的是compilation的hash值。
[chunkhash] is replaced by the hash of the chunk.
chunkhash
代表的是chunk的hash值。
chunkhash
很好了解,chunk在Webpack中的含義我們都清楚,簡單講,chunk就是子產品。
chunkhash
也就是根據子產品内容計算出的hash值。
那麼該如何了解
hash
是compilation的hash值這句話呢?
首先先講解一下Webpack中compilation的含義。
1.1 compilation
Webpack官方文檔中How to write a plugin章節有對compilation的詳解。
A compilation object represents a single build of versioned assets. While running Webpack development middleware, a new compilation will be created each time a file change is detected, thus generating a new set of compiled assets. A compilation surfaces information about the present state of module resources, compiled assets, changed files, and watched dependencies.
compilation對象代表某個版本的資源對應的編譯程序。當使用Webpack的development中間件時,每次檢測到項目檔案有改動就會建立一個compilation,進而能夠針對改動生産全新的編譯檔案。compilation對象包含目前子產品資源、待編譯檔案、有改動的檔案和監聽依賴的所有資訊。
與compilation對應的有個compiler對象,通過對比,可以幫助大家對compilation有更深入的了解。
The compiler object represents the fully configured Webpack environment. This object is built once upon starting Webpack, and is configured with all operational settings including options, loaders, and plugins.
compiler對象代表的是配置完備的Webpack環境。 compiler對象隻在Webpack啟動時建構一次,由Webpack組合所有的配置項建構生成。
簡單的講,compiler對象代表的是不變的webpack環境,是針對webpack的;而compilation對象針對的是随時可變的項目檔案,隻要檔案有改動,compilation就會被重新建立。
了解了compilation之後,再回頭看
hash
的定義:
compilation在項目中任何一個檔案改動後就會被重新建立,然後webpack計算新的compilation的hash值,這個hash值便是
hash
。
如果使用
hash
作為編譯輸出檔案的hash指紋的話,如下:
output: {
filename: '[name].[hash:8].js',
path: __dirname + '/built'
}
hash
是compilation對象計算所得,而不是具體的項目檔案計算所得。是以以上配置的編譯輸出檔案,所有的檔案名都會使用相同的hash指紋。如下:

這樣帶來的問題是,三個js檔案任何一個改動都會影響另外兩個檔案的最終檔案名。上線後,另外兩個檔案的浏覽器緩存也全部失效。這肯定不是我們想要的結果。
那麼如何避免這個問題呢?
答案就是
chunkhash
!
根據
chunkhash
的定義知道,
chunkhash
是根據具體子產品檔案的内容計算所得的hash值,是以某個檔案的改動隻會影響它本身的hash指紋,不會影響其他檔案。配置webpack的output如下:
output: {
filename: '[name].[chunkhash:8].js',
path: __dirname + '/built'
}
編譯輸出的檔案為:
每個檔案的hash指紋都不相同,上線後無改動的檔案不會失去緩存。
說來說去,好像chunkhash可以完全取代hash,那麼hash就毫無用處嗎?
1.2 hash應用場景
接上文所述,webpack的
hash
字段是根據每次編譯compilation的内容計算所得,也可以了解為項目總體檔案的hash值,而不是針對每個具體檔案的。
webpack針對compilation提供了兩個hash相關的生命周期鈎子:
before-hash
after-hash
。源碼如下:
this.applyPlugins("before-hash");
this.createHash();
this.applyPlugins("after-hash");
hash
可以作為版本控制的一環,将其作為編譯輸出檔案夾的名稱統一管理,如下:
output: {
filename: '/dest/[hash]/[name].js'
}
我們不讨論這種方式的合理性和效率,這隻是
hash
的一種應用場景。當然,
hash
還有其他的應用場景,不過筆者目前未接觸過,歡迎大家補充。
2. js與css共用相同chunkhash的解決方案
webpack的理念是把所有類型的檔案都以js為彙聚點,不支援js檔案以外的檔案為編譯入口。是以如果我們要編譯style檔案,唯一的辦法是在js檔案中引入style檔案。如下:
import 'style/style.scss';
webpack預設将js/style檔案統統編譯到一個js檔案中,可以借助extract-text-webpack-plugin将style檔案單獨編譯輸出。從這點可以看出,webpack将style檔案視為js的一部分。
這樣的模式下有個很嚴重的問題,當我們希望将css單獨編譯輸出并且打上hash指紋,按照前文所述的使用
chunkhash
配置輸出檔案名時,編譯的結果是js和css檔案的hash指紋完全相同。不論是單獨修改了js代碼還是style代碼,編譯輸出的js/css檔案都會打上全新的相同的hash指紋。這種狀況下我們無法有效的進行版本管理和部署上線。
為什麼會産生這種問題呢?
2.1 chunkhash的計算模式
前文提到了webpack的編譯理念,webpack将style視為js的一部分,是以在計算
chunkhash
時,會把所有的js代碼和style代碼混合在一起計算。比如
main.js
引用了
main.scss
:
import 'main.scss';
alert('I am main.js');
main.scss
的内容如下:
body{
color: #000;
}
webpack計算chunkhash時,以
main.js
檔案為編譯入口,整個chunk的内容會将
main.scss
的内容也計算在内:
body{
color: #000;
}
alert('I am main.js');
是以,不論是修改了js代碼還是scss代碼,整個chunk的内容都改變了,計算所得的chunkhash自然就不同了。
那麼如何解決這種問題呢?
2.2 contenthash
前文提到了使用extract-text-webpack-plugin單獨編譯輸出css檔案,造成上一節js/css共用hash指紋的配置為:
new ExtractTextPlugin('[name].[chunkhash].css');
extract-text-webpack-plugin提供了另外一種hash值:
contenthash
。顧名思義,
contenthash
代表的是文本檔案内容的hash值,也就是隻有style檔案的hash值。這個hash值就是解決上述問題的銀彈。修改配置如下:
new ExtractTextPlugin('[name].[contenthash].css');
編譯輸出的js和css檔案将會有其獨立的hash指紋。
到這裡是不是就找到完美的解決方案了呢?
遠遠沒有!
結合上文提到的種種,考慮一下這個問題:如果隻修改了
main.scss
檔案,未修改
main.js
檔案,那麼編譯輸出的js檔案的hash指紋會改變嗎?
答案是肯定的。
修改了
main.scss
編譯輸出的css檔案hash指紋理所當然要更新,但是我們并未修改
main.js
,可是js檔案的hash指紋也更新了。這是因為上文提到的:
main.js
的内容也計算在内。
main.scss
那麼怎麼解決這個問題呢?
很簡單,既然我們知道了webpack計算chunkhash的方式,那我們就從這一點出發,嘗試修改chunkhash的計算方式。
2.3 chunk-hash
此小節内容隻适用于webpack1,webpack2已經修複了hash相關的計算規則。
chunk-hash并不是webpack中另一種hash值,而是compilation執行生命周期中的一個鈎子。chunk-hash鈎子代表的是哪個階段呢?請看webpack的Compilation.js源碼中以下部分:
for(i = 0; i < chunks.length; i++) {
chunk = chunks[i];
var chunkHash = require("crypto").createHash(hashFunction);
if(outputOptions.hashSalt)
hash.update(outputOptions.hashSalt);
chunk.updateHash(chunkHash);
if(chunk.entry) {
this.mainTemplate.updateHashForChunk(chunkHash, chunk);
} else {
this.chunkTemplate.updateHashForChunk(chunkHash);
}
this.applyPlugins("chunk-hash", chunk, chunkHash);
chunk.hash = chunkHash.digest(hashDigest);
hash.update(chunk.hash);
chunk.renderedHash = chunk.hash.substr(0, hashDigestLength);
}
webpack使用NodeJS内置的crypto子產品計算chunkhash,具體使用哪種算法與我們讨論的内容無關,我們隻需要關注上述代碼中
this.applyPlugins("chunk-hash", chunk, chunkHash);
的執行時機。
chunk-hash是在chunhash計算完畢之後執行的,這就意味着如果我們在chunk-hash鈎子中可以用新的chunkhash替換已存在的值。如下僞代碼:
compilation.plugin("chunk-hash", function(chunk, chunkHash) {
var new_hash = md5(chunk);
chunkHash.digest = function () {
return new_hash;
};
});
webpack之是以如果流行的原因之一就是擁有龐大的社群和不計其數的開發者們,實際上,我們遇到的問題已經有先驅者幫我們解決了。插件webpack-md5-hash便是上述僞代碼的具體實作,我們需要做的隻是将這個插件加入到webpack的配置中:
var WebpackMd5Hash = require('webpack-md5-hash');
module.exports = {
output: {
//...
chunkFilename: "[chunkhash].chunk.js"
},
plugins: [
new WebpackMd5Hash()
]
};
3. 結語
靜态資源的版本管理是前端工程化中非常重要的一環,使用webpack作為建構工具時需要謹慎使用
hash
chunkhash
,并且還需要注意webpack将一切視為js子產品這種理念帶來的一些不便。
webpack可以說是目前最流行的建構工具了,但是其官方文檔太過籠統,許多細節并未列出,需要研究源碼才會了解。好在我們并非獨立戰鬥,龐大的社群資源也是促進webpack流行的重要因素之一。
行文至此,正常的前端項目中關于靜态資源hash指紋的問題基本得到了解決,但是前端的環境是複雜的,各種新技術新架構層出不窮。最後留一點懸念給大家:像vue這種将template/js/style統統寫在一個js檔案中,如何保證在隻修改了style時不影響編譯輸出的js檔案hash指紋?