了不得的 Webpack HMR 學習指南(含源碼分析)

學習時間:2020.06.14
學習章節:《Webpack HMR 原理解析》
了不得的 Webpack HMR 學習指南.pngjavascript

1、HMR 介紹

Hot Module Replacement(如下簡稱:HMR 模塊熱替換)是 Webpack 提供的一個很是有用的功能,它容許在 JavaScript 運行時更新各類模塊,而無需徹底刷新html

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 主要經過如下幾種方式,來顯著加快開發速度:java

  • 保留在徹底從新加載頁面時丟失的應用程序狀態;
  • 只更新變動內容,以節省寶貴的開發時間;
  • 調整樣式更加快速 - 幾乎至關於在瀏覽器調試器中更改樣式。

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

2、HMR 使用方式

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

1. 方式一:使用 devServer

1.1 設置 devServer 選項

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

// 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 命令便可:github

// package.json

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

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

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

// webpack.config.js

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

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

// package.json

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

3. 簡單示例

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

模塊代碼以下實現:express

// 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" ,證實咱們監聽到文件更新了。
image.png


簡單 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,
}

3、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)調用。

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

4、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 保存下來,它將會用到後模塊熱更新
image.png

// 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 返回給瀏覽器端。
image.png
這邊涉及一些原生 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 。
image.png

而後 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();
    }
});

5、總結

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

參考文章

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

bg

相關文章
相關標籤/搜索