自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

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

開發(fā) 前端
本文主要和大家分享 Webpack 的 HMR 使用和實現(xiàn)原理及源碼分析。

一、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 會在應(yīng)用程序運行過程中替換、添加或刪除模塊,而無需重新加載整個頁面。

HMR 主要通過以下幾種方式,來顯著加快開發(fā)速度:

  •  保留在完全重新加載頁面時丟失的應(yīng)用程序狀態(tài);
  •  只更新變更內(nèi)容,以節(jié)省寶貴的開發(fā)時間;
  •  調(diào)整樣式更加快速 - 幾乎相當于在瀏覽器調(diào)試器中更改樣式。

需要注意:HMR 不適用于生產(chǎn)環(huán)境,這意味著它應(yīng)當只在開發(fā)環(huán)境使用。

二、HMR 使用方式

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

1. 方式一:使用 devServer

1.1 設(shè)置 devServer 選項

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

  1. // webpack.config.js  
  2. const path = require('path')  
  3. const webpack = require('webpack')  
  4. module.exports = {  
  5.  entry: './index.js',  
  6.  output: {  
  7.   filename: 'bundle.js',  
  8.   path: path.join(__dirname, '/')  
  9.  },  
  10. + devServer: {  
  11. +  hot: true,   // 啟動模塊熱更新 HMR  
  12. +   open: true,  // 開啟自動打開瀏覽器頁面  
  13. + },  
  14.   plugins: [  
  15. +   new webpack.NamedModulesPlugin(),  
  16. +   new webpack.HotModuleReplacementPlugin()  
  17.   ]  

1.2 添加 scripts

然后在 package.json 中為 scripts 命令即可: 

  1. // package.json  
  2.  
  3.   // ...  
  4.   "scripts": {  
  5. +    "start": "webpack-dev-server"  
  6.   },  
  7.   // ...  

2. 方式二、使用命令行參數(shù)

另一種是通過添加 --hot 參數(shù)來實現(xiàn)。添加 --hot 參數(shù)后,devServer 會告訴 Webpack 自動引入 HotModuleReplacementPlugin ,而不需要我們手動引入。

另外常常也搭配 --open 來自動打開瀏覽器到頁面。

這里移除掉前面添加的兩個 Plugins : 

  1. // webpack.config.js  
  2. const path = require('path')  
  3. const webpack = require('webpack')  
  4. module.exports = {  
  5.  // ...  
  6. - plugins: [  
  7. -   new webpack.NamedModulesPlugin(),  
  8. -   new webpack.HotModuleReplacementPlugin()  
  9. - ]  

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

  1. // package.json  
  2.  
  3.   // ...  
  4.   "scripts": {  
  5. -    "start": "webpack-dev-server"  
  6. +    "start": "webpack-dev-server --hot --open"  
  7.   },  
  8.   // ...  

3. 簡單示例

基于上述配置,我們簡單實現(xiàn)一個場景:index.js 文件中導(dǎo)入 hello.js 模塊,當 hello.js 模塊發(fā)生變化時, index.js 將更新模塊。

模塊代碼如下實現(xiàn): 

  1. // hello.js  
  2. export default () => 'hi leo!';  
  3. // index.js  
  4. import hello from './hello.js'  
  5. const div = document.createElement('div');  
  6. div.innerHTML = hello(); 
  7. document.body.appendChild(div); 

然后在 index.html 中導(dǎo)入打包后的 JS 文件,并執(zhí)行 npm start 運行項目: 

  1. <!DOCTYPE html>  
  2. <html lang="en">  
  3. <head>  
  4.  <meta charset="UTF-8">  
  5. </head>  
  6. <body>  
  7.  <div>了不起的 Webpack HMR 學(xué)習指南</div>  
  8.  <script src="bundle.js"></script>  
  9. </body>  
  10. </html> 

4. 實現(xiàn)監(jiān)聽更新

當我們通過 HotModuleReplacementPlugin  插件啟用了 HMR,則它的接口將被暴露在全局 module.hot  屬性下面。通常,可以先檢查這個接口是否可訪問,然后再開始使用它。

舉個例子,你可以這樣 accept  一個更新的模塊: 

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

關(guān)于 module.hot 更多 API ,可以查看官方文檔《Hot Module Replacement API》 。

回到上面示例,我們測試更新模塊的功能。

這時我們修改 index.js 代碼,來監(jiān)聽 hello.js 模塊中的更新: 

  1. import hello from './hello.js';  
  2. const div = document.createElement('div');  
  3. div.innerHTML = hello();  
  4. document.body.appendChild(div);  
  5. + if (module.hot) {  
  6. +   module.hot.accept('./hello.js', function() {  
  7. +     console.log('現(xiàn)在在更新 hello 模塊了~');  
  8. +     div.innerHTML = hello();  
  9. +   })  
  10. + } 

然后修改 hello.js 文件內(nèi)容,測試效果: 

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

當我們保存代碼時,控制臺輸出 "現(xiàn)在在更新 hello模塊了~" ,并且頁面中 "hi leo!" 也更新為 "hi leo! hello world" ,證明我們監(jiān)聽到文件更新了。

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

5. devServer 常用配置和技巧

5.1 常用配置

根據(jù)目錄結(jié)構(gòu)的不同,contentBase、openPage 參數(shù)要配置合適的值,否則運行時應(yīng)該不會立刻訪問到你的首頁。同時要注意你的 publicPath,靜態(tài)資源打包后生成的路徑是一個需要思考的點,取決于你的目錄結(jié)構(gòu)。 

  1. devServer: {  
  2.   contentBase: path.join(__dirname, 'static'),    // 告訴服務(wù)器從哪里提供內(nèi)容(默認當前工作目錄)  
  3.   openPage: 'views/index.html',  // 指定默認啟動瀏覽器時打開的頁面  
  4.   index: 'views/index.html',  // 指定首頁位置  
  5.   watchContentBase: true, // contentBase下文件變動將reload頁面(默認false)  
  6.   host: 'localhost', // 默認localhost,想外部可訪問用'0.0.0.0'  
  7.   port: 8080, // 默認8080  
  8.   inline: true, // 可以監(jiān)控js變化  
  9.   hot: true, // 熱啟動  
  10.   open: true, // 啟動時自動打開瀏覽器(指定打開chrome,open: 'Google Chrome')  
  11.   compress: true, // 一切服務(wù)都啟用gzip 壓縮  
  12.   disableHostCheck: true, // true:不進行host檢查  
  13.   quiet: false,  
  14.   https: false,  
  15.   clientLogLevel: 'none',  
  16.   stats: { // 設(shè)置控制臺的提示信息  
  17.     chunks: false,  
  18.     children: false,  
  19.     modules: false,  
  20.     entrypoints: false, // 是否輸出入口信息  
  21.     warnings: false,  
  22.     performance: false, // 是否輸出webpack建議(如文件體積大?。?nbsp; 
  23.   },  
  24.   historyApiFallback: {  
  25.     disableDotRule: true,  
  26.   },  
  27.   watchOptions: {  
  28.     ignored: /node_modules/, // 略過node_modules目錄  
  29.   },  
  30.   proxy: { // 接口代理(這段配置更推薦:寫到package.json,再引入到這里)  
  31.     "/api-dev": {  
  32.       "target": "http://api.test.xxx.com",  
  33.       "secure": false,  
  34.       "changeOrigin": true,  
  35.       "pathRewrite": { // 將url上的某段重寫(例如此處是將 api-dev 替換成了空)  
  36.         "^/api-dev": ""  
  37.       }  
  38.     }  
  39.   },  
  40.   before(app) { },  

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

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

  1. devServer:{  
  2.   writeToDisk: true,  

通常可以用于代理映射文件調(diào)試,編譯時會產(chǎn)出許多帶 hash 的 js 文件,不帶 hash 的文件同樣也是實時編譯的。

5.3 技巧2:默認使用本地 IP 啟動服務(wù)

有的時候,啟動服務(wù)時,想要默認使用本地的 ip 地址打開: 

  1. devServer:{  
  2.   disableHostCheck: true, // true:不進行host檢查  
  3.   // useLocalIp: true, // 建議不在這里配置  
  4.   // host: '0.0.0.0', // 建議不在這里配置  

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

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

5.4 技巧3:指定啟動的調(diào)試域名

有時啟動的時候希望是指定的調(diào)試域名,例如:local.test.baidu.com: 

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

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

  1. 127.0.0.1 local.test.baidu.com 

服務(wù)啟動后將自動打開 local.test.baidu.com:8080 訪問

5.5 技巧4:啟動 gzip 壓縮 

  1. devServer:{  
  2.   compress: true,  

三、HMR 基本原理介紹

從前面介紹中,我們知道:HMR 主要功能是會在應(yīng)用程序運行過程中替換、添加或刪除模塊,而無需重新加載整個頁面。

那么,Webpack 編譯源碼所產(chǎn)生的文件變化在編譯時,替換模塊實現(xiàn)在運行時,兩者如何聯(lián)系起來?

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

HMR 工作流程圖.png

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

  1.  當 Webpack(Watchman) 監(jiān)聽到項目中的文件/模塊代碼發(fā)生變化后,將變化通知 Webpack 中的構(gòu)建工具(Packager)即 HMR Plugin;
  2.  然后經(jīng)過 HMR Plugin 處理后,將結(jié)果發(fā)送到應(yīng)用程序(Application)的運行時框架(HMR Runtime);
  3.  最后由 HMR Runtime 將這些發(fā)生變化的文件/模塊更新(新增/刪除或替換)到模塊系統(tǒng)中。

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

注意💖:建議先理解上面這張圖的大致流程,在進行后續(xù)閱讀。放心,我等著大家~😃

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

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

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

Webpack HMR.png

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

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

  •  Webpack-dev-server :一個服務(wù)器插件,相當于 express 服務(wù)器,啟動一個 Web 服務(wù),只適用于開發(fā)環(huán)境;
  •  Webpack-dev-middleware :一個 Webpack-dev-server 的中間件,作用簡單總結(jié)為:通過watch mode,監(jiān)聽資源的變更,然后自動打包。
  •  Webpack-hot-middleware :結(jié)合 Webpack-dev-middleware 使用的中間件,它可以實現(xiàn)瀏覽器的無刷新更新,也就是 HMR;

下面一起學(xué)習 HMR 整個工作原理吧:

1.監(jiān)控代碼變化,重新編譯打包

首先根據(jù) devServer 配置,使用 npm start 將啟動 Webpack-dev-server 啟動本地服務(wù)器并進入 Webpack 的 watch 模式,然后初始化 Webpack-dev-middleware ,在 Webpack-dev-middleware 中通過調(diào)用 startWatch() 方法對文件系統(tǒng)進行 watch: 

  1. // webpack-dev-server\bin\webpack-dev-server.js  
  2. // 1.啟動本地服務(wù)器 Line 386  
  3. server = new Server(compiler, options);  
  4. // webpack-dev-server\lib\Server.js  
  5. // 2.初始化 Webpack-dev-middleware Line 109  
  6. this.middleware = webpackDevMiddleware(compiler, Object.assign({}, options, wdmOptions));  
  7. // webpack-dev-middleware\lib\Shared.js  
  8. // 3.開始 watch 文件系統(tǒng) Line 171  
  9. startWatch: function() {  
  10.  //...  
  11.  // start watching  
  12.  if(!options.lazy) {  
  13.   var watching = compiler.watch(options.watchOptions, share.handleCompilerCallback);  
  14.   context.watching = watching;  
  15.  }  
  16.  //...  
  17.  
  18. share.startWatch();  
  19. // ... 

當 startWatch() 方法執(zhí)行后,便進入 watch 模式,若發(fā)現(xiàn)文件中代碼發(fā)生修改,則根據(jù)配置文件對模塊重新編譯打包。

2.保存編譯結(jié)果

Webpack 與  Webpack-dev-middleware 交互,Webpack-dev-middleware 調(diào)用 Webpack 的 API 對代碼變化進行監(jiān)控,并通知 Webpack 將重新編譯的代碼通過 JavaScript 對象保存在內(nèi)存中。

我們會發(fā)現(xiàn),在 output.path 指定的 dist 目錄并沒有保存編譯結(jié)果的文件,這是為什么?

其實, Webpack 將編譯結(jié)果保存在內(nèi)存中,因為訪問內(nèi)存中的代碼比訪問文件系統(tǒng)中的文件快,這樣可以減少代碼寫入文件的開銷。

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

  1. // webpack-dev-middleware\lib\Shared.js Line 108  
  2. // store our files in memory  
  3. var fs;  
  4. var isMemoryFs = !compiler.compilers &&   
  5.     compiler.outputFileSystem instanceof MemoryFileSystem;  
  6. if(isMemoryFs) {  
  7.  fs = compiler.outputFileSystem;  
  8. } else {  
  9.  fs = compiler.outputFileSystem = new MemoryFileSystem();  
  10.  
  11. context.fs = fs; 

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

3.監(jiān)控文件變化,刷新瀏覽器

Webpack-dev-server 開始監(jiān)控文件變化,與第 1 步不同的是,這里并不是監(jiān)控代碼變化重新編譯打包。

當我們在配置文件中配置了 devServer.watchContentBase 為 true ,Webpack-dev-server 會監(jiān)聽配置文件夾中靜態(tài)文件的變化,發(fā)生變化時,通知瀏覽器端對應(yīng)用進行瀏覽器刷新,這與 HMR 不一樣。 

  1. // webpack-dev-server\lib\Server.js  
  2. // 1. 讀取參數(shù) Line 385  
  3. if (options.watchContentBase) { defaultFeatures.push('watchContentBase'); }  
  4. // 2. 定義 _watch 方法 Line 697  
  5. Server.prototype._watch = function (watchPath) {  
  6.  // ...  
  7.   const watcher = chokidar.watch(watchPath, options).on('change', () => {  
  8.     this.sockWrite(this.sockets, 'content-changed');  
  9.   });  
  10.   this.contentBaseWatchers.push(watcher);  
  11. };  
  12. // 3. 執(zhí)行 _watch() 監(jiān)聽文件變化 Line 339  
  13. watchContentBase: () => {  
  14.     if (/^(https?:)?\/\//.test(contentBase) || typeof contentBase === 'number') {  
  15.         throw new Error('Watching remote files is not supported.');  
  16.     } else if (Array.isArray(contentBase)) {  
  17.         contentBase.forEach((item) => {  
  18.             this._watch(item);  
  19.         });  
  20.     } else {  
  21.         this._watch(contentBase);  
  22.     }  

4.建立 WS,同步編譯階段狀態(tài)

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

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

  •  發(fā)送狀態(tài)

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

  •  保存狀態(tài)

瀏覽器端將_sendStats 發(fā)送過來的 hash 保存下來,它將會用到后模塊熱更新。

 

  1. // webpack-dev-server\lib\Server.js  
  2. // 1. 定義 _sendStats 方法 Line 685  
  3. // send stats to a socket or multiple sockets  
  4. Server.prototype._sendStats = function (sockets, stats, force) {  
  5.   //...  
  6.   this.sockWrite(sockets, 'hash', stats.hash);  
  7. };  
  8. // 2. 監(jiān)聽 done 事件 Line 86  
  9. compiler.plugin('done', (stats) => {  
  10.    // 將最新打包文件的 hash 值(stats.hash)作為參數(shù)傳入 _sendStats()  
  11.     this._sendStats(this.sockets, stats.toJson(clientStats));  
  12.     this._stats = stats;  
  13. });  
  14. // webpack-dev-server\client\index.js  
  15. // 3. 保存 hash 值 Line 74  
  16. var onSocketMsg = {  
  17.   // ...  
  18.   hash: function hash(_hash) {  
  19.     currentHash = _hash 
  20.   },  
  21.   // ...  
  22.  
  23. socket(socketUrl, onSocketMsg); 

5.瀏覽器端發(fā)布消息

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

  1. // webpack-dev-server\client\index.js  
  2. // 1. 處理 ok 消息 Line 135  
  3. var onSocketMsg = {  
  4.   // ...  
  5.   ok: function ok() {  
  6.       sendMsg('Ok');  
  7.       if (useWarningOverlay || useErrorOverlay) overlay.clear();  
  8.       if (initial) return initial = false; // eslint-disable-line no-return-assign  
  9.       reloadApp();  
  10.   },  
  11.   // ...  
  12.  
  13. // 2. 處理刷新 APP Line 218  
  14. function reloadApp() {  
  15.   // ...  
  16.   if (_hot) {  
  17.     // 動態(tài)加載 emitter  
  18.     var hotEmitter = require('webpack/hot/emitter');  
  19.     hotEmitter.emit('webpackHotUpdate', currentHash);  
  20.     if (typeof self !== 'undefined' && self.window) {  
  21.       // broadcast update to window  
  22.       self.postMessage('webpackHotUpdate' + currentHash, '*');  
  23.     }  
  24.   }  
  25.   // ...  

6.傳遞 hash 到 HMR

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

  1. // webpack\hot\dev-server.js  
  2. // 1.監(jiān)聽 webpackHotUpdate Line 42  
  3. var hotEmitter = require("./emitter");  
  4. hotEmitter.on("webpackHotUpdate", function(currentHash) {  
  5.     lastHash = currentHash 
  6.     if(!upToDate() && module.hot.status() === "idle") {  
  7.         log("info", "[HMR] Checking for updates on the server...");  
  8.         check();  
  9.     }  
  10. });  
  11. var check = function check() {  
  12.     module.hot.check(true).then(function(updatedModules) {  
  13.         if(!updatedModules) {  
  14.             // ...  
  15.       window.location.reload();// 瀏覽器刷新  
  16.             return;  
  17.         }  
  18.         if(!upToDate()) {  
  19.             check();  
  20.         }  
  21.     }).catch(function(err) { /*...*/});  
  22. };  
  23. // webpack\lib\HotModuleReplacement.runtime.js  
  24. // 3.調(diào)用 HotModuleReplacement.runtime 定義的 check 方法 Line 167  
  25. function hotCheck(apply) {  
  26.     if(hotStatus !== "idle") throw new Error("check() is only allowed in idle status");  
  27.     hotApplyOnUpdate = apply 
  28.     hotSetStatus("check");  
  29.     return hotDownloadManifest(hotRequestTimeout).then(function(update) {  
  30.     //...  
  31.     });  

7.檢測是否存在更新

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

  1. // webpack\lib\HotModuleReplacement.runtime.js  
  2. // 1.調(diào)用 HotModuleReplacement.runtime 定義 hotDownloadUpdateChunk 方法 Line 171  
  3. function hotCheck(apply) {  
  4.     if(hotStatus !== "idle") throw new Error("check() is only allowed in idle status");  
  5.     hotApplyOnUpdate = apply 
  6.     hotSetStatus("check");  
  7.     return hotDownloadManifest(hotRequestTimeout).then(function(update) {  
  8.     //...  
  9.         {  
  10.           // hotEnsureUpdateChunk 方法中會調(diào)用 hotDownloadUpdateChunk  
  11.           hotEnsureUpdateChunk(chunkId);  
  12.         }  
  13.     });  

其中 hotEnsureUpdateChunk 方法中會調(diào)用 hotDownloadUpdateChunk : 

  1. // webpack\lib\HotModuleReplacement.runtime.js Line 215  
  2.  function hotEnsureUpdateChunk(chunkId) {  
  3.   if(!hotAvailableFilesMap[chunkId]) {  
  4.    hotWaitingFilesMap[chunkId] = true;  
  5.   } else {  
  6.    hotRequestedFilesMap[chunkId] = true;  
  7.    hotWaitingFiles++;  
  8.    hotDownloadUpdateChunk(chunkId);  
  9.   }  
  10.  } 

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

在調(diào)用 check 方法時,會先調(diào)用 JsonpMainTemplate.runtime 中的 hotDownloadManifest 方法, 通過向服務(wù)端發(fā)起 AJAX 請求獲取是否有更新文件,如果有的話將 mainfest 返回給瀏覽器端。

這邊涉及一些原生 XMLHttpRequest,就不全部貼出了~ 

  1. // webpack\lib\JsonpMainTemplate.runtime.js  
  2. // hotDownloadManifest 定義 Line 22  
  3. function hotDownloadManifest(requestTimeout) {  
  4.     return new Promise(function(resolve, reject) {  
  5.         try {  
  6.             var request = new XMLHttpRequest();  
  7.             var requestPath = $require$.p + $hotMainFilename$;  
  8.             request.open("GET", requestPath, true);  
  9.             request.timeout = requestTimeout 
  10.             request.send(null);  
  11.         } catch(err) {  
  12.             return reject(err);  
  13.         }  
  14.         request.onreadystatechange = function() {  
  15.             // ...  
  16.         };  
  17.     });  

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

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

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

  1. // webpack\lib\JsonpMainTemplate.runtime.js  
  2. // hotDownloadManifest 定義 Line 12  
  3. function hotDownloadUpdateChunk(chunkId) {  
  4.   // 創(chuàng)建 script 標簽,發(fā)起 JSONP 請求  
  5.     var head = document.getElementsByTagName("head")[0];  
  6.     var script = document.createElement("script");  
  7.     script.type = "text/javascript" 
  8.     script.charset = "utf-8" 
  9.     script.src = $require$.p + $hotChunkFilename$;  
  10.     $crossOriginLoading$;  
  11.     head.appendChild(script);  

10.更新模塊和依賴引用

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

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

  1.  找出過期模塊 outdatedModules 和過期依賴 outdatedDependencies ; 
  1. // webpack\lib\HotModuleReplacement.runtime.js  
  2. // 找出 outdatedModules 和 outdatedDependencies Line 342  
  3. function hotApply() {   
  4.   // ...  
  5.   var outdatedDependencies = {};  
  6.   var outdatedModules = [];  
  7.   function getAffectedStuff(updateModuleId) {  
  8.     var outdatedModules = [updateModuleId];  
  9.     var outdatedDependencies = {};  
  10.     // ...  
  11.     return {  
  12.         type: "accepted",  
  13.         moduleId: updateModuleId,  
  14.         outdatedModules: outdatedModules, 
  15.          outdatedDependencies: outdatedDependencies  
  16.     };  
  17.  };  
  18.   function addAllToSet(a, b) {  
  19.       for (var i = 0; i < b.length; i++) {  
  20.           var item = b[i];  
  21.           if (a.indexOf(item) < 0 
  22.               a.push(item);  
  23.       }  
  24.   }  
  25.   for(var id in hotUpdate) {  
  26.       if(Object.prototype.hasOwnProperty.call(hotUpdate, id)) {  
  27.           // ... 省略多余代碼  
  28.           if(hotUpdate[id]) {  
  29.               result = getAffectedStuff(moduleId);  
  30.           }  
  31.           if(doApply) {  
  32.               for(moduleId in result.outdatedDependencies) {  
  33.                  // 添加到 outdatedDependencies  
  34.                   addAllToSet(outdatedDependencies[moduleId], result.outdatedDependencies[moduleId]);  
  35.               }  
  36.           }  
  37.           if(doDispose) {  
  38.               // 添加到 outdatedModules  
  39.               addAllToSet(outdatedModules, [result.moduleId]);  
  40.               appliedUpdate[moduleId] = warnUnexpectedRequire;  
  41.           }  
  42.       }  
  43.   }  

     2.  從緩存中刪除過期模塊、依賴和所有子元素的引用; 

  1. // webpack\lib\HotModuleReplacement.runtime.js  
  2. // 從緩存中刪除過期模塊、依賴和所有子元素的引用 Line 442  
  3. function hotApply() {  
  4.    // ...  
  5.     var idx;  
  6.     var queue = outdatedModules.slice();  
  7.     while(queue.length > 0) {  
  8.         moduleId = queue.pop();  
  9.         module = installedModules[moduleId];  
  10.         // ...  
  11.         // 移除緩存中的模塊  
  12.         delete installedModules[moduleId];  
  13.         // 移除過期依賴中不需要使用的處理方法  
  14.         delete outdatedDependencies[moduleId];  
  15.         // 移除所有子元素的引用  
  16.         for(j = 0; j < module.children.length; j++) {  
  17.             var child = installedModules[module.children[j]];  
  18.             if(!child) continue;  
  19.             idx = child.parents.indexOf(moduleId);  
  20.             if(idx >= 0) {  
  21.                 child.parents.splice(idx, 1);  
  22.             }  
  23.         }  
  24.     }  
  25.   // 從模塊子組件中刪除過時的依賴項  
  26.   var dependency;  
  27.   var moduleOutdatedDependencies;  
  28.   for(moduleId in outdatedDependencies) {  
  29.    if(Object.prototype.hasOwnProperty.call(outdatedDependencies, moduleId)) {  
  30.     module = installedModules[moduleId];  
  31.     if(module) {  
  32.      moduleOutdatedDependencies = outdatedDependencies[moduleId];  
  33.      for(j = 0; j < moduleOutdatedDependencies.length; j++) {  
  34.       dependency = moduleOutdatedDependencies[j];  
  35.       idx = module.children.indexOf(dependency);  
  36.       if(idx >= 0) module.children.splice(idx, 1);  
  37.      }  
  38.     } 
  39.    }  
  40.   }  

     3.  將新模塊代碼添加到 modules 中,當下次調(diào)用 __webpack_require__  (webpack 重寫的 require  方法)方法的時候,就是獲取到了新的模塊代碼了。

 

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

hotApply 方法執(zhí)行之后,新代碼已經(jīng)替換舊代碼,但是我們業(yè)務(wù)代碼并不知道這些變化,因此需要通過 accept事件通知應(yīng)用層使用新的模塊進行“局部刷新”,我們在業(yè)務(wù)中是這么使用: 

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

11.熱更新錯誤處理

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

  1. // webpack\hot\dev-server.js Line 13  
  2. module.hot.check(true).then(function (updatedModules) {  
  3.     if (!updatedModules) {  
  4.         return window.location.reload();  
  5.     }  
  6.     // ...  
  7. }).catch(function (err) {  
  8.     var status = module.hot.status();  
  9.     if (["abort", "fail"].indexOf(status) >= 0) {  
  10.         window.location.reload();  
  11.     }  
  12. }); 

五、總結(jié)

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

 

責任編輯:龐桂玉 來源: 前端大全
相關(guān)推薦

2010-07-15 15:39:51

Perl線程

2010-07-21 11:04:44

Perl學(xué)習指南

2009-11-11 14:32:33

路由協(xié)議介紹

2010-07-16 15:29:02

Windows Pho

2010-07-29 09:31:28

Flex編程模型

2020-06-12 10:00:25

前端tsconfig.js命令

2021-08-17 05:57:56

數(shù)據(jù)分析數(shù)據(jù)分析師工具

2021-09-13 09:40:35

Webpack 前端HMR 原理

2010-07-16 10:44:54

Perl數(shù)組

2011-08-23 10:29:13

LuaPlayer

2009-09-17 09:01:10

CCNA學(xué)習指南CCNA

2021-06-15 07:20:47

Webpack 機制HMR

2010-06-11 14:21:53

2014-12-11 17:55:05

程序員

2015-12-07 11:22:00

算法學(xué)習指南

2024-10-15 08:10:49

NotebookLMYouTube視頻AI

2015-09-24 14:04:59

機器學(xué)習

2009-11-24 14:48:00

2009-11-12 17:20:17

2009-03-22 12:01:33

CCNA視頻教程下載
點贊
收藏

51CTO技術(shù)棧公眾號