摘要:應(yīng)用源碼分析解讀結(jié)論熱更新的流程在構(gòu)建項(xiàng)目時(shí)會(huì)創(chuàng)建服務(wù)端基于和客戶(hù)端通常指瀏覽器,項(xiàng)目正式啟動(dòng)運(yùn)行時(shí)雙方會(huì)通過(guò)保持連接,用來(lái)滿(mǎn)足前后端實(shí)時(shí)通訊。服務(wù)端源碼的關(guān)鍵部分每一個(gè)都是沒(méi)有屬性,表示沒(méi)有發(fā)生變化。
webpack-dev-server 簡(jiǎn)介
Use webpack with a development server that provides live reloading. This should be used for development only.應(yīng)用
It uses webpack-dev-middleware under the hood, which provides fast in-memory access to the webpack assets.
將webpack與提供實(shí)時(shí)重新加載的開(kāi)發(fā)服務(wù)器一起使用。 這應(yīng)該僅用于開(kāi)發(fā)。
它使用了引擎蓋下的webpack-dev-middleware,它提供了對(duì)webpack資產(chǎn)的快速內(nèi)存訪問(wèn)。
Getting Started
源碼分析解讀 1. 結(jié)論:熱更新的流程webpack在構(gòu)建項(xiàng)目時(shí)會(huì)創(chuàng)建服務(wù)端(server基于node)和客戶(hù)端(client通常指瀏覽器),項(xiàng)目正式啟動(dòng)運(yùn)行時(shí)雙方會(huì)通過(guò)socket保持連接,用來(lái)滿(mǎn)足前后端實(shí)時(shí)通訊。
當(dāng)我們保存代碼時(shí),會(huì)被webpack監(jiān)聽(tīng)到文件變化,觸發(fā)新一輪編譯生成新的編譯器compiler(就是我們所說(shuō)的Tapable實(shí)例。它混合在一起Tapable來(lái)吸收功能來(lái)注冊(cè)和調(diào)用插件本身,可以理解為一個(gè)狀態(tài)機(jī)。)。
在compiler的’done’鉤子函數(shù)(生命周期)里調(diào)用_sendStats發(fā)放向client發(fā)送ok或warning消息,并同時(shí)發(fā)送向client發(fā)送hash值,在client保存下來(lái)。
client接收到ok或warning消息后調(diào)用reloadApp發(fā)布客戶(hù)端檢查更新事件(webpackHotUpdate)
webpack/hot部分監(jiān)聽(tīng)到webpackHotUpdate事件,調(diào)用check方法進(jìn)行hash值對(duì)比以及檢查各modules是否需要更新。如需更新會(huì)調(diào)用hotDownloadManifest方法下載json(manifest)文件。
hotDownloadManifest完成后調(diào)用hotDownloadUpdateChunk方法,通過(guò)jsonp的方式加載最新的chunk,之后分析對(duì)比文件進(jìn)行文件的更新替換,完成整個(gè)熱更新流程。
注:以下源碼分析采用倒敘分析方式
2. webpack/hot 源碼解讀在webpack構(gòu)建項(xiàng)目時(shí),webpack-dev-server會(huì)在編譯后js文件加上兩個(gè)依賴(lài)文件:
/***/ (function(module, exports, __webpack_require__) { // 建立socket連接,保持前后端實(shí)時(shí)通訊 __webpack_require__("./node_modules/webpack-dev-server/client/index.js?http://localhost:8080"); // dev-server client熱更新的方法 __webpack_require__("./node_modules/webpack/hot/dev-server.js"); module.exports = __webpack_require__("./src/index.js"); /***/ })
webpack/hot/dev-server.js的文件內(nèi)容:
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ /*globals window __webpack_hash__ */ if (module.hot) { var lastHash; //__webpack_hash__是每次編譯的hash值是全局的,就是放在window上 var upToDate = function upToDate() { return lastHash.indexOf(__webpack_hash__) >= 0; }; var log = require("./log"); var check = function check() { module.hot .check(true) // 這里的check方法最終進(jìn)入到webpacklibHotModuleReplacement.runtime.js文件中 .then(function(updatedModules) { //檢查所有要更新的module,如果沒(méi)有module要更新那么返回null if (!updatedModules) { log("warning", "[HMR] Cannot find update. Need to do a full reload!"); log( "warning", "[HMR] (Probably because of restarting the webpack-dev-server)" ); window.location.reload(); return; } //檢測(cè)時(shí)候還有module需要更新,如果有=>check() if (!upToDate()) { check(); } //打印更新結(jié)果,所有需要更新的module和已經(jīng)被更新的module都是updatedModules require("./log-apply-result")(updatedModules, updatedModules); if (upToDate()) { log("info", "[HMR] App is up to date."); } }) .catch(function(err) { //如果報(bào)錯(cuò)直接全局reload var status = module.hot.status(); if (["abort", "fail"].indexOf(status) >= 0) { log( "warning", "[HMR] Cannot apply update. Need to do a full reload!" ); log("warning", "[HMR] " + (err.stack || err.message)); window.location.reload(); } else { log("warning", "[HMR] Update failed: " + (err.stack || err.message)); } }); }; //獲取MyEmitter對(duì)象 var hotEmitter = require("./emitter"); //監(jiān)聽(tīng)‘webpackHotUpdate’事件 hotEmitter.on("webpackHotUpdate", function(currentHash) { lastHash = currentHash; //根據(jù)兩點(diǎn)判斷是否需要檢查modules以及更新 // 1 對(duì)比服務(wù)端傳過(guò)來(lái)的最新hash值和客戶(hù)端的__webpack_hash__是否一致 // 2 調(diào)用module.hot.status方法獲取狀態(tài) 是否為 ‘idle’ if (!upToDate() && module.hot.status() === "idle") { log("info", "[HMR] Checking for updates on the server..."); check(); } }); log("info", "[HMR] Waiting for update signal from WDS..."); } else { throw new Error("[HMR] Hot Module Replacement is disabled."); }
加載最新chunk的源碼分析:
方法調(diào)用關(guān)系:module.hot.check=>HotModuleReplacement=>hotDownloadManifest=>hotEnsureUpdateChunk=>hotDownloadUpdateChunk
HotModuleReplacement模塊內(nèi)容:
... function hotCheck(apply) { ... return hotDownloadManifest(hotRequestTimeout).then(function(update) { ... // 獲取到manifest后通過(guò)jsonp加載最新的chunk /*foreachInstalledChunks*/ // eslint-disable-next-line no-lone-blocks { /*globals chunkId */ hotEnsureUpdateChunk(chunkId); } ... }); } ... function hotEnsureUpdateChunk(chunkId) { if (!hotAvailableFilesMap[chunkId]) { hotWaitingFilesMap[chunkId] = true; } else { hotRequestedFilesMap[chunkId] = true; hotWaitingFiles++; hotDownloadUpdateChunk(chunkId); } }
webpack會(huì)根據(jù)不同運(yùn)行環(huán)境(node / webworker / web)調(diào)用對(duì)應(yīng)的方法(hotDownloadManifest & hotDownloadUpdateChunk)來(lái)加載chunk:,我們主要考慮web環(huán)境下的方法
// eslint-disable-next-line no-unused-vars function webpackHotUpdateCallback(chunkId, moreModules) { //... } //$semicolon // eslint-disable-next-line no-unused-vars //jsonp方法加載chunk function hotDownloadUpdateChunk(chunkId) { var script = document.createElement("script"); script.charset = "utf-8"; script.src = $require$.p + $hotChunkFilename$; if ($crossOriginLoading$) script.crossOrigin = $crossOriginLoading$; document.head.appendChild(script); } // eslint-disable-next-line no-unused-vars function hotDownloadManifest(requestTimeout) { //... }
log-apply-result模塊內(nèi)容:
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ module.exports = function(updatedModules, renewedModules) { //renewedModules表示哪些模塊被更新了,剩余的模塊表示,哪些模塊由于 ignoreDeclined,ignoreUnaccepted配置沒(méi)有更新 var unacceptedModules = updatedModules.filter(function(moduleId) { return renewedModules && renewedModules.indexOf(moduleId) < 0; }); var log = require("./log"); //哪些模塊無(wú)法HMR,打印log //哪些模塊由于某種原因沒(méi)有更新成功。其中沒(méi)有更新的原因可能是如下的: // ignoreUnaccepted // ignoreDecline // ignoreErrored if (unacceptedModules.length > 0) { log( "warning", "[HMR] The following modules couldn"t be hot updated: (They would need a full reload!)" ); unacceptedModules.forEach(function(moduleId) { log("warning", "[HMR] - " + moduleId); }); } //沒(méi)有模塊更新,表示模塊是最新的 if (!renewedModules || renewedModules.length === 0) { log("info", "[HMR] Nothing hot updated."); } else { log("info", "[HMR] Updated modules:"); //更新的模塊 renewedModules.forEach(function(moduleId) { if (typeof moduleId === "string" && moduleId.indexOf("!") !== -1) { var parts = moduleId.split("!"); log.groupCollapsed("info", "[HMR] - " + parts.pop()); log("info", "[HMR] - " + moduleId); log.groupEnd("info"); } else { log("info", "[HMR] - " + moduleId); } }); //每一個(gè)moduleId都是數(shù)字那么建議使用NamedModulesPlugin var numberIds = renewedModules.every(function(moduleId) { return typeof moduleId === "number"; }); if (numberIds) log( "info", "[HMR] Consider using the NamedModulesPlugin for module names." ); } };3. webpack-dev-server源碼解讀
webpack構(gòu)建過(guò)程觸發(fā)module更新的時(shí)機(jī)
webpack-dev-server客戶(hù)端源碼的關(guān)鍵部分
... ... const onSocketMsg = { ... ok() { sendMsg("Ok"); if (useWarningOverlay || useErrorOverlay) overlay.clear(); if (initial) return (initial = false); // eslint-disable-line no-return-assign reloadApp(); }, warnings(warnings) { log.warn("[WDS] Warnings while compiling."); const strippedWarnings = warnings.map((warning) => stripAnsi(warning)); sendMsg("Warnings", strippedWarnings); for (let i = 0; i < strippedWarnings.length; i++) { log.warn(strippedWarnings[i]); } if (useWarningOverlay) overlay.showMessage(warnings); if (initial) return (initial = false); // eslint-disable-line no-return-assign reloadApp(); }, ... }; ... function reloadApp() { if (isUnloading || !hotReload) { return; } if (hot) { log.info("[WDS] App hot update..."); // eslint-disable-next-line global-require const hotEmitter = require("webpack/hot/emitter"); hotEmitter.emit("webpackHotUpdate", currentHash); //重新啟動(dòng)webpack/hot/emitter,同時(shí)設(shè)置當(dāng)前hash if (typeof self !== "undefined" && self.window) { // broadcast update to window self.postMessage(`webpackHotUpdate${currentHash}`, "*"); } } else { //如果不是Hotupdate那么我們直接reload我們的window就可以了 let rootWindow = self; // use parent window for reload (in case we"re in an iframe with no valid src) const intervalId = self.setInterval(() => { if (rootWindow.location.protocol !== "about:") { // reload immediately if protocol is valid applyReload(rootWindow, intervalId); } else { rootWindow = rootWindow.parent; if (rootWindow.parent === rootWindow) { // if parent equals current window we"ve reached the root which would continue forever, so trigger a reload anyways applyReload(rootWindow, intervalId); } } }); } function applyReload(rootWindow, intervalId) { clearInterval(intervalId); log.info("[WDS] App updated. Reloading..."); rootWindow.location.reload(); }
根據(jù)以上代碼 可以看出:當(dāng)客戶(hù)端接收到服務(wù)器端發(fā)送的ok和warning信息的時(shí)候,同時(shí)支持HMR的情況下就會(huì)要求檢查更新,同時(shí)還收到服務(wù)器端本次編譯的hash值。我們?cè)倏匆幌路?wù)端是在什么時(shí)機(jī)發(fā)送’ok’和’warning’消息。
webpack-dev-server服務(wù)端源碼的關(guān)鍵部分
class Server { ... _sendStats(sockets, stats, force) { if ( !force && stats && (!stats.errors || stats.errors.length === 0) && stats.assets && //每一個(gè)asset都是沒(méi)有emitted屬性,表示沒(méi)有發(fā)生變化。如果發(fā)生變化那么這個(gè)assets肯定有emitted屬性 stats.assets.every((asset) => !asset.emitted) ) { return this.sockWrite(sockets, "still-ok"); } //設(shè)置hash this.sockWrite(sockets, "hash", stats.hash); if (stats.errors.length > 0) { this.sockWrite(sockets, "errors", stats.errors); } else if (stats.warnings.length > 0) { this.sockWrite(sockets, "warnings", stats.warnings); } else { this.sockWrite(sockets, "ok"); } } ... }
上面的代碼是發(fā)送‘ok’和‘warning’消息的方法,那具體在什么時(shí)機(jī)調(diào)用此方法呢?
class Server { constructor(compiler, options = {}, _log) { ... const addHooks = (compiler) => { const { compile, invalid, done } = compiler.hooks; compile.tap("webpack-dev-server", invalidPlugin); invalid.tap("webpack-dev-server", invalidPlugin); done.tap("webpack-dev-server", (stats) => { this._sendStats(this.sockets, stats.toJson(STATS)); this._stats = stats; }); }; if (compiler.compilers) { compiler.compilers.forEach(addHooks); } else { addHooks(compiler); } ... } ... }
再看這部分代碼,就可以理解了,每次在compiler的’done’鉤子函數(shù)(生命周期)被調(diào)用的時(shí)候就會(huì)通過(guò)socket向客戶(hù)端發(fā)送消息,要求客戶(hù)端去檢查模塊更新完成HMR工作。
總結(jié):最近正處于換工作階段,有些閑暇時(shí)間,正好拔草,之前工作的過(guò)程就很想看一下webpack-dev-server 的實(shí)現(xiàn)原理,趁此機(jī)會(huì)粗略學(xué)習(xí)了一下它的源碼,有什么不對(duì)的地方還望指教,互相學(xué)習(xí),加深理解。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/102738.html
摘要:在過(guò)程中會(huì)利用簡(jiǎn)稱(chēng)中的兩個(gè)方法和。是通過(guò)請(qǐng)求最新的模塊代碼,然后將代碼返回給,會(huì)根據(jù)返回的新模塊代碼做進(jìn)一步處理,可能是刷新頁(yè)面,也可能是對(duì)模塊進(jìn)行熱更新。該方法返回的就是最新值對(duì)應(yīng)的代碼塊。 Hot Module Replacement(簡(jiǎn)稱(chēng) HMR) 包含以下內(nèi)容: 熱更新圖 熱更新步驟講解 showImg(https://segmentfault.com/img/remote...
摘要:原文首次發(fā)表在的用法前言如果你有單獨(dú)的后端開(kāi)發(fā)服務(wù)器,并且希望在同域名下發(fā)送請(qǐng)求,那么代理某些會(huì)很有用。可以基于一個(gè)函數(shù)的返回值繞過(guò)代理。要為傳出連接綁定的本地接口字符串,默認(rèn)值將主機(jī)標(biāo)頭的原點(diǎn)更改為目標(biāo)參考官方文檔 原文首次發(fā)表在: Webpack-dev-server的proxy用法 前言 如果你有單獨(dú)的后端開(kāi)發(fā)服務(wù)器 API,并且希望在同域名下發(fā)送 API 請(qǐng)求 ,那么代理某...
摘要:馬上要出了,完全手寫(xiě)一個(gè)優(yōu)化后的腳手架是不可或缺的技能。每個(gè)依賴(lài)項(xiàng)隨即被處理,最后輸出到稱(chēng)之為的文件中,我們將在下一章節(jié)詳細(xì)討論這個(gè)過(guò)程。的事件流機(jī)制保證了插件的有序性,使得整個(gè)系統(tǒng)擴(kuò)展性很好。 webpack馬上要出5了,完全手寫(xiě)一個(gè)優(yōu)化后的腳手架是不可或缺的技能。 本文書(shū)寫(xiě)時(shí)間 2019年5月9日 , webpack版本 4.30.0最新版本 本人所有代碼均手寫(xiě),親自試驗(yàn)過(guò)可...
摘要:馬上要出了,完全手寫(xiě)一個(gè)優(yōu)化后的腳手架是不可或缺的技能。每個(gè)依賴(lài)項(xiàng)隨即被處理,最后輸出到稱(chēng)之為的文件中,我們將在下一章節(jié)詳細(xì)討論這個(gè)過(guò)程。的事件流機(jī)制保證了插件的有序性,使得整個(gè)系統(tǒng)擴(kuò)展性很好。 webpack馬上要出5了,完全手寫(xiě)一個(gè)優(yōu)化后的腳手架是不可或缺的技能。 本文書(shū)寫(xiě)時(shí)間 2019年5月9日 , webpack版本 4.30.0最新版本 本人所有代碼均手寫(xiě),親自試驗(yàn)過(guò)可...
閱讀 2850·2021-11-25 09:43
閱讀 2260·2021-11-24 09:39
閱讀 2219·2021-11-17 09:33
閱讀 2875·2021-09-27 14:11
閱讀 2045·2019-08-30 15:54
閱讀 3330·2019-08-26 18:27
閱讀 1339·2019-08-23 18:00
閱讀 1899·2019-08-23 17:53