一.HMR
Hot Module Replacement(HMR)特性最早由 webpack 提供,能夠對執行時的 JavaScript 模組進行熱更新(無需重刷,即可替換、新增、刪除模組):
Hot Module Replacement (HMR) exchanges, adds, or removes modules while an application is running, without a full reload.
(摘自 Hot Module Replacement Concepts)
與整個重刷相比,模組級熱更新最大的意義在於能夠保留應用程式的目前執行時狀態,讓更加高效的 Hot Reloading 開發模式成為了可能
P.S.後來其他建置工具也實現了類似的機制,例如 Browserify、甚至 React Native Packager
可是,編輯原始碼產生的檔案變化在編譯時,替換模組實現在執行時,二者是怎樣聯繫起來的呢?
二.基本原理

監聽到檔案變化後,通知建置工具(HMR plugin),將發生變化的檔案(模組)發送給跑在應用程式裡的執行時框架(HMR Runtime),由執行時框架把這些模組塞進模組系統(新增/刪除,或替掉現有模組)
其中,HMR Runtime 是建置工具在編譯時注入的,透過統一的模組 ID 將編譯時的檔案與執行時的模組對應起來,並暴露出一系列 API 供應用層框架(如 React、Vue 等)對接
三.HMR API
最常用的是 accept:
module.hot.accept(dependencies, callback):監聽指定依賴模組的更新
例如:
import printMe from './print.js';
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
printMe();
})
}
觸發 accept(回呼)時,表示新模組已經塞進模組系統了,在此之後存取到的都是新模組實例
P.S.完整範例,見 Hot Module Replacement Guides
然而,實際場景中模組間一般存在多級依賴,替換一個模組會影響(直接或間接)依賴到它的所有模組:

那豈不是要在所有模組中都添一段類似的更新處理邏輯?
通常不需要,因為模組更新事件有冒泡機制,未經 accept 處理的更新事件會沿依賴鏈反向傳遞,只需要在一些重要的節點(比如 Router 組件)上集中處理即可
除 accept 外,還提供了:
-
module.hot.decline(dependencies):將依賴項標記為不可更新(期望整個重刷) -
module.hot.dispose/addDisposeHandler(data => {}):目前模組被替換時觸發,用來清理資源或(透過data參數)傳遞狀態給新模組 -
module.hot.invalidate():讓目前模組失效,用來強制更新目前模組 -
module.hot.removeDisposeHandler(callback):取消監聽模組替換事件
P.S.關於 webpack HMR API 的具體資訊,見 Hot Module Replacement API
四.HMR Runtime
從應用程式的角度來看,模組替換過程如下:
-
應用程式要求 HMR Runtime 檢查更新
-
HMR Runtime 非同步下載更新並通知應用程式
-
應用程式要求 HMR Runtime 應用這些更新
-
HMR Runtime 同步應用更新
接到(建置工具發來的)模組更新通知後,HMR Runtime 向 Webpack Dev Server 查詢更新清單(manifest),接著下載每一個更新模組,所有新模組下載完成後,準備就緒,進入應用階段
將更新清單中的所有模組都標記為失效,對於每一個被標記為失效的模組,如果在目前模組沒有發現 accept 事件處理,就向上冒泡,將其父模組也標記失效,一直冒到應用入口模組
之後所有失效模組被釋放(dispose),並從模組系統中卸載掉,最後更新模組 hash 並呼叫所有相關 accept 事件處理函數
五.實現細節
實現上,應用程式在初始化時會與 Webpack Dev Server 建立 WebSocket 連線:

Webpack Dev Server 向應用程式發出一系列訊息:
o
a["{"type":"log-level","data":"info"}"]
a["{\"type\":\"hot\"}"]
a["{"type":"liveReload"}"]
a["{"type":"hash","data":"411ae3e5f4bab84432bf"}"]
a["{"type":"ok"}"]
檔案內容發生變化時,Webpack Dev Server 會通知應用程式:
a["{"type":"invalid"}"]
a["{"type":"invalid"}"]
a["{"type":"hash","data":"a0b08ce32f8682379721"}"]
a["{"type":"ok"}"]
接著,HMR Runtime 發起 HTTP 請求獲取模組更新清單:
XHR GET http://localhost:8080/411ae3e5f4bab84432bf.hot-update.json
{"h":"a0b08ce32f8682379721","c":{"main":true}}
透過 script 標籤「下載」所有模組更新:
SCRIPT SRC http://localhost:8080/main.411ae3e5f4bab84432bf.hot-update.js
webpackHotUpdate("main", {
"./src/App.js": (function(module, __webpack_exports__, __webpack_require__) {
// (新的)檔案內容
})
})
如此這般,執行時的 HMR Runtime 順利拿到了編譯時的檔案變化,接下來將新模組塞進模組系統(modules 大表):
// insert new code
for (moduleId in appliedUpdate) {
if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
最後透過 accept 事件通知應用層使用新的模組進行「局部刷新」:
// call accept handlers
for (moduleId in outdatedDependencies) {
module = installedModules[moduleId];
if (module) {
moduleOutdatedDependencies = outdatedDependencies[moduleId];
var callbacks = [];
for (i = 0; i < moduleOutdatedDependencies.length; i++) {
dependency = moduleOutdatedDependencies[i];
cb = module.hot._acceptedDependencies[dependency];
if (cb) {
if (callbacks.indexOf(cb) !== -1) continue;
callbacks.push(cb);
}
}
for (i = 0; i < callbacks.length; i++) {
// 觸發accept模組更新事件
cb(moduleOutdatedDependencies);
}
}
}
至此,水落石出
暫無評論,快來發表你的看法吧