文件的hash指紋一般做爲前端靜態資源實現增量更新的方案之一,Webpack是目前最流行的開源編譯工具之一,其強大的功能也帶來不少坑(固然,大部分麻煩其實均可以在官方文檔中找到答案)。javascript
好比,在Webpack編譯輸出文件的配置過程當中,若是須要爲文件加入hash指紋,Webpack提供了兩個配置項可供使用:hash
和chunkhash
。那麼二者有何區別呢?其各自典型的應用場景又是什麼?本文結合筆者工做中遇到的問題,簡單記錄一下以上問題的解決方案。css
首先咱們先看一下官方文檔對於二者的定義:html
[hash] is replaced by the hash of the compilation.前端
hash
表明的是compilation的hash值。vue
[chunkhash] is replaced by the hash of the chunk.java
chunkhash
表明的是chunk的hash值。node
chunkhash
很好理解,chunk在Webpack中的含義咱們都清楚,簡單講,chunk就是模塊。chunkhash
也就是根據模塊內容計算出的hash值。webpack
那麼該如何理解hash
是compilation的hash值這句話呢?git
首先先講解一下Webpack中compilation的含義。github
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
的定義:
[hash] is replaced by the hash of the compilation.
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就毫無用處嗎?
接上文所述,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
還有其餘的應用場景,不過筆者目前未接觸過,歡迎你們補充。
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指紋。這種情況下咱們沒法有效的進行版本管理和部署上線。
爲何會產生這種問題呢?
前文提到了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天然就不一樣了。
那麼如何解決這種問題呢?
前文提到了使用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指紋也更新了。這是由於上文提到的:
webpack計算chunkhash時,以
main.js
文件爲編譯入口,整個chunk的內容會將main.scss
的內容也計算在內。
那麼怎麼解決這個問題呢?
很簡單,既然咱們知道了webpack計算chunkhash的方式,那咱們就從這一點出發,嘗試修改chunkhash的計算方式。
此小節內容只適用於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() ] };
靜態資源的版本管理是前端工程化中很是重要的一環,使用webpack做爲構建工具時須要謹慎使用hash
和 chunkhash
,而且還須要注意webpack將一切視爲js模塊這種理念帶來的一些不便。
webpack能夠說是目前最流行的構建工具了,可是其官方文檔太過籠統,許多細節並未列出,須要研究源碼纔會瞭解。好在咱們並不是獨立戰鬥,龐大的社區資源也是促進webpack流行的重要因素之一。
行文至此,常規的前端項目中關於靜態資源hash指紋的問題基本獲得瞭解決,可是前端的環境是複雜的,各類新技術新框架層出不窮。最後留一點懸念給你們:像vue這種將template/js/style通通寫在一個js文件中,如何保證在只修改了style時不影響編譯輸出的js文件hash指紋?