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