跳到主要內容
黯羽輕揚每天積累一點點

webpack HMR

免費2020-05-24#Tool#webpack hot module replacement#webpack hot reload#webpack hot#HMR#热模块替换

HMR 是什麼,有什麼用?

一.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

從應用程式的角度來看,模組替換過程如下:

  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 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);
    }
  }
}

至此,水落石出

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論