一文搞懂 webpack HMR 原理

一文搞懂 webpack HMR 原理
關注「前端向後」微信公衆號,你將收穫一系列「用心原創」的高質量技術文章,主題包括但不限於前端、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

二.基本原理框架

一文搞懂 webpack HMR 原理
監聽到文件變化後,通知構建工具(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

然而,實際場景中模塊間通常存在多級依賴,替換一個模塊會影響(直接或間接)依賴到它的全部模塊:
一文搞懂 webpack HMR 原理

那豈不是要在全部模塊中都添一段相似的更新處理邏輯?

一般不須要,由於模塊更新事件有冒泡機制,未經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
從應用程序的角度來看,模塊替換過程以下:

  1. 應用程序要求 HMR Runtime 檢查更新

  2. HMR Runtime 異步下載更新並通知應用程序

  3. 應用程序要求 HMR Runtime 應用這些更新

  4. HMR Runtime 同步應用更新

接到(構建工具發來的)模塊更新通知後,HMR Runtime 向 Webpack Dev Server 查詢更新清單(manifest),接着下載每個更新模塊,全部新模塊下載完成後,準備就緒,進入應用階段

將更新清單中的全部模塊都標記爲失效,對於每個被標記爲失效的模塊,若是在當前模塊沒有發現accept事件處理,就向上冒泡,將其父模塊也標記失效,一直冒到應用入口模塊

以後全部失效模塊被釋放(dispose),並從模塊系統中卸載掉,最後更新模塊 hash 並調用全部相關accept事件處理函數

五.實現細節
實現上,應用程序在初始化時會與 Webpack Dev Server 創建 WebSocket 鏈接:

一文搞懂 webpack HMR 原理

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

相關文章
相關標籤/搜索