摘要:舉例這個方案還有些小缺點,就是用模塊文件路徑作為哈希輸入還不是百分百完美,如果文件名改了,那么模塊就不穩定了。其實,可以用模塊文件內容作為哈希輸入,考慮到效率問題,權衡之下還是用路徑好了。
來自 http://zhenyong.site/2016/10/...
使用 webpack 構建輸出文件時,通常會給文件名加上 hash,該 hash 值根據文件內容計算得到,只要文件內容不變,hash 就不變,于是就可以利用瀏覽器緩存來節省下載流量。可是 webpack 提供的 hash 似乎不那么靠譜...
本文只圍繞如何保證 webpack 1.x 在 生產發布階段 輸出穩定的 hash 值展開討論,如果對 webpack 還沒了解的,可以戳 webpack。
本文 基于 webpack 1.x 的背景展開討論,畢竟有些問題在 webpack 2 已經得到解決。為了方便描述問題,文中展示的代碼、配置可能很挫,也許不是工程最佳實踐,請輕拍。
懶得看文章的可以考慮直接讀插件源碼 zhenyong/webpack-stable-module-id-and-hash
目標除了 html 文件以外,其他靜態資源文件名都帶上哈希值,根據文件本身的內容計算得到,保證文件沒變化,則構建后的文件名跟上次一樣。
webpack 提供的 hash [hash]假設文件目錄長這樣:
/src |- pageA.js (入口1) |- pageB.js (入口2)
使用 webpack 配置:
entry: { pageA: "./src/pageA.js", pageB: "./src/pageB.js", }, output: { path: __dirname + "/build", // [hash:4] 表示截取 [hash] 前四位 filename: "[name].[hash:4].js" },
首次構建輸出:
pageA.c56c.js 1.47 kB 0 [emitted] pageA pageB.c56c.js 1.47 kB 1 [emitted] pageB
再次構建輸出:
pageA.c56c.js 1.47 kB 0 [emitted] pageA pageB.c56c.js 1.47 kB 1 [emitted] pageB
hash 值是穩定的呀,是不是就可以了呢?且慢!
根據 Configuration · webpack/docs Wiki :
[hash] is replaced by the hash of the compilation.
意譯:
[hash] 是根據一個 compilation 對象計算得出的哈希值,如果 compilation 對象的信息不變,則 [hash] 不變
結合 how to write a plugin 提到:
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 對象代表對某個版本進行一次編譯構建的過程,如果在開發模式下(例如用 --watch 檢測變化,實時編譯),則每次內容變化時會新建一個 complidation,包含了構建所需的上下文信息(構建器配置、文件、文件依賴)。
我們來動一下 pageA.js,再次構建:
pageA.e6a9.js 1.48 kB 0 [emitted] pageA pageB.e6a9.js 1.47 kB 1 [emitted] pageB
發現 hash 變了,并且所有文件的 hash 值總是一樣,這似乎就跟文檔描述的一致,只要構建過程依賴的任何資源(代碼)發生變化,compilation 的信息就會跟上一次不一樣了。
那是不是肯定說,源碼不變的話,hash 值就一定穩定呢?也不是的,我們改一下 webpack 配置:
entry: { pageA: "./src/pageA.js", // 不再構建入口 pageB // pageB: "./src/pageB.js", },
再次構建:
pageA.1f01.js 1.48 kB 0 [emitted] pageA
compilation 的信息還包括構建上下文,所以,移除入口或者換個loader 都會引起 hash 改變。
[hash] 的缺點很明顯,不是根據內容來計算哈希,但是 hash 值是"穩定的",用這種方案能保證『每次上線,瀏覽器訪問到的靜態資源都是新的(url 變了)』
你接受用 [hash] 嗎,我是接受不了?于是我們看 webpack 提供的另一種根據內容計算 hash 的配置。
[chunkhash][chunkhash] is replaced by the hash of the chunk.
意譯:
[chunkhash] 根據 chunk 的內容計算得到。(chunk 可以理解成一個輸出文件,其中可能包含多個 js 模塊)
我們改下配置:
entry: { pageA: "./src/pageA.js", pageB: "./src/pageB.js", }, output: { path: __dirname + "/build", filename: "[name].[chunkhash:4].js", },
構建試試:
pageA.f308.js 1.48 kB 0 [emitted] pageA pageB.53a9.js 1.47 kB 1 [emitted] pageB
動下 pageA.js 再構建:
pageA.16d6.js 1.48 kB 0 [emitted] pageA pageB.53a9.js 1.47 kB 1 [emitted] pageB
發現只有 pageA 的 hash 變了,似乎 [chunkhash] 就能解決問題了?且慢!
我們目前的代碼沒涉及到 css,先加點 css 文件依賴:
/src |- pageA.js |- pageA.css //pageA.js require("./a.css");
給 webpack 配置 css 文件的 loader,并且抽取所有樣式輸出到一個文件
module: { loaders: [{ test: /.css$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader") }], }, plugins: [ // 這里的 contenthash 是 ExtractTextPlugin 根據抽取輸出的文件內容計算得到 new ExtractTextPlugin("[name].[contenthash:4].css") ],
構建:
pageA.ab4b.js 1.6 kB 0 [emitted] pageA pageA.b9bc.css 36 bytes 0 [emitted] pageA
改一下樣式,那么樣式的 hash 肯定會變的,那 pageA.js 的 hash 變不變呢?
答案是『變了』:
pageA.0482.js 1.6 kB 0 [emitted] pageA pageA.c61a.css 31 bytes 0 [emitted] pageA
記得之前說 webpack 的 [chunkhash] 是根據 chunk 的內容計算的,而 pageA.js 這個 chunk 的輸出在 webpack 看來是包括 css 文件的,只不過被你抽取出來罷了,所以你改 css 也就改了這個 chunk 的內容,這體驗很不好吧,怎么讓 css 不影響 js 的 hash 呢?
自定義 chunkhash源碼 webpack/Compilation.js:
... this.applyPlugins("chunk-hash", chunk, chunkHash); chunk.hash = chunkHash.digest(hashDigest); ...
通過這段代碼可以發現,通過在 "chunk-hash" "鉤子" 中替換掉 chunk 的 digest 方法,就可以自定義 chunk.hash 了。
查看文檔 how to write a plugin 了解怎么寫插件來注冊一個鉤子方法:
plugins: [ ... new ContentHashPlugin() // 添加插件(生產發布階段使用) ], }; // 插件函數 function ContentHashPlugin() {} // webpack 會執行插件函數的 apply 方法 ContentHashPlugin.prototype.apply = function(compiler) { compiler.plugin("compilation", function(compilation) { compilation.plugin("chunk-hash", function(chunk, chunkHash) { // 這里注冊了之前說到的 "chunk-hash" 鉤子 chunk.digest = function () { return "這就是自定義的 hash 值"; } }); }); };
那么這個 hash 值如何計算好呢?
可以將 chunk 所依賴的各個模塊 (單個源碼文件) 的內容拼接后計算一個 md5 作為 hash 值,當然需要對所有文件排序后再拼接:
var crypto = require("crypto"); var md5Cache = {} function md5(content) { if (!md5Cache[content]) { md5Cache[content] = crypto.createHash("md5") // .update(content, "utf-8").digest("hex") } return md5Cache[content]; } function ContentHashPlugin() {} ContentHashPlugin.prototype.apply = function(compiler) { var context = compiler.options.context; function getModFilePath(mod) { // 獲取形如 "./src/pageA.css" 這樣的路徑 // libIdent 方法會處理好不同平臺的路徑分隔符問題 return mod.libIdent({ context: context }); } // 根據模塊對應的文件路徑排序 //(可以根據模塊ID,但是暫時不靠譜,后面會講) function compareMod(modA, modB) { var modAPath = getModFilePath(modA); var modBPath = getModFilePath(modB); return modAPath > modBPath ? 1 : modAPath < modBPath ? -1 : 0; } // 獲取模塊源碼,開發階段別用 function getModSrc(mod) { return mod._source && mod._source._value || ""; } compiler.plugin("compilation", function(compilation) { compilation.plugin("chunk-hash", function(chunk, chunkHash) { var source = chunk.modules.sort(compareMod).map(getModSrc).join(""); chunkHash.digest = function() { return md5(source); }; }); }); }; module.exports = ContentHashPlugin;
此時,pageA.css 修改之后,再也不會影響 pageA.js 的 hash 值。
另外要注意,ExtractTextPlugin 會把 pageA.css 的內容抽取之后,替換該模塊的內容 mod._source._value 為:
// removed by extract-text-webpack-plugin
由于每一個 css 模塊都對應這段內容,所以不會影響效果。
erm0l0v/webpack-md5-hash 插件也是為了解決類似問題,但是它其中的『排序』算法是基于模塊的 id,而模塊的 id 理論上是不穩定的,接下來我們就討論不穩定的模塊 ID 帶來的坑。
模塊 ID 的坑我們簡單的把每個文件理解為一個模塊(module),在 webpack 處理模塊依賴關系時,會給每個模塊定義一個 ID,查看 webpack/Compilation.js 發現,webpack 根據收集 module 的順序給每個模塊分配遞增數字作為 ID,至于『收集的 module 順序』,在你開發生涯里,這玩意絕對是不穩定!不穩定的!
Module ID 不穩定怎么了我們的文件結構現在長這樣:
/src |- pageA.js |- pageB.js |- a.js |- b.js |- c.js
pageA.js
require("./a.js") // a.js require("./b.js") // b.js var a = "this is pageA";
pageB.js
require("./b.js") // b.js" require("./c.js") // c.js var b = "this is pageB";
更新配置,把引用達到 2 次的模塊抽取出來:
output: { chunkFilename: "[id].[chunkhash:4].bundle.js", ... plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: "commons", minChunks: 2, chunks: ["pageA", "pageB"], }), ...
build build build:
pageA.1cda.js 262 bytes 0 [emitted] pageA pageB.0752.js 280 bytes 1 [emitted] pageB commons.14bf.js 3.64 kB 2 [emitted] commons
觀察 pageB.0752.js,有一段:
__webpack_require__(2) // b.js" __webpack_require__(3) // c.js var b = "this is pageB";
從上面看出,webpack 構建時給 b.js 的模塊 ID 為 2
這時,我們改一下 pageA.js:
// 移除對 a.js 的依賴 // require("./a.js") // a.js require("./b.js") // b.js var a = "this is pageA";
build build build :
pageA.a945.js 200 bytes 0 [emitted] pageA pageB.0752.js 271 bytes 1 [emitted] pageB commons.14bf.js 3.65 kB 2 [emitted] commons
嗯! 只有 pageA.js 的 hash 變了,挺合理合理,我們進去 pageB.0752.js 看看
__webpack_require__(1) // b.js" __webpack_require__(2) // c.js var b = "this is pageB";
看出來了沒!這次構建,webpack 給 b.js 的 ID 是 1。
我們 pageB.js 的 hash 沒變,因為背后依賴的模塊內容 (b.js、c.js) 沒有變呀,但是此時 pageB.0752.js 的內容確實變了,如果你用 CDN 上傳這個文件,也許會傳不上去,因為文件大小和名稱一模一樣,就是這個不穩定的模塊 ID 給坑的!
怎么解決呢?
第一念頭:把原來計算 hash 的方式改一下,就那構建輸出后的文件內容來計算?
細想: 不要,明明 pageB 這一次就不用重新上傳的,浪費。
比較優雅的思路就是:讓模塊 ID 給我穩定下來!!!
給我穩定的 Module ID webpack 1 的官方方案webpack 文檔提供了幾種方案
OccurrenceOrderPlugin
這個插件根據 module 被引用的次數(被 entry 引用、被 chunk 引用)來排序分配 ID,如果你的整個應用的文件依賴是沒太多變化,那么模塊 ID 就穩定,但是誰能保證呢?
recordsPath 配置
>Store/Load compiler state from/to a json file. This will result in persistent ids of modules and chunks. 會記錄每一次打包的模塊的"文件處理路徑"使用的 ID,下次打包同樣的模塊直接使用記錄中的 ID:
"node_modules/style-loader/index.js!node_modules/css-loader/index.js!src/b.css": 9,
這就要求每個人都得提交這份文件了,港真,我覺得體驗很差咯。 另外一旦你修改文件名,或者是增減 loader,原來的路徑就無效了,從而再次入坑!
DllPlugin 和 DllReferencePlugin
原理就是在你打包源碼前,你得新建一個構建配置用 [DllPlugin](https://github.com/webpack/webpack/tree/master/examples/dll) 多帶帶打包生成一份模塊文件路徑對應的 ID 記錄,然后在你的原來配置使用 [DllReferencePlugin](https://github.com/webpack/webpack/tree/master/examples/dll-user) 引用這份記錄,跟 recordsPath 大同小異,但是更高效和穩定,但是這個額外的構建,我覺得不夠優雅,至于能快多少呢,我目前還不在意這個速度,另外還是得提交多一份記錄文件。webpack 2 的思路
webpack/HashedModuleIdsPlugin.js at master · webpack/webpack
webpack/NamedModulesPlugin.js at master · webpack/webpack
以上兩個插件的思路都是用模塊對應的文件路徑直接作為模塊 ID,而不是 webpack 1 中的默認使用數字,另外 webpack 1 不接受非數字作為 模塊 ID。
我們的思路把模塊對應的文件路徑通過一個哈希計算映射為數字,用這個全局唯一的數字作為 ID 就解決了,妥妥的!
參考:
webpack/Compilation.js 中暴露的 before-module-ids 鉤子
webpack/HashedModuleIdsPlugin.js
給出 webpack 1.x 中的解決方案:
... xx.prototype.apply = function(compiler) { function hexToNum(str) { str = str.toUpperCase(); var code = "" for (var i = 0; i < str.length; i++) { var c = str.charCodeAt(i) + ""; if ((c + "").length < 2) { c = "0" + c } code += c } return parseInt(code, 10); } var usedIds = {}; function genModuleId(module) { var modulePath = module.libIdent({ context: compiler.options.context }); var id = md5(modulePath); var len = 4; while (usedIds[id.substr(0, len)]) { len++; } id = id.substr(0, len); return hexToNum(id) } compiler.plugin("compilation", function(compilation) { compilation.plugin("before-module-ids", function(modules) { modules.forEach(function(module) { if (module.libIdent && module.id === null) { module.id = genModuleId(module); usedIds[module.id] = true; } }); }); }); }; ...
注冊鉤子的思路跟之前的 content hash 插件差不多,獲取到模塊文件路徑后,通過 md5 計算輸出 16 進制的字符串([0-9A-E]),再把字符串的字符逐個轉為 ascii 形式的整數,由于 16 進制字符串只會包含 [0-9A-E],所以保證單個字符轉化的整數是兩位就能保證這個算法是有效的。
舉例:
path = "/node_module/xxx" md5Hash = md5(path) // => A3E... nul = hexToNum(md5Hash) // => 650369
這個方案還有些小缺點,就是用模塊文件路徑作為哈希輸入還不是百分百完美,如果文件名改了,那么模塊 ID 就 "不穩定了"。其實,可以用模塊文件內容作為哈希輸入,考慮到效率問題,權衡之下還是用路徑好了。
總結為了保證 webpack 1.x 生產階段的文件 hash 值能夠完美跟文件內容一一映射,查閱了大量信息,根據目前 github 上討論的解決方案算是大體解決了問題,但是還不夠優雅和完美,于是借鑒 webpack 2 的思路加上一點小技巧,比較優雅地解決了這個問題。
插件放在 Github: zhenyong/webpack-stable-module-id-and-hash『有用的話給個 star 嘛 O(∩_∩)O』
參考資料Vendor chunkhash changes when app code changes · Issue #1315 · webpack/webpack
Vendor chunkhash changes when app code changes · Issue #1315 · webpack/webpack
Webpack中hash與chunkhash的區別,以及js與css的hash指紋解耦方案 - zhoujunpeng - 博客園
webpack使用優化 | Web前端 騰訊AlloyTeam Blog | 愿景: 成為地球卓越的Web團隊!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/61830.html
摘要:更新日志更新完成靜態頁面原型修復使用的正確姿勢更新添加靜態頁面更新添加使用方法請戳我主要作用就是在你開發環節在后端同學還未開發完成的情況下,提供一個。 底下評論說是標題黨,或者是光扔個github地址上來的同學我就不說什么了。你們有看看倉庫的提交記錄么?我還沒有吃撐到開個倉庫去騙star.我的出發點就是每天更新一部分代碼,教大家用我所提到的技術棧搭建一個blog,我的出發點就是這么簡單...
摘要:續前端臨床手札構建逐步解構上工作流程案例最近添加了雪碧圖功能,并把替換成的,詳細可以看分支構建生產上一篇說完了本地測試和是如何工作,接下來分析構建生產模式下配置如何配置和每個模塊干了什么。 續 前端臨床手札——webpack構建逐步解構(上) 工作流程 showImg(https://segmentfault.com/img/bVCXjo?w=793&h=410); 案例:multip...
摘要:容器化應用日志收集挑戰應用日志的收集分析和監控是日常運維工作重要的部分,妥善地處理應用日志收集往往是應用容器化重要的一個課題。日志來源識別采用統一應用日志收集方案,日志分散在很多不同容器的相互隔離的環境中,需要解決日志的來源識別問題。 容器化應用日志收集挑戰 應用日志的收集、分析和監控是日常運維工作重要的部分,妥善地處理應用日志收集往往是應用容器化重要的一個課題。 Docker處理日志...
摘要:的開發環境配置說明完整的的配置地址開發環境的搭建,總體而言就比較輕松,因為用戶就是開發者們。的做法是在的字段配置類似這樣這樣配置后,當運行時,在里通過可以取到值以來做判斷就可以啦。 webpack4 的開發環境配置說明 完整的webpack4的配置clone地址: https://github.com/ziwei3749/... 開發環境的搭建,總體而言就比較輕松,因為用戶就是開發者們...
閱讀 3464·2021-11-18 10:02
閱讀 3707·2021-09-13 10:25
閱讀 1920·2021-07-26 23:38
閱讀 2569·2019-08-30 15:44
閱讀 2270·2019-08-30 13:51
閱讀 1225·2019-08-26 11:35
閱讀 2273·2019-08-26 10:29
閱讀 3444·2019-08-23 14:56