【Webpack】627- 了不起的 Webpack HMR 學習指南(含源碼分析)

學習時間:2020.06.14
學習章節:《Webpack HMR 原理解析》

一、HMR 介紹

Hot Module Replacement(以下簡稱:HMR 模塊熱替換)是 Webpack 提供的一個非常有用的功能,它允許在 JavaScript 運行時更新各種模塊,而無需完全刷新

Hot Module Replacement (or HMR) is one of the most useful features offered by webpack. It allows all kinds of modules to be updated at runtime without the need for a full refresh. --《Hot Module Replacement》

當我們修改代碼並保存後,Webpack 將對代碼重新打包,HMR 會在應用程序運行過程中替換、添加或刪除模塊,而無需重新加載整個頁面。
HMR 主要通過以下幾種方式,來顯著加快開發速度:

  • 保留在完全重新加載頁面時丟失的應用程序狀態;

  • 只更新變更內容,以節省寶貴的開發時間;

  • 調整樣式更加快速 - 幾乎相當於在瀏覽器調試器中更改樣式。

需要注意:HMR 不適用於生產環境,這意味着它應當只在開發環境使用。

二、HMR 使用方式

在 Webpack 中啓用 HMR 功能比較簡單:

1. 方式一:使用 devServer

1.1 設置 devServer 選項

只需要在 webpack.config.js 中添加 devServer 選項,並設置 hot 值爲 true ,並使用HotModuleReplacementPlugin 和 NamedModulesPlugin (可選)兩個 Plugins :

// webpack.config.js

const path = require('path')
const webpack = require('webpack')
module.exports = {
 entry: './index.js',
 output: {
  filename: 'bundle.js',
  path: path.join(__dirname, '/')
 },
+ devServer: {
+  hot: true,   // 啓動模塊熱更新 HMR
+   open: true,  // 開啓自動打開瀏覽器頁面
+ },
  plugins: [
+   new webpack.NamedModulesPlugin(),
+   new webpack.HotModuleReplacementPlugin()
  ]
}

1.2 添加 scripts

然後在 package.json 中爲 scripts 命令即可:

// package.json

{
  // ...
  "scripts": {
+    "start": "webpack-dev-server"
  },
  // ...
}

2. 方式二、使用命令行參數

另一種是通過添加 --hot 參數來實現。添加 --hot 參數後,devServer 會告訴 Webpack 自動引入 HotModuleReplacementPlugin ,而不需要我們手動引入。
另外常常也搭配 --open 來自動打開瀏覽器到頁面。
這裏移除掉前面添加的兩個 Plugins :

// webpack.config.js

const path = require('path')
const webpack = require('webpack')
module.exports = {
 // ...
- plugins: [
-   new webpack.NamedModulesPlugin(),
-   new webpack.HotModuleReplacementPlugin()
- ]
}

然後修改 package.json 文件中的 scripts 配置:

// package.json

{
  // ...
  "scripts": {
-    "start": "webpack-dev-server"
+    "start": "webpack-dev-server --hot --open"
  },
  // ...
}

3. 簡單示例

基於上述配置,我們簡單實現一個場景:index.js 文件中導入 hello.js 模塊,當 hello.js 模塊發生變化時, index.js 將更新模塊。
模塊代碼如下實現:

// hello.js
export default () => 'hi leo!';

// index.js
import hello from './hello.js'
const div = document.createElement('div');
div.innerHTML = hello();

document.body.appendChild(div);

然後在 index.html 中導入打包後的 JS 文件,並執行 npm start 運行項目:

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
</head>
<body>
 <div>了不起的 Webpack HMR 學習指南</div>
 <script src="bundle.js"></script>
</body>
</html>

4. 實現監聽更新

當我們通過 HotModuleReplacementPlugin  插件啓用了 HMR,則它的接口將被暴露在全局 module.hot  屬性下面。通常,可以先檢查這個接口是否可訪問,然後再開始使用它。
舉個例子,你可以這樣 accept  一個更新的模塊:

if (module.hot) {
  module.hot.accept('./library.js', function() {
    // 使用更新過的 library 模塊執行某些操作...
  })
}

