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

Webpack 原理系列十:HMR 原理全解析

開發(fā)
HMR 全稱 Hot Module Replacement,中文語境通常翻譯為模塊熱更新,它能夠在保持頁面狀態(tài)的情況下動(dòng)態(tài)替換資源模塊,提供絲滑順暢的 Web 頁面開發(fā)體驗(yàn)。

[[423457]]

一、什么是 HMR

HMR 全稱 Hot Module Replacement,中文語境通常翻譯為模塊熱更新,它能夠在保持頁面狀態(tài)的情況下動(dòng)態(tài)替換資源模塊,提供絲滑順暢的 Web 頁面開發(fā)體驗(yàn)。

HMR 最初由 Webpack 設(shè)計(jì)實(shí)現(xiàn),至今已幾乎成為現(xiàn)代工程化工具必備特性之一。

1.1 HMR 之前

在 HMR 之前,應(yīng)用的加載、更新是一種頁面級(jí)別的原子操作,即使只是單個(gè)代碼文件發(fā)生變更都需要刷新整個(gè)頁面才能最新代碼映射到瀏覽器上,這會(huì)丟失之前在頁面執(zhí)行過的所有交互與狀態(tài),例如:

對于復(fù)雜表單場景,這意味著你可能需要重新填充非常多字段信息

彈框消失,你必須重新執(zhí)行交互動(dòng)作才會(huì)重新彈出

再小的改動(dòng),例如更新字體大小,改變備注信息都會(huì)需要整個(gè)頁面重新加載執(zhí)行,影響開發(fā)體驗(yàn)。引入 HMR 后,雖然無法覆蓋所有場景,但大多數(shù)小改動(dòng)都可以實(shí)時(shí)熱更新到頁面上,從而確保連續(xù)、順暢的開發(fā)調(diào)試體驗(yàn),對開發(fā)效率有較大增益效果。

1.2 使用 HMR

Webpack 生態(tài)下,只需要經(jīng)過簡單的配置即可啟動(dòng) HMR 功能,大致上分兩步:

配置 devServer.hot 屬性為 true,如:

  1. // webpack.config.js 
  2. module.exports = { 
  3.   // ... 
  4.   devServer: { 
  5.     // 必須設(shè)置 devServer.hot = true,啟動(dòng) HMR 功能 
  6.     hot: true 
  7.   } 
  8. }; 
  • 之后,還需要調(diào)用 module.hot.accept 接口,聲明如何將模塊安全地替換為最新代碼,如:
  1. import component from "./component"
  2. let demoComponent = component(); 
  3.  
  4. document.body.appendChild(demoComponent); 
  5. // HMR interface 
  6. if (module.hot) { 
  7.   // Capture hot update 
  8.   module.hot.accept("./component", () => { 
  9.     const nextComponent = component(); 
  10.  
  11.     // Replace old content with the hot loaded one 
  12.     document.body.replaceChild(nextComponent, demoComponent); 
  13.  
  14.     demoComponent = nextComponent; 
  15.   }); 

模塊代碼的替換邏輯可能非常復(fù)雜,幸運(yùn)的是我們通常不太需要對此過多關(guān)注,因?yàn)闃I(yè)界許多 Webpack Loader 已經(jīng)提供了針對不同資源的 HMR 功能,例如:

  • style-loader 內(nèi)置 Css 模塊熱更
  • vue-loader 內(nèi)置 Vue 模塊熱更
  • react-hot-reload 內(nèi)置 React 模塊熱更接口

因此,站在使用的角度,只需要針對不同資源配置對應(yīng)支持 HMR 的 Loader 即可,很容易上手。

二、實(shí)現(xiàn)原理

Webpack HMR 特性的原理并不復(fù)雜,核心流程:

  1. 使用 webpack-dev-server (后面簡稱 WDS)托管靜態(tài)資源,同時(shí)以 Runtime 方式注入 HMR 客戶端代碼
  2. 瀏覽器加載頁面后,與 WDS 建立 WebSocket 連接
  3. Webpack 監(jiān)聽到文件變化后,增量構(gòu)建發(fā)生變更的模塊,并通過 WebSocket 發(fā)送 hash 事件
  4. 瀏覽器接收到 hash 事件后,請求 manifest 資源文件,確認(rèn)增量變更范圍
  5. 瀏覽器加載發(fā)生變更的增量模塊
  6. Webpack 運(yùn)行時(shí)觸發(fā)變更模塊的 module.hot.accept 回調(diào),執(zhí)行代碼變更邏輯
  7. done

