本文內容只適用於webpack v1版本,webpack v2已經修復了hash計算規則。javascript
以前討論了webpack的hash與chunkhash的區別以及各自的應用場景,若是是常規單頁面應用的話,上篇文章提供的方案是沒有問題的。可是前端項目複雜多變,應對複雜多頁面項目時,咱們不得不繼續踩webpack的hash坑。css
在進入正文以前先解釋一下所謂的常規單頁面和複雜多頁面是什麼意思。html
這兩個並不是專業術語,而是筆者實在想不出更恰當的說法了,見諒。前端
常規單頁面符合如下條件:java
與主文件不關聯的懶加載文件指的是邏輯與主文件徹底無關的js文件,這類文件不參與主文件打包。好比主文件main.js
中有如下代碼:webpack
window.onload = function(){ var script = document.createElement('script'); script.src = '//static.daojia.com/bi.js'; document.head.appendChild(script); }
其中bi.js
的內部邏輯與main.js
沒有任何關聯,它對於main.js
來講就是一個字符串而已。git
與之相對應的是與主文件有邏輯關係的模塊文件,好比如下代碼:github
window.onload = function(){ require.ensure([],function(require){ require('./part.a.js'); },'a'); }
其中part.a.js
是懶加載模塊,以上源碼經編譯會生成獨立的part文件,由main.js
按需加載。web
複雜多頁面項目符合如下條件:算法
那麼這種類型的項目複雜度在哪呢?如何應用webpack去解決hash問題?
上篇文章webpack的hash與chunkhash的區別以及各自的應用場景提到應該使用chunkhash
結合webpack-md5-plugin做爲js文件hash解決方案。這種方案在應對全部模塊均同步編譯的場景是沒有問題的,可是請你們首先考慮下文的場景。
入口文件main.app.js
的代碼以下:
import '../style/main.app.scss'; import fn_d from './part.d.js'; console.log('main'); window.onload = function(){ require.ensure([],(require)=>{ require('./part.a.js'); }); }
異步模塊part.a.js
代碼以下:
import fn_d from './part.d.js'; console.log('part a'); setTimeout(()=>{ require.ensure([],(require)=>{ require('./part.b.js'); }); },10000);
異步模塊part.b.js
代碼以下:
import fn_c from './part.c.js'; import fn_d from './part.d.js'; console.log('part b');
使用webpack將以上源代碼進行編譯,輸出如下文件:
main.app.[chunkhash].js
:主文件;part.a.[chunkhash].js
:異步模塊a;part.b.[chunkhash].js
:異步模塊b;main.app.[chunkhash].css
:樣式文件。截止到目前是沒有問題的,如今,請你們想象一下:若是咱們修改了part.a.js
源碼,編譯的結果文件哪些文件的hash改變了?
首先能夠確定的是part.a.[chunkhash].js
的hash值會改變,那麼其餘文件呢?
答案是:只有part.a.[chunkhash].js
的hash改變了,其他文件的hash都與修改前一致。
那麼這種結果是否合理呢?
在回答這個問題以前,咱們首先了解一下webpack runtime是如何加載異步模塊的。請看如下代碼:
var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); script.type = 'text/javascript'; script.charset = 'utf-8'; script.async = true; script.src = __webpack_require__.p + "js/part/part." + ({ "1": "a", "2": "b" }[chunkId] || chunkId) + "." + { "1": "f5ea7d95", "2": "b93662b0" }[chunkId] + ".js"; head.appendChild(script);
上述代碼是編譯生成的main.app.[chunkhash].js
中實現懶加載的邏輯,原理就是你們熟知的動態生成<script>
標籤。可是在對script.src
賦值時,webpack有如下三個概念須要知曉:
chunkId
,對應上述代碼中的"1"
和"2"
;chunkName
,對應上述代碼中的"a"
和"b"
;chunkHash
,對應上述代碼中的"f5ea7d95"
和"b93662b0"
。
chunkId
和chunkName
暫時不用關心,咱們只須要關注chunkHash
的變更。
也就是說,part.a.[chunkhash].js
和part.b.[chunkhash].js
的hash值是寫死在main.app.[chunkhash].js
中的。按照以前的編譯結果,part.a.[chunkhash].js
的hash變了,可是main.app.[chunkhash].js
的hash沒變,那麼用戶的瀏覽器仍然緩存着舊版本的main.app.[chunkhash].js
,此時異步加載的part.a.[chunkhash].js
仍然是舊版本的文件。這顯然是不符合需求的。
總結以上所述,懶加載模塊的改動經編譯,主文件的hash值沒有變化,影響了版本發佈。
筆者在初次遇到上述問題時,第一個出如今腦海裏的念頭是:主文件計算hash值時沒有把異步模塊的內容計算在內。
結合上篇文章webpack的hash與chunkhash的區別以及各自的應用場景,webpack-md5-plugin是在chunk-hash
鉤子函數中替換了chunkhash
,那麼webpack在執行chunk-hash
鉤子函數以前對源代碼的編譯進行到了哪一步?
咱們在chunk-hash
鉤子函數內將各模塊的信息打印出來:
compilation.plugin("chunk-hash", function(chunk, chunkHash) { console.log(chunk); });
因爲打印信息太多,就不貼出來了。此時一共有5個chunk:
main.app
;part.a
;part.b
。其中html和style都是由插件導出,因此這兩個chunk是不會被分配chunkId
和chunkName
的,不會影響js的編譯。
而後打印一下各模塊對應此時的代碼。main.app.js
此時的代碼以下:
require('../styles/main.app.scss'); var _partD = require('./part.d.js'); var _partD2 = _interopRequireDefault(_partD); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } console.log('main'); window.onload = function () { require.ensure(['./part.a.js'], function (require) { require('./part.a.js'); }, 'a'); };"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = function (msg) { console.log(msg); };
能夠看出,main.app.js
相關的同步模塊part.d.js
的內容已經被編譯進了主文件(最後三行),只是url仍然未改變。而異步模塊part.a.js
不只url仍然是原始的本地相對地址,並且內容也並無編譯進主文件。
可是請注意,上文提到的5個chunk中包含了part.a
,也就是說part.a.js
此時已經被編譯了,而且已經計算了hash值。
詳細的log信息你們能夠自行打印出來研究。
此時main.app.js
的chunkhash仍然是使用webpack自身計算所得,webpack默認的chunkhash計算方法是將與當前模塊全部相關的所有內容做爲算法參數,包括style文件。而webpack-md5-hash插件對chunk-hash
鉤子進行捕獲並從新計算chunkhash,它的計算方法是只計算模塊自己的當前內容(包括同步模塊),也就是上文的代碼。這種計算方式把異步模塊的內容忽略掉了,形成了本文面對的問題:異步模塊的修改並未影響主文件的hash值。
既然找到了引發問題的緣由,那麼相應的解決方案相信你們內心多少有點數了。
可能會有人說:我不使用webpack-md5-hash插件不就好了嗎?
你們還記得上篇文章webpack的hash與chunkhash的區別以及各自的應用場景提到的webpack計算chunkhash的方法,style文件也會被計算在內,因此使用webpack自身的chunkhash計算方案確定是不可行的。
若是有研究webpack稍微深刻的同窗可能會發現:主文件使用[hash]
而不是[chunkhash]
,異步模塊使用chunkhash
,同時搭配webpack-md5-hash插件使用。這種方案下,style的修改並不會影響主文件的[hash]
值。這種方案是否可行呢?
首先咱們分析一下這種方案的原理。[hash]
是compilation實例的hash值,webpack是在全部的chunkhash基礎上進行計算此hash值。默認狀況下,main.app.js
的chunkhash會包括style文件的內容,而webpack-md5-hash插件將style文件內容剔除,只計算js部分。因此,style文件的修改不影響最後的[hash]
值。
乍看起來,以上方案是能夠解決咱們的問題的。可是你們請考慮這種場景:若是項目中存在不止一個主js文件呢?修改任意js代碼會不會影響最終主文件的[hash]
值?
答案是確定的!webpack將全部js文件的內容做爲計算[hash]
的參數,任何js文件的修改都會影響最終的結果。也就是說,假如我修改了主文件main.app_a.js
或者main.app_a.js
的任意(同步/異步)模塊,那麼main.app.js
的hash值也會改變。這顯然是不符合需求的。
既然上面的兩種方案都不行,到底什麼纔是可行的方案呢?
其實,解決問題的關鍵在前文中都提到了,只要打印出chunk-hash
鉤子函數的chunk信息,解決方案就浮出水面了。關鍵點有兩個:
chunk-hash
時異步模塊已經被編譯了,而且生成了hash值;chunks
屬性,value是異步模塊chunk的集合數組。咱們主文件中獲取到各異步模塊的hash值,而後將這些hash值與主文件的代碼內容一同做爲計算hash的參數,這樣就能保證主文件的hash值會跟隨異步模塊的修改而修改。
基於以上方案,筆者站在巨人肩上,在webpack-md5-hash插件的基礎上進行了簡單地修改。代碼以下:
compilation.plugin("chunk-hash", function(chunk, chunkHash) { var source = chunk.modules.sort(compareModules).map(getModuleSource).reduce(concatenateSource, ''); // get children chunks hashes so that child chunk impact main file's hash var child_hashes = ''; if (chunk.entry && chunk.name && chunk.chunks && chunk.chunks.length > 0) { child_hashes = getHashes(chunk.chunks); } var chunk_hash = child_hashes === '' ? md5(source) : md5(source + child_hashes); chunkHash.digest = function() { return chunk_hash; }; });
以上插件已發佈webpack-split-hash
webpack的不少理念和解決方案是針對SPA項目的,多頁面應用的一些問題須要一些複雜的方案去解決。hash是前端靜態資源增量發佈的通用手段,而webpack針對hash的解決方案是沒法應對多頁面項目的。本篇文章以筆者真實遇到的場景爲例,記錄了懶加載場景下各模塊的hash解決方案。
最後打個廣告,58到家前端工程集成解決方案boi已經開源。boi是對webpack的深度使用,它不是最好的前端工程解決方案,咱們在不斷踩坑的路上儘可能分享webpack以及前端工程化的心得,但願可以幫助你們少踩點坑。