關於 module.hot 更多 API ,可以查看官方文檔《Hot Module Replacement API》 。
回到上面示例,我們測試更新模塊的功能。
這時我們修改 index.js 代碼,來監聽 hello.js 模塊中的更新:

import hello from './hello.js';
const div = document.createElement('div');
div.innerHTML = hello();
document.body.appendChild(div);

+ if (module.hot) {
+   module.hot.accept('./hello.js', function() {
+     console.log('現在在更新 hello 模塊了~');
+     div.innerHTML = hello();
+   })
+ }

然後修改 hello.js 文件內容,測試效果:

- export default () => 'hi leo!';
+ export default () => 'hi leo! hello world';

當我們保存代碼時,控制檯輸出 "現在在更新 hello模塊了~" ,並且頁面中 "hi leo!" 也更新爲 "hi leo! hello world" ,證明我們監聽到文件更新了。


簡單 Webpack HMR 使用方式就介紹到這,更多介紹,還請閱讀官方文檔《Hot Module Replacement》。

5. devServer 常用配置和技巧

5.1 常用配置

根據目錄結構的不同,contentBaseopenPage 參數要配置合適的值,否則運行時應該不會立刻訪問到你的首頁。同時要注意你的 publicPath,靜態資源打包後生成的路徑是一個需要思考的點,取決於你的目錄結構。

devServer: {
  contentBase: path.join(__dirname, 'static'),    // 告訴服務器從哪裏提供內容(默認當前工作目錄)
  openPage: 'views/index.html',  // 指定默認啓動瀏覽器時打開的頁面
  index: 'views/index.html',  // 指定首頁位置
  watchContentBase: true, // contentBase下文件變動將reload頁面(默認false)
  host: 'localhost', // 默認localhost,想外部可訪問用'0.0.0.0'
  port: 8080, // 默認8080
  inline: true, // 可以監控js變化
  hot: true, // 熱啓動
  open: true, // 啓動時自動打開瀏覽器(指定打開chrome,open: 'Google Chrome')
  compress: true, // 一切服務都啓用gzip 壓縮
  disableHostCheck: true, // true:不進行host檢查
  quiet: false,
  https: false,
  clientLogLevel: 'none',
  stats: { // 設置控制檯的提示信息
    chunks: false,
    children: false,
    modules: false,
    entrypoints: false, // 是否輸出入口信息
    warnings: false,
    performance: false, // 是否輸出webpack建議(如文件體積大小)
  },
  historyApiFallback: {
    disableDotRule: true,
  },
  watchOptions: {
    ignored: /node_modules/, // 略過node_modules目錄
  },
  proxy: { // 接口代理(這段配置更推薦:寫到package.json,再引入到這裏)
    "/api-dev": {
      "target": "http://api.test.xxx.com",
      "secure": false,
      "changeOrigin": true,
      "pathRewrite": { // 將url上的某段重寫(例如此處是將 api-dev 替換成了空)
        "^/api-dev": ""
      }
    }
  },
  before(app) { },
}

5.2 技巧1:文件形式輸出 dev-server 代碼

dev-server 輸出的代碼通常在內存中,但也可以寫入硬盤,產出實體文件:

devServer:{
  writeToDisk: true,
}

通常可以用於代理映射文件調試,編譯時會產出許多帶 hash 的 js 文件,不帶 hash 的文件同樣也是實時編譯的

5.3 技巧2:默認使用本地 IP 啓動服務

有的時候,啓動服務時,想要默認使用本地的 ip 地址打開:

devServer:{
  disableHostCheck: true, // true:不進行host檢查
  // useLocalIp: true, // 建議不在這裏配置
  // host: '0.0.0.0', // 建議不在這裏配置
}

同時還需要將 host 配置爲 0.0.0.0,這個配置建議在 scripts 命令中追加,而非在配置中寫死,否則將來不想要這種方式往回改折騰,取巧一點,配個新命令:

"dev-ip": "yarn run dev --host 0.0.0.0 --useLocalIp"

5.4 技巧3:指定啓動的調試域名