接下來我會(huì)展開 HMR 的核心源碼,詳細(xì)講解 Webpack 5 中 Hot Module Replacement 原理的關(guān)鍵部分,內(nèi)容略微晦澀,不感興趣的同學(xué)可以直接跳到下一章。

2.1 注入 HMR 客戶端運(yùn)行時(shí)

執(zhí)行 npx webpack serve 命令后,WDS 調(diào)用 HotModuleReplacementPlugin 插件向應(yīng)用的主 Chunk 注入一系列 HMR Runtime,包括:

  • 用于建立 WebSocket 連接,處理 hash 等消息的運(yùn)行時(shí)代碼
  • 用于加載熱更新資源的 RuntimeGlobals.hmrDownloadManifest 與 RuntimeGlobals.hmrDownloadUpdateHandlers 接口
  • 用于處理模塊更新策略的 module.hot.accept 接口
  • 等等

關(guān)于 Webpack Runtime,可參考 Webpack 原理系列六:徹底理解 Webpack 運(yùn)行時(shí)。

經(jīng)過 HotModuleReplacementPlugin 處理后,構(gòu)建產(chǎn)物中即包含了所有運(yùn)行 HMR 所需的客戶端運(yùn)行時(shí)與接口。這些 HMR 運(yùn)行時(shí)會(huì)在瀏覽器執(zhí)行一套基于 WebSocket 消息的時(shí)序框架,如圖:

2.2 增量構(gòu)建

