關注「前端向後」微信公衆號,你將收穫一系列「用心原創」的高質量技術文章,主題包括但不限於前端、Node.js以及服務端技術前端
一.HMR
Hot Module Replacement(HMR)特性最先由 webpack 提供,可以對運行時的 JavaScript 模塊進行熱更新(無需重刷,便可替換、新增、刪除模塊):webpack
Hot Module Replacement (HMR) exchanges, adds, or removes modules while an application is running, without a full reload.
(摘自Hot Module Replacement Concepts)web
與整個重刷相比,模塊級熱更新最大的意義在於可以保留應用程序的當前運行時狀態,讓更加高效的Hot Reloading開發模式成爲了可能json
P.S.後來其它構建工具也實現了相似的機制,例如Browserify、甚至React Native Packager微信
但是,編輯源碼產生的文件變化在編譯時,替換模塊實如今運行時,兩者是怎樣聯繫起來的呢?app
二.基本原理框架
監聽到文件變化後,通知構建工具(HMR plugin),將發生變化的文件(模塊)發送給跑在應用程序裏的運行時框架(HMR Runtime),由運行時框架把這些模塊塞進模塊系統(新增/刪除,或替掉現有模塊)異步
其中,HMR Runtime 是構建工具在編譯時注入的,經過統一的模塊 ID 將編譯時的文件與運行時的模塊對應起來,並暴露出一系列 API 供應用層框架(如 React、Vue 等)對接ide
三.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():讓當前模塊失效,用來強制更新當前模塊
P.S.關於 webpack HMR API 的具體信息,見Hot Module Replacement API
四.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); } } }
至此,水落石出
參考資料
What exactly is Hot Module Replacement in Webpack?
Understanding webpack HMR beyond the docs
Introducing Hot Reloading