有時啓動的時候希望是指定的調試域名,例如:local.test.baidu.com

devServer:{
  open: true,
  public: 'local.test.baidu.com:8080', // 需要帶上端口
  port: 8080,
}

同時需要將 127.0.0.1 修改爲指定的 host,可以藉助 iHost 等工具去修改,各個工具大同小異,格式如下:

127.0.0.1 local.test.baidu.com

服務啓動後將自動打開 local.test.baidu.com:8080 訪問

5.5 技巧4:啓動 gzip 壓縮

devServer:{
  compress: true,
}

三、HMR 基本原理介紹

從前面介紹中,我們知道:HMR 主要功能是會在應用程序運行過程中替換、添加或刪除模塊,而無需重新加載整個頁面
那麼,Webpack 編譯源碼所產生的文件變化在編譯時,替換模塊實現在運行時,兩者如何聯繫起來?

帶着這兩個問題,我們先簡單看下 HMR 核心工作流程(簡化版):

HMR 工作流程圖.png

接下來開始 HMR 工作流程分析:

  1. 當 Webpack(Watchman) 監聽到項目中的文件/模塊代碼發生變化後,將變化通知 Webpack 中的構建工具(Packager)即 HMR Plugin;

  2. 然後經過 HMR Plugin 處理後,將結果發送到應用程序(Application)的運行時框架(HMR Runtime);

  3. 最後由 HMR Runtime 將這些發生變化的文件/模塊更新(新增/刪除或替換)到模塊系統中。

其中,HMR Runtime 是構建工具在編譯時注入的,通過統一的 Module ID 將編譯時的文件與運行時的模塊對應起來,並且對外提供一系列 API 供應用層框架(如 React)調用。

????注意????:建議先理解上面這張圖的大致流程,在進行後續閱讀。放心,我等着大家~????

四、HMR 完整原理和源碼分析

通過上一節內容,我們大概知道 HMR 簡單工作流程,那麼或許你現在可能還有很多疑惑:文件更新是什麼通知 HMR Plugin?HMR Plugin 怎麼發送更新到 HMR Runtime?等等問題。

那麼接下來我們開始詳細結合源碼分析整個 HMR 模塊熱更新流程,首先還是先看流程圖,可以先不瞭解圖中方法名稱(紅色字體黃色背景色部分):

Webpack HMR.png

上圖展示了從我們修改代碼,到模塊熱更新完成的一個 HMR 完整工作流程,圖中已用紅色阿拉伯數字符號將流程標識出來。

要了解上面工作原理,我們先理解圖中這幾個名稱概念:

  • Webpack-dev-server :一個服務器插件,相當於 express 服務器,啓動一個 Web 服務,只適用於開發環境;

  • Webpack-dev-middleware :一個 Webpack-dev-server 的中間件,作用簡單總結爲:通過watch mode,監聽資源的變更,然後自動打包。

  • Webpack-hot-middleware :結合 Webpack-dev-middleware 使用的中間件,它可以實現瀏覽器的無刷新更新,也就是 HMR;

下面一起學習 HMR 整個工作原理吧:

1.監控代碼變化,重新編譯打包

首先根據 devServer 配置,使用 npm start 將啓動 Webpack-dev-server 啓動本地服務器並進入 Webpack 的 watch 模式,然後初始化 Webpack-dev-middleware ,在 Webpack-dev-middleware 中通過調用 startWatch() 方法對文件系統進行 watch:

// webpack-dev-server\bin\webpack-dev-server.js
// 1.啓動本地服務器 Line 386
server = new Server(compiler, options);

// webpack-dev-server\lib\Server.js
// 2.初始化 Webpack-dev-middleware Line 109
this.middleware = webpackDevMiddleware(compiler, Object.assign({}, options, wdmOptions));

// webpack-dev-middleware\lib\Shared.js
// 3.開始 watch 文件系統 Line 171
startWatch: function() {
 //...
 // start watching
 if(!options.lazy) {
  var watching = compiler.watch(options.watchOptions, share.handleCompilerCallback);
  context.watching = watching;
 }
 //...
}
share.startWatch();
// ...