除注入客戶端代碼外,HotModuleReplacementPlugin 插件還會(huì)借助 Webpack 的 watch 能力,在代碼文件發(fā)生變化后執(zhí)行增量構(gòu)建,生成:

  • manifest 文件:JSON 格式文件,包含所有發(fā)生變更的模塊列表,命名為 [hash].hot-update.json
  • 模塊變更文件:js 格式,包含編譯后的模塊代碼,命名為 [hash].hot-update.js增量構(gòu)建完畢后,Webpack 將觸發(fā) compilation.hooks.done 鉤子,并傳遞本次構(gòu)建的統(tǒng)計(jì)信息對象 stats。WDS 則監(jiān)聽 done 鉤子,在回調(diào)中通過 WebSocket 發(fā)送模塊更新消息:
  1. {"type":"hash","data":"${stats.hash}"

實(shí)際效果:

2.3 加載更新

客戶端接受到 hash 消息后,首先發(fā)出 manifest 請求獲取本輪熱更新涉及的 chunk,如:

注意,在 Webpack 4 及之前,熱更新文件以模塊為單位,即所有發(fā)生變化的模塊都會(huì)生成對應(yīng)的熱更新文件; Webpack 5 之后熱更新文件以 chunk 為單位,如上例中,main chunk 下任意文件的變化都只會(huì)生成 main.[hash].hot-update.js 更新文件。

manifest 請求完成后,客戶端 HMR 運(yùn)行時(shí)開始下載發(fā)生變化的 chunk 文件,將最新模塊代碼加載到本地。

2.4module.hot.accept回調(diào)

經(jīng)過上述步驟,瀏覽器加載完最新模塊代碼后,HMR 運(yùn)行時(shí)會(huì)繼續(xù)觸發(fā) module.hot.accept 回調(diào),將最新代碼替換到運(yùn)行環(huán)境中。

module.hot.accept 是 HMR 運(yùn)行時(shí)暴露給用戶代碼的重要接口之一,它在 Webpack HMR 體系中開了一個(gè)口子,讓用戶能夠自定義模塊熱替換的邏輯。module.hot.accept 接口簽名如下:

  1. module.hot.accept(path?: string, callback?: function); 

它接受兩個(gè)參數(shù):

  • path:指定需要攔截變更行為的模塊路徑
  • callback:模塊更新后,將最新模塊代碼應(yīng)用到運(yùn)行環(huán)境的函數(shù)

例如,對于如下代碼:

  1. // src/bar.js 
  2. export const bar = 'bar' 
  3.  
  4. // src/index.js 
  5. import { bar } from './bar'
  6. const node = document.createElement('div'
  7. node.innerText = bar; 
  8. document.body.appendChild(node) 
  9.  
  10. module.hot.accept('./bar.js'function () { 
  11.     node.innerText = bar; 
  12. }) 

示例中,module.hot.accept 函數(shù)監(jiān)聽 ./bar.js 模塊的變更事件,一旦代碼發(fā)生變動(dòng)就觸發(fā)回調(diào),將 ./bar.js 導(dǎo)出的值應(yīng)用到頁面上,從而實(shí)現(xiàn)熱更新效果。

module.hot.accept 的作用并不復(fù)雜,但使用過程中還是有一些值得注意的點(diǎn),下面細(xì)講。

2.4.1 失敗兜底

module.hot.accept 函數(shù)只接受具體路徑的 path 參數(shù),也就是說我們無法通過 glob 或類似風(fēng)格的方式批量注冊熱更新回調(diào)。

一旦某個(gè)模塊沒有注冊對應(yīng)的 module.hot.accept 函數(shù)后,HMR 運(yùn)行時(shí)會(huì)執(zhí)行兜底策略,通常是刷新頁面,確保頁面上運(yùn)行的始終是最新的代碼。

2.4.2 更新事件冒泡

在 Webpack HMR 框架中,module.hot.accept 函數(shù)只能捕獲當(dāng)前模塊對應(yīng)子孫模塊的更新事件,例如對于下面的模塊依賴樹:

示例中,更新事件會(huì)沿著模塊依賴樹自底向上逐級(jí)傳遞,從 foo 到 index ,從 bar-1 到 bar 再到 index,但不支持反向或跨子樹傳遞,也就是說:

  • 在 foo.js 中無法捕獲 bar.js 及其子模塊的變更事件
  • 在 bar-1.js 中無法捕獲 bar.js 的變更事件

這一特性與 DOM 事件規(guī)范中的冒泡過程極為相似,使用時(shí)如果摸不準(zhǔn)模塊的依賴關(guān)系,建議直接在應(yīng)用的入口文件中編寫熱更新函數(shù)。

2.4.3 無參數(shù)調(diào)用

除上述調(diào)用方式外,module.hot.accept 函數(shù)還支持無參數(shù)調(diào)用風(fēng)格,作用是捕獲當(dāng)前文件的變更事件,并從模塊第一行開始重新運(yùn)行該模塊的代碼,例如:

  1. // src/bar.js 
  2. console.log('bar'); 
  3.  
  4. module.hot.accept(); 

示例模塊發(fā)生變動(dòng)之后,會(huì)從頭開始重復(fù)執(zhí)行 console.log 語句。

2.5 小結(jié)

回顧整個(gè) HMR 過程,所有的狀態(tài)流轉(zhuǎn)均由 WebSocket 消息驅(qū)動(dòng),這部分邏輯由 HMR 運(yùn)行時(shí)控制,開發(fā)者幾乎無感。

唯一需要開發(fā)者關(guān)心的是為每一個(gè)需要處理熱更新的文件注冊 module.hot.accept 回調(diào),所幸這部分需求已經(jīng)被許多成熟的 Loader 處理,作為示例,下一節(jié)我們挖掘 vue-loader 源碼,學(xué)習(xí)如何靈活使用 module.hot.accept 函數(shù)處理文件更新。

三、 vue-loader 如何實(shí)現(xiàn) HMR

vue-loader 是一個(gè)用于處理 Vue Single File Component 的 Webpack 加載器,它能夠?qū)⑷缦赂袷降膬?nèi)容轉(zhuǎn)譯為可在瀏覽器運(yùn)行的等價(jià)代碼:

除常規(guī)的代碼轉(zhuǎn)譯外,在 HMR 模式下,vue-loader 還會(huì)為每一個(gè) Vue 文件注入一段處理模塊替換的邏輯,如:

  1. "./src/a.vue"
  2. /*!*******************!*\ 
  3.     !*** ./src/a.vue ***! 
  4.     \*******************/ 
  5. /***/ 
  6. ((module, __webpack_exports__, __webpack_require__) => { 
  7.     // 模塊代碼 
  8.     // ... 
  9.     /* hot reload */ 
  10.     if (true) { 
  11.     var api = __webpack_require__( /*! ../node_modules/vue-hot-reload-api/dist/index.js */ "../node_modules/vue-hot-reload-api/dist/index.js"
  12.     api.install(__webpack_require__( /*! vue */ "../node_modules/vue/dist/vue.runtime.esm.js")) 
  13.     if (api.compatible) { 
  14.         module.hot.accept() 
  15.         if (!api.isRecorded('45c6ab58')) { 
  16.         api.createRecord('45c6ab58', component.options) 
  17.         } else { 
  18.         api.reload('45c6ab58', component.options) 
  19.         } 
  20.         module.hot.accept( /*! ./a.vue?vue&type=template&id=45c6ab58& */ "./src/a.vue?vue&type=template&id=45c6ab58&", __WEBPACK_OUTDATED_DEPENDENCIES__ => { 
  21.         /* harmony import */ 
  22.         _a_vue_vue_type_template_id_45c6ab58___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( /*! ./a.vue?vue&type=template&id=45c6ab58& */ "./src/a.vue?vue&type=template&id=45c6ab58&"); 
  23.         (function () { 
  24.             api.rerender('45c6ab58', { 
  25.             render: _a_vue_vue_type_template_id_45c6ab58___WEBPACK_IMPORTED_MODULE_0__.render, 
  26.             staticRenderFns: _a_vue_vue_type_template_id_45c6ab58___WEBPACK_IMPORTED_MODULE_0__.staticRenderFns 
  27.             }) 
  28.         })(__WEBPACK_OUTDATED_DEPENDENCIES__); 
  29.         }) 
  30.     } 
  31.     } 
  32.     // ... 
  33.  
  34.     /***/ 
  35. }), 

這段被注入用于處理模塊熱替換的代碼,主要步驟有:

  • 首次執(zhí)行時(shí),調(diào)用 api.createRecord 記錄組件配置,api 為 vue-hot-reload-api 庫暴露的接口
  • 執(zhí)行 module.hot.accept() 語句,監(jiān)聽當(dāng)前模塊變更事件,當(dāng)模塊發(fā)生變化時(shí)調(diào)用 api.reload
  • 執(zhí)行 module.hot.accept("xxx.vue?vue&type=template&xxxx", fn) ,監(jiān)聽 Vue 文件 template 代碼的變更事件,當(dāng) template 模塊發(fā)生變更時(shí)調(diào)用 api.rerender

為什么需要調(diào)用兩次 module.hot.accept?

這是因?yàn)?vue-loader 在做轉(zhuǎn)譯時(shí),會(huì)將 SFC 不同板塊拆解成多個(gè) module,例如: template 對應(yīng)生成 xxx.vue?vue&type=template ;script 對應(yīng)生成 xxx.vue?vue&type=script。因此,vue-loader 必須為這些不同的 module 分別調(diào)用 accept 接口,才能處理好不同代碼塊的變更事件。

可以看到,vue-loader 對 HMR 的支持,基本上圍繞 vue-hot-reload-api 展開,當(dāng)代碼文件發(fā)生變化觸發(fā) module.hot.accept 回調(diào)時(shí),會(huì)根據(jù)情況執(zhí)行 vue-hot-reload-api 暴露的 reload 與 rerender 函數(shù),兩者最終都會(huì)觸發(fā)組件實(shí)例的 $forceUpdate 函數(shù)強(qiáng)制執(zhí)行重新渲染。

四、總結(jié)

最后再回顧一下,Webpack 的 HMR 特性有兩個(gè)重點(diǎn),一是監(jiān)聽文件變化并通過 WebSocket 發(fā)送變更消息;二是需要客戶端提供配合,通過 module.hot.accept 接口明確告知 Webpack 如何執(zhí)行代碼替換。整體盤下來,并沒有想象中那么困難。

本文轉(zhuǎn)載自微信公眾號(hào)「Tecvan」

 

責(zé)任編輯:姜華 來源: Tecvan
相關(guān)推薦

2021-08-26 10:30:29

WebpackTree-Shakin前端

2021-05-31 05:36:43

WebpackJavaScript 前端

2020-08-05 08:21:41

Webpack

2021-06-28 05:59:17

Webpack 前端打包與工程化

2021-12-20 00:03:38

Webpack運(yùn)行機(jī)制

2010-10-08 13:53:02

Silverlight

2024-04-26 08:41:04

ViteHMR項(xiàng)目

2020-07-08 14:50:18

WebpackHMR前端

2021-04-19 10:45:52

Webpack熱更新前端

2025-01-23 00:00:01

2022-08-26 13:24:03

version源碼sources

2021-06-15 07:20:47

Webpack 機(jī)制HMR

2019-11-15 15:12:19

Windows激活KMS

2021-12-15 23:42:56

Webpack原理實(shí)踐

2020-12-03 10:40:23

webpack加載原理前端

2023-02-28 09:07:18

ChatGPTAI

2021-12-16 22:02:28

webpack原理模塊化

2009-11-11 10:21:25

路由選擇協(xié)議

2025-03-27 08:10:19

Spring開發(fā)架構(gòu)

2024-06-27 08:26:10

LooperAndroid內(nèi)存
點(diǎn)贊
收藏

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