<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Webpack 熱更新HMR 原理全解析

          共 6873字,需瀏覽 14分鐘

           ·

          2021-10-26 11:56

          這是 Webpack 原理分析系列第十篇文章,前文可到公眾號【Tecvan】查閱。

          一、什么是 HMR

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

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

          1.1?HMR 之前

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

          • 對于復(fù)雜表單場景,這意味著你可能需要重新填充非常多字段信息
          • 彈框消失,你必須重新執(zhí)行交互動作才會重新彈出

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

          1.2 使用 HMR

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

          • 配置?devServer.hot?屬性為 true,如:
          //?webpack.config.js
          module.exports?=?{
          ??//?...
          ??devServer:?{
          ????//?必須設(shè)置?devServer.hot?=?true,啟動?HMR?功能
          ????hot:?true
          ??}
          };
          • 之后,還需要調(diào)用?module.hot.accept?接口,聲明如何將模塊安全地替換為最新代碼,如:
          import?component?from?"./component";
          let?demoComponent?=?component();

          document.body.appendChild(demoComponent);
          //?HMR?interface
          if?(module.hot)?{
          ??//?Capture?hot?update
          ??module.hot.accept("./component",?()?=>?{
          ????const?nextComponent?=?component();

          ????//?Replace?old?content?with?the?hot?loaded?one
          ????document.body.replaceChild(nextComponent,?demoComponent);

          ????demoComponent?=?nextComponent;
          ??});
          }

          模塊代碼的替換邏輯可能非常復(fù)雜,幸運的是我們通常不太需要對此過多關(guān)注,因為業(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 即可,很容易上手。

          二、實現(xiàn)原理

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

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

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

          2.1?注入 HMR 客戶端運行時

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

          • 用于建立 WebSocket 連接,處理?hash?等消息的運行時代碼
          • 用于加載熱更新資源的?RuntimeGlobals.hmrDownloadManifest?與?RuntimeGlobals.hmrDownloadUpdateHandlers?接口
          • 用于處理模塊更新策略的?module.hot.accept?接口
          • 等等
          關(guān)于 Webpack Runtime,可參考?Webpack 原理系列六:徹底理解 Webpack 運行時

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

          2.2 增量構(gòu)建

          除注入客戶端代碼外,HotModuleReplacementPlugin?插件還會借助 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)計信息對象?stats。WDS 則監(jiān)聽?done?鉤子,在回調(diào)中通過 WebSocket 發(fā)送模塊更新消息:

          {"type":"hash","data":"${stats.hash}"}

          實際效果:

          2.3 加載更新

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

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

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

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

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

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

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

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

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

          例如,對于如下代碼:

          //?src/bar.js
          export?const?bar?=?'bar'

          //?src/index.js
          import?{?bar?}?from?'./bar';
          const?node?=?document.createElement('div')
          node.innerText?=?bar;
          document.body.appendChild(node)

          module.hot.accept('./bar.js',?function?()?{
          ????node.innerText?=?bar;
          })

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

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

          2.4.1 失敗兜底

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

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

          2.4.2 更新事件冒泡

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

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

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

          這一特性與 DOM 事件規(guī)范中的冒泡過程極為相似,使用時如果摸不準(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)前文件的變更事件,并從模塊第一行開始重新運行該模塊的代碼,例如:

          //?src/bar.js
          console.log('bar');

          module.hot.accept();

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

          2.5 小結(jié)

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

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

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

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

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

          "./src/a.vue":
          /*!*******************!*\
          ????!***?./src/a.vue?***!
          ????\*******************/

          /***/
          ((module,?__webpack_exports__,?__webpack_require__)?=>?{
          ????//?模塊代碼
          ????//?...
          ????/*?hot?reload?*/
          ????if?(true)?{
          ????var?api?=?__webpack_require__(?/*!?../node_modules/vue-hot-reload-api/dist/index.js?*/?"../node_modules/vue-hot-reload-api/dist/index.js")
          ????api.install(__webpack_require__(?/*!?vue?*/?"../node_modules/vue/dist/vue.runtime.esm.js"))
          ????if?(api.compatible)?{
          ????????module.hot.accept()
          ????????if?(!api.isRecorded('45c6ab58'))?{
          ????????api.createRecord('45c6ab58',?component.options)
          ????????}?else?{
          ????????api.reload('45c6ab58',?component.options)
          ????????}
          ????????module.hot.accept(?/*!?./a.vue?vue&type=template&id=45c6ab58&?*/?"./src/a.vue?vue&type=template&id=45c6ab58&",?__WEBPACK_OUTDATED_DEPENDENCIES__?=>?{
          ????????/*?harmony?import?*/
          ????????_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&");
          ????????(function?()?{
          ????????????api.rerender('45c6ab58',?{
          ????????????render:?_a_vue_vue_type_template_id_45c6ab58___WEBPACK_IMPORTED_MODULE_0__.render,
          ????????????staticRenderFns:?_a_vue_vue_type_template_id_45c6ab58___WEBPACK_IMPORTED_MODULE_0__.staticRenderFns
          ????????????})
          ????????})(__WEBPACK_OUTDATED_DEPENDENCIES__);
          ????????})
          ????}
          ????}
          ????//?...

          ????/***/
          }),

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

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

          這是因為?vue-loader?在做轉(zhuǎn)譯時,會將 SFC 不同板塊拆解成多個 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)時,會根據(jù)情況執(zhí)行?vue-hot-reload-api?暴露的?reload?與?rerender?函數(shù),兩者最終都會觸發(fā)組件實例的?$forceUpdate?函數(shù)強制執(zhí)行重新渲染。

          四、總結(jié)

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

          最近很忙,雙月OKR、年中績效,還有一些突發(fā)事件,這篇文章實際上在上周就完成了80%,但一直沒時間收尾,后面我盡量保持 1-2 周一更吧。

          BTW,字節(jié)游戲中臺-前端團(tuán)隊持續(xù)熱招,歡迎直接聯(lián)系我內(nèi)推,我會跟進(jìn)內(nèi)推整個過程,「知無不言言無不盡!」



          瀏覽 79
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费A片完整视频 | 91成人手机在线 | 婷婷国产精品久久久 | 三级片网站在线播放 | 操逼视频黄片中国版的中国的 |