startWatch() 方法執行後,便進入 watch 模式,若發現文件中代碼發生修改,則根據配置文件對模塊重新編譯打包

2.保存編譯結果

Webpack 與  Webpack-dev-middleware 交互,Webpack-dev-middleware 調用 Webpack 的 API 對代碼變化進行監控,並通知 Webpack 將重新編譯的代碼通過 JavaScript 對象保存在內存中

我們會發現,在 output.path 指定的 dist 目錄並沒有保存編譯結果的文件,這是爲什麼?

其實, Webpack 將編譯結果保存在內存中,因爲訪問內存中的代碼比訪問文件系統中的文件快,這樣可以減少代碼寫入文件的開銷。

Webpack 能將代碼保存到內存中,需要歸功於 Webpack-dev-middleware 的 memory-fs 依賴庫,它將原本 outputFileSystem  替換成了 MemoryFileSystem  的實例,便實現代碼輸出到內存中。其中部分源碼如下:

// webpack-dev-middleware\lib\Shared.js Line 108

// store our files in memory
var fs;
var isMemoryFs = !compiler.compilers && 
    compiler.outputFileSystem instanceof MemoryFileSystem;
if(isMemoryFs) {
 fs = compiler.outputFileSystem;
} else {
 fs = compiler.outputFileSystem = new MemoryFileSystem();
}
context.fs = fs;

上述代碼先判斷 fileSystem 是否是 MemoryFileSystem 的實例,若不是,則用 MemoryFileSystem 的實例替換 compiler 之前的 outputFileSystem。這樣 bundle.js 文件代碼就作爲一個簡單 JavaScript 對象保存在內存中,當瀏覽器請求 bundle.js 文件時,devServer 就直接去內存中找到上面保存的 JavaScript 對象並返回給瀏覽器端。

3.監控文件變化,刷新瀏覽器

Webpack-dev-server 開始監控文件變化,與第 1 步不同的是,這裏並不是監控代碼變化重新編譯打包。
當我們在配置文件中配置了 devServer.watchContentBase 爲 true ,Webpack-dev-server 會監聽配置文件夾中靜態文件的變化,發生變化時,通知瀏覽器端對應用進行瀏覽器刷新,這與 HMR 不一樣。

// webpack-dev-server\lib\Server.js
// 1. 讀取參數 Line 385
if (options.watchContentBase) { defaultFeatures.push('watchContentBase'); }

// 2. 定義 _watch 方法 Line 697
Server.prototype._watch = function (watchPath) {
 // ...
  const watcher = chokidar.watch(watchPath, options).on('change', () => {
    this.sockWrite(this.sockets, 'content-changed');
  });

  this.contentBaseWatchers.push(watcher);
};

// 3. 執行 _watch() 監聽文件變化 Line 339
watchContentBase: () => {
    if (/^(https?:)?\/\//.test(contentBase) || typeof contentBase === 'number') {
        throw new Error('Watching remote files is not supported.');
    } else if (Array.isArray(contentBase)) {
        contentBase.forEach((item) => {
            this._watch(item);
        });
    } else {
        this._watch(contentBase);
    }
}

4.建立 WS,同步編譯階段狀態

這一步都是 Webpack-dev-server 中處理,主要通過 sockjs(Webpack-dev-server 的依賴),在 Webpack-dev-server 的瀏覽器端(Client)和服務器端(Webpack-dev-middleware)之間建立 WebSocket 長連接

然後將 Webpack 編譯打包的各個階段狀態信息同步到瀏覽器端。其中有兩個重要步驟:

  • 發送狀態

Webpack-dev-server 通過 Webpack API 監聽 compile 的 done 事件,當 compile 完成後,Webpack-dev-server 通過 _sendStats 方法將編譯後新模塊的 hash 值用 socket 發送給瀏覽器端。

  • 保存狀態

瀏覽器端將_sendStats 發送過來的 hash 保存下來,它將會用到後模塊熱更新

// webpack-dev-server\lib\Server.js

// 1. 定義 _sendStats 方法 Line 685
// send stats to a socket or multiple sockets
Server.prototype._sendStats = function (sockets, stats, force) {
  //...
  this.sockWrite(sockets, 'hash', stats.hash);
};

// 2. 監聽 done 事件 Line 86
compiler.plugin('done', (stats) => {
   // 將最新打包文件的 hash 值(stats.hash)作爲參數傳入 _sendStats()
    this._sendStats(this.sockets, stats.toJson(clientStats));
    this._stats = stats;
});

// webpack-dev-server\client\index.js
// 3. 保存 hash 值 Line 74
var onSocketMsg = {
  // ...
  hash: function hash(_hash) {
    currentHash = _hash;
  },
  // ...
}
socket(socketUrl, onSocketMsg);

5.瀏覽器端發佈消息

hash 消息發送完成後,socket 還會發送一條 ok 的消息告知 Webpack-dev-server,由於客戶端(Client)並不請求熱更新代碼,也不執行熱更新模塊操作,因此通過 emit 一個 "webpackHotUpdate" 消息,將工作轉交回 Webpack。

// webpack-dev-server\client\index.js
// 1. 處理 ok 消息 Line 135
var onSocketMsg = {
  // ...
  ok: function ok() {
      sendMsg('Ok');
      if (useWarningOverlay || useErrorOverlay) overlay.clear();
      if (initial) return initial = false; // eslint-disable-line no-return-assign
      reloadApp();
  },
  // ...
}

// 2. 處理刷新 APP Line 218
function reloadApp() {
  // ...
  if (_hot) {
    // 動態加載 emitter
    var hotEmitter = require('webpack/hot/emitter');
    hotEmitter.emit('webpackHotUpdate', currentHash);
    if (typeof self !== 'undefined' && self.window) {
      // broadcast update to window
      self.postMessage('webpackHotUpdate' + currentHash, '*');
    }
  }
  // ...
}

6.傳遞 hash 到 HMR

Webpack/hot/dev-server 監聽瀏覽器端 webpackHotUpdate 消息,將新模塊 hash 值傳到客戶端 HMR 核心中樞的 HotModuleReplacement.runtime ,並調用 check 方法檢測更新,判斷是瀏覽器刷新還是模塊熱更新。如果是瀏覽器刷新的話,則沒有後續步驟咯~~

// webpack\hot\dev-server.js
// 1.監聽 webpackHotUpdate Line 42
var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate", function(currentHash) {
    lastHash = currentHash;
    if(!upToDate() && module.hot.status() === "idle") {
        log("info", "[HMR] Checking for updates on the server...");
        check();
    }
});

var check = function check() {
    module.hot.check(true).then(function(updatedModules) {
        if(!updatedModules) {
            // ...
      window.location.reload();// 瀏覽器刷新
            return;
        }
        if(!upToDate()) {
            check();
        }
    }).catch(function(err) { /*...*/});
};

// webpack\lib\HotModuleReplacement.runtime.js
// 3.調用 HotModuleReplacement.runtime 定義的 check 方法 Line 167
function hotCheck(apply) {
    if(hotStatus !== "idle") throw new Error("check() is only allowed in idle status");
    hotApplyOnUpdate = apply;
    hotSetStatus("check");
    return hotDownloadManifest(hotRequestTimeout).then(function(update) {
    //...
    });
}

7.檢測是否存在更新

當 HotModuleReplacement.runtime 調用 check 方法時,會調用 JsonpMainTemplate.runtime 中的 hotDownloadUpdateChunk (獲取最新模塊代碼)和 hotDownloadManifest (獲取是否有更新文件)兩個方法,這兩個方法的源碼,在下一步展開。

// webpack\lib\HotModuleReplacement.runtime.js
// 1.調用 HotModuleReplacement.runtime 定義 hotDownloadUpdateChunk 方法 Line 171
function hotCheck(apply) {
    if(hotStatus !== "idle") throw new Error("check() is only allowed in idle status");
    hotApplyOnUpdate = apply;
    hotSetStatus("check");
    return hotDownloadManifest(hotRequestTimeout).then(function(update) {
    //...
        {
          // hotEnsureUpdateChunk 方法中會調用 hotDownloadUpdateChunk
          hotEnsureUpdateChunk(chunkId);
        }
    });
}

其中 hotEnsureUpdateChunk 方法中會調用 hotDownloadUpdateChunk

// webpack\lib\HotModuleReplacement.runtime.js Line 215
 function hotEnsureUpdateChunk(chunkId) {
  if(!hotAvailableFilesMap[chunkId]) {
   hotWaitingFilesMap[chunkId] = true;
  } else {
   hotRequestedFilesMap[chunkId] = true;
   hotWaitingFiles++;
   hotDownloadUpdateChunk(chunkId);
  }
 }

8.請求更新最新文件列表

在調用 check 方法時,會先調用 JsonpMainTemplate.runtime 中的 hotDownloadManifest 方法, 通過向服務端發起 AJAX 請求獲取是否有更新文件,如果有的話將 mainfest 返回給瀏覽器端。這邊涉及一些原生 XMLHttpRequest,就不全部貼出了~

// webpack\lib\JsonpMainTemplate.runtime.js
// hotDownloadManifest 定義 Line 22
function hotDownloadManifest(requestTimeout) {
    return new Promise(function(resolve, reject) {
        try {
            var request = new XMLHttpRequest();
            var requestPath = $require$.p + $hotMainFilename$;
            request.open("GET", requestPath, true);
            request.timeout = requestTimeout;
            request.send(null);
        } catch(err) {
            return reject(err);
        }
        request.onreadystatechange = function() {
            // ...
        };
    });
}

9.請求更新最新模塊代碼

在  hotDownloadManifest 方法中,還會執行  hotDownloadUpdateChunk 方法,通過 JSONP 請求最新的模塊代碼,並將代碼返回給 HMR runtime 。

然後 HMR runtime 會將新代碼進一步處理,判斷是瀏覽器刷新還是模塊熱更新

// webpack\lib\JsonpMainTemplate.runtime.js
// hotDownloadManifest 定義 Line 12
function hotDownloadUpdateChunk(chunkId) {
  // 創建 script 標籤,發起 JSONP 請求
    var head = document.getElementsByTagName("head")[0];
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.charset = "utf-8";
    script.src = $require$.p + $hotChunkFilename$;
    $crossOriginLoading$;
    head.appendChild(script);
}

10.更新模塊和依賴引用

這一步是整個模塊熱更新(HMR)的核心步驟,通過 HMR runtime 的 hotApply 方法,移除過期模塊和代碼,並添加新的模塊和代碼實現熱更新。

hotApply 方法可以看出,模塊熱替換主要分三個階段:

  1. 找出過期模塊 outdatedModules 和過期依賴 outdatedDependencies

// webpack\lib\HotModuleReplacement.runtime.js
// 找出 outdatedModules 和 outdatedDependencies Line 342
function hotApply() { 
  // ...
  var outdatedDependencies = {};
  var outdatedModules = [];
  function getAffectedStuff(updateModuleId) {
    var outdatedModules = [updateModuleId];
    var outdatedDependencies = {};
    // ...
    return {
        type: "accepted",
        moduleId: updateModuleId,
        outdatedModules: outdatedModules,
        outdatedDependencies: outdatedDependencies
    };
 };
  function addAllToSet(a, b) {
      for (var i = 0; i < b.length; i++) {
          var item = b[i];
          if (a.indexOf(item) < 0)
              a.push(item);
      }
  }
  for(var id in hotUpdate) {
      if(Object.prototype.hasOwnProperty.call(hotUpdate, id)) {
          // ... 省略多餘代碼
          if(hotUpdate[id]) {
              result = getAffectedStuff(moduleId);
          }
          if(doApply) {
              for(moduleId in result.outdatedDependencies) {
                 // 添加到 outdatedDependencies
                  addAllToSet(outdatedDependencies[moduleId], result.outdatedDependencies[moduleId]);
              }
          }
          if(doDispose) {
              // 添加到 outdatedModules
              addAllToSet(outdatedModules, [result.moduleId]);
              appliedUpdate[moduleId] = warnUnexpectedRequire;
          }
      }
  }
}
  1. 從緩存中刪除過期模塊、依賴和所有子元素的引用;

// webpack\lib\HotModuleReplacement.runtime.js
// 從緩存中刪除過期模塊、依賴和所有子元素的引用 Line 442
function hotApply() {
   // ...
    var idx;
    var queue = outdatedModules.slice();
    while(queue.length > 0) {
        moduleId = queue.pop();
        module = installedModules[moduleId];
        // ...
        // 移除緩存中的模塊
        delete installedModules[moduleId];
        // 移除過期依賴中不需要使用的處理方法
        delete outdatedDependencies[moduleId];
        // 移除所有子元素的引用
        for(j = 0; j < module.children.length; j++) {
            var child = installedModules[module.children[j]];
            if(!child) continue;
            idx = child.parents.indexOf(moduleId);
            if(idx >= 0) {
                child.parents.splice(idx, 1);
            }
        }
    } 
  // 從模塊子組件中刪除過時的依賴項
  var dependency;
  var moduleOutdatedDependencies;
  for(moduleId in outdatedDependencies) {
   if(Object.prototype.hasOwnProperty.call(outdatedDependencies, moduleId)) {
    module = installedModules[moduleId];
    if(module) {
     moduleOutdatedDependencies = outdatedDependencies[moduleId];
     for(j = 0; j < moduleOutdatedDependencies.length; j++) {
      dependency = moduleOutdatedDependencies[j];
      idx = module.children.indexOf(dependency);
      if(idx >= 0) module.children.splice(idx, 1);
     }
    }
   }
  }
}
  1. 將新模塊代碼添加到 modules 中,當下次調用 __webpack_require__  (webpack 重寫的 require  方法)方法的時候,就是獲取到了新的模塊代碼了。

// webpack\lib\HotModuleReplacement.runtime.js
// 將新模塊代碼添加到 modules 中 Line 501
function hotApply() {
   // ...
    for(moduleId in appliedUpdate) {
        if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
            modules[moduleId] = appliedUpdate[moduleId];
        }
    }
}

hotApply 方法執行之後,新代碼已經替換舊代碼,但是我們業務代碼並不知道這些變化,因此需要通過 accept事件通知應用層使用新的模塊進行“局部刷新”,我們在業務中是這麼使用:


if (module.hot) {
  module.hot.accept('./library.js', function() {
    // 使用更新過的 library 模塊執行某些操作...
  })
}

11.熱更新錯誤處理

在熱更新過程中,hotApply 過程中可能出現 abort 或者 fail 錯誤,則熱更新退回到刷新瀏覽器(Browser Reload),整個模塊熱更新完成。

// webpack\hot\dev-server.js Line 13
module.hot.check(true).then(function (updatedModules) {
    if (!updatedModules) {
        return window.location.reload();
    }
    // ...
}).catch(function (err) {
    var status = module.hot.status();
    if (["abort", "fail"].indexOf(status) >= 0) {
        window.location.reload();
    }
});

五、總結

本文主要和大家分享 Webpack 的 HMR 使用和實現原理及源碼分析,在源碼分析中,通過一張“Webpack HMR 工作原理解析”圖讓大家對 HMR 整個工作流程有所瞭解,HMR 本身源碼內容較多,許多細節之處本文沒有完整寫出,需要各位讀者自己慢慢閱讀和理解源碼。

參考文章

1.官方文檔《Hot Module Replacement》
2.《Webpack HMR 原理解析》
3.《webpack HMR》 
4.《配置 dev-server》

1. JavaScript 重溫系列(22篇全)

2. ECMAScript 重溫系列(10篇全)

3. JavaScript設計模式 重溫系列(9篇全)

4. 正則 / 框架 / 算法等 重溫系列(16篇全)

5. Webpack4 入門(上)|| Webpack4 入門(下)

6. MobX 入門(上) ||  MobX 入門(下)

7. 59篇原創系列彙總

回覆“加羣”與大佬們一起交流學習~

點擊“閱讀原文”查看70+篇原創文章

點這,與大家一起分享本文吧~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章