<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>

          120 行代碼幫你了解 Webpack 下的 HMR 機(jī)制

          共 12873字,需瀏覽 26分鐘

           ·

          2021-06-19 13:44


          朱海華:  微醫(yī)前端技術(shù)部平臺(tái)支撐組 我本地是好的,你再試試~??

          HMR 的背景

          在使用Webpack Dev Server以后 可以讓我們在開發(fā)工程中 專注于 Coding, 因?yàn)樗梢员O(jiān)聽代碼的變化 從而實(shí)現(xiàn)打包更新,并且最后通過自動(dòng)刷新的方式同步到瀏覽器,便于我們及時(shí)查看效果。但是 Dev Server 從監(jiān)聽到打包再到通知瀏覽器整體刷新頁面 就會(huì)導(dǎo)致一個(gè)讓人困擾的問題 那就是 無法保存應(yīng)用狀態(tài)  因此 針對這個(gè)問題,Webpack 提供了一個(gè)新的解決方案 Hot Module Replacement

          HMR 簡單概念

          Hot Module Replacement 是指當(dāng)我們對代碼修改并保存后,Webpack 將會(huì)對代碼進(jìn)行重新打包,并將新的模塊發(fā)送到瀏覽器端,瀏覽器用新的模塊替換掉舊的模塊,以實(shí)現(xiàn)在不刷新瀏覽器的前提下更新頁面。最明顯的優(yōu)勢就是相對于傳統(tǒng)的live reload而言,HMR 并不會(huì)丟失應(yīng)用的狀態(tài),提高開發(fā)效率。在開始深入了解 Webpack HMR 之前 我們可以先簡單過一下下面這張流程圖

          HRM 流程概覽

          1597240262452-5ecbaec0-6245-4ed5-9195-59c7a38e8b24.png
          1. Webpack Compile:  watch 打包本地文件 寫入內(nèi)存
          2. Boundle Server: 啟一個(gè)本地服務(wù),提供文件在瀏覽器端進(jìn)行訪問
          3. HMR Server: 將熱更新的文件輸出給 HMR Runtime
          4. HRM Runtime: 生成的文件,注入至瀏覽器內(nèi)存
          5. Bundle: 構(gòu)建輸出文件

          HMR 入門體驗(yàn)

          開啟 HMR 其實(shí)也極其容易 因?yàn)?HMR 本身就已經(jīng)集成在了 Webpack 里 開啟方式有兩種

          1. 直接通過運(yùn)行 webpack-dev-server 命令時(shí) 加入 --hot參數(shù) 直接開啟 HMR
          2. 寫入配置文件 代碼如下
          // ./webpack.config.js
          const webpack = require('webpack')
          module.exports = {
            // ...
            devServer: {
              // 開啟 HMR 特性 如果不支持 MMR 則會(huì) fallback 到 live reload
              hottrue,
            },
            plugins: [
              // ...
              // HMR 依賴的插件
              new webpack.HotModuleReplacementPlugin()
            ]
          }

          HMR 中的 Server 和 Client

          devServer 通知瀏覽器文件變更

          通過翻閱 webpack-dev-server 源碼 在這一過程中,依賴于 sockjs 提供的服務(wù)端與瀏覽器端之間的橋梁,在 devServer 啟動(dòng)的同時(shí),建立了一個(gè) webSocket 長鏈接,用于通知瀏覽器在 webpack 編譯和打包下的各個(gè)狀態(tài),同時(shí)監(jiān)聽 compile 下的 done 事件,當(dāng) compile 完成以后,通過 sendStats 方法, 將重新編譯打包好的新模塊 hash 值發(fā)送給瀏覽器。

          // webpack-dev-server/blob/master/lib/Server.js
          sendStats(sockets, stats, force) {
              const shouldEmit =
                !force &&
                stats &&
                (!stats.errors || stats.errors.length === 0) &&
                (!stats.warnings || stats.warnings.length === 0) &&
                stats.assets &&
                stats.assets.every((asset) => !asset.emitted);

              if (shouldEmit) {
                this.sockWrite(sockets, 'still-ok');

                return;
              }

              this.sockWrite(sockets, 'hash', stats.hash);

              if (stats.errors.length > 0) {
                this.sockWrite(sockets, 'errors', stats.errors);
              } else if (stats.warnings.length > 0) {
                this.sockWrite(sockets, 'warnings', stats.warnings);
              } else {
                this.sockWrite(sockets, 'ok');
              }
            }

          Client 接收到服務(wù)端消息做出響應(yīng)

          webpack-dev-server/client 當(dāng)接收到 type 為 hash 消息后會(huì)將 hash 值暫時(shí)緩存起來,同時(shí)當(dāng)接收到到 type 為 ok 的時(shí)候,對瀏覽器執(zhí)行 reload 操作。

          reload 策略選擇

          function reloadApp(
            { hotReload, hot, liveReload },
            { isUnloading, currentHash }
          {
            if (isUnloading || !hotReload) {
              return;
            }

            if (hot) {
              log.info('App hot update...');

              const hotEmitter = require('webpack/hot/emitter');

              hotEmitter.emit('webpackHotUpdate', currentHash);

              if (typeof self !== 'undefined' && self.window) {
                // broadcast update to window
                self.postMessage(`webpackHotUpdate${currentHash}`'*');
              }
            }
            // allow refreshing the page only if liveReload isn't disabled
            else if (liveReload) {
              let rootWindow = self;

              // use parent window for reload (in case we're in an iframe with no valid src)
              const intervalId = self.setInterval(() => {
                if (rootWindow.location.protocol !== 'about:') {
                  // reload immediately if protocol is valid
                  applyReload(rootWindow, intervalId);
                } else {
                  rootWindow = rootWindow.parent;

                  if (rootWindow.parent === rootWindow) {
                    // if parent equals current window we've reached the root which would continue forever, so trigger a reload anyways
                    applyReload(rootWindow, intervalId);
                  }
                }
              });
            }

            function applyReload(rootWindow, intervalId{
              clearInterval(intervalId);

              log.info('App updated. Reloading...');

              rootWindow.location.reload();
            }

          通過翻閱 webpack-dev-server/client源碼,我們可以看到,首先會(huì)根據(jù) hot 配置決定是采用哪種更新策略,刷新瀏覽器或者代碼進(jìn)行熱更新(HMR),如果配置了 HMR,就調(diào)用 webpack/hot/emitter 將最新 hash 值發(fā)送給 webpack,如果沒有配置模塊熱更新,就直接調(diào)用 applyReload下的location.reload 方法刷新頁面。

          webpack 根據(jù) hash 請求最新模塊代碼

          在這一步,其實(shí)是 webpack 中三個(gè)模塊(三個(gè)文件,后面英文名對應(yīng)文件路徑)之間配合的結(jié)果,首先是 webpack/hot/dev-server(以下簡稱 dev-server) 監(jiān)聽第三步 webpack-dev-server/client 發(fā)送的 webpackHotUpdate 消息,調(diào)用 webpack/lib/HotModuleReplacement.runtime(簡稱 HMR runtime)中的 check 方法,檢測是否有新的更新,在 check 過程中會(huì)利用 webpack/lib/JsonpMainTemplate.runtime(簡稱 jsonp runtime)中的兩個(gè)方法 hotDownloadUpdateChunk 和 hotDownloadManifest , 第二個(gè)方法是調(diào)用 AJAX 向服務(wù)端請求是否有更新的文件,如果有將發(fā)更新的文件列表返回瀏覽器端,而第一個(gè)方法是通過 jsonp 請求最新的模塊代碼,然后將代碼返回給 HMR runtime,HMR runtime 會(huì)根據(jù)返回的新模塊代碼做進(jìn)一步處理,可能是刷新頁面,也可能是對模塊進(jìn)行熱更新。

          在這個(gè)過程中,其實(shí)是 webpack 三個(gè)模塊配合執(zhí)行之后獲取的結(jié)果

          1. webpack/hot/dev-server監(jiān)聽 client 發(fā)送的webpackHotUpdate消息
          // ....
          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();
            }
           });
           log("info""[HMR] Waiting for update signal from WDS...");
          else {
           throw new Error("[HMR] Hot Module Replacement is disabled.");
          1. [HMR runtime/check()](https://github.com/webpack/webpack/blob/v4.41.5/lib/HotModuleReplacement.runtime.js)檢測是否有新的更新,check 過程中會(huì)利用webpack/lib/web/JsonpMainTemplate.runtime.js中的hotDownloadUpdateChunk(通過 jsonp 請求新的模塊代碼并且返回給 HMR Runtime)以及hotDownloadManifest(發(fā)送 AJAx 請求向 Server 請求是否有更新的文件,如果有則會(huì)將新的文件返回給瀏覽器)

          獲取更新文件列表獲取模塊更新以后的最新代碼

          HMR Runtime 對模塊進(jìn)行熱更新

          這里就是整個(gè) HMR 最關(guān)鍵的步驟了,而其中 最關(guān)鍵的 無非就是hotApply這個(gè)方法了,由于代碼量實(shí)在太多,這里我們直接進(jìn)入過程解析(關(guān)鍵代碼),有興趣的同學(xué)可以閱讀一下源碼。

          1. 找出 outdatedModulesoutdatedDependencies
          2. 刪除過期的模塊以及對應(yīng)依賴
          // remove module from cache
          delete installedModules[moduleId];

          // when disposing there is no need to call dispose handler
          delete outdatedDependencies[moduleId];
          1. 新模塊添加至 modules 中
          for(moduleId in appliedUpdate) {
            if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
                modules[moduleId] = appliedUpdate[moduleId];
            }
          }

          至此 一整個(gè)模塊替換的流程已經(jīng)結(jié)束了,已經(jīng)可以獲取到最新的模塊代碼了,接下來就輪到業(yè)務(wù)代碼如何知曉模塊已經(jīng)發(fā)生了變化~

          HMR 中的 hot 成員

          HotModuleReplaceMentPlugin

          由于我們編寫的 JavaScript 代碼是沒有任何規(guī)律可言的模塊,可以導(dǎo)出的是一個(gè)模塊、函數(shù)、甚至于只是一個(gè)字符串 而對于這些毫無規(guī)律可言的模塊來說 Webpack 是無法提供一個(gè)通用的模塊替換方案去處理的 因此在這種情況下,還想要體驗(yàn)完整的 HMR 開發(fā)流程 是需要我們自己手動(dòng)處理 當(dāng) JS 模塊更新以后,如何將更新以后的 JS 模塊替換至頁面當(dāng)中 因此 HotModuleReplacementPlugin 為我們提供了一系列關(guān)于 HMR 的 API 而其中 最關(guān)鍵的部分則是hot.accept

          接下來 我們將嘗試 自己手動(dòng)處理 JS 模塊更新 并通知到瀏覽器實(shí)現(xiàn)對應(yīng)的局部刷新

          :::info 當(dāng)前主流開發(fā)框架 Vue、React 都提供了統(tǒng)一的模塊替換函數(shù), 因此 Vue、React 項(xiàng)目并不需要針對 HMR 做手動(dòng)的代碼處理,同時(shí) css 文件也由 style-loader 統(tǒng)一處理 因此也不需要額外的處理,因此接下去的代碼處理邏輯,全部建立在純原生開發(fā)的基礎(chǔ)之上實(shí)現(xiàn) :::

          回到代碼中來 假設(shè)當(dāng)前 main.js 文件如下

          // ./src/main.js
          import createChild from './child'

          const child = createChild()
          document.body.appendChild(child)

          main.js 是 Webpack 打包的入口文件 在文件中引入了 Child 模塊 因此 當(dāng) Child 模塊里的業(yè)務(wù)代碼更改以后 webpack 必然會(huì)重新打包,并且重新使用這些更新以后的模塊,所以,我們需要在 main.js 里實(shí)現(xiàn)去處理它所依賴的這些模塊更新后的熱替換邏輯

          在 HMR 已開啟的情況下,我們可以通過訪問全局的module對象下的hot 成員它提供了一個(gè)accept 方法,這個(gè)方法用來注冊當(dāng)某個(gè)模塊更新以后需要如何處理,它接受兩個(gè)參數(shù) 一個(gè)是需要監(jiān)聽模塊的 path(相對路徑),第二個(gè)參數(shù)就是當(dāng)模塊更新以后如何處理 其實(shí)也就是一個(gè)回調(diào)函數(shù)

          // main.js
          // 監(jiān)聽 child 模塊變化
          module.hot.accept('./child', () => {
            console.log('老板好,child 模塊更新啦~')
          })

          當(dāng)做完這些以后,重新運(yùn)行 npm run serve 同時(shí)修改 child 模塊 你會(huì)發(fā)現(xiàn),控制臺(tái)會(huì)輸出以上的 console 內(nèi)容,同時(shí),瀏覽器也不會(huì)自動(dòng)更新了,因此,我們可以得出一個(gè)結(jié)論 當(dāng)你手動(dòng)處理了某個(gè)模塊的更新以后,是不會(huì)出發(fā)自動(dòng)刷新機(jī)制的,接下來 就來一起看看 其中的原理 以及 如何實(shí)現(xiàn) HMR 中的 JS 模塊替換邏輯

          module.hot.accept 原理

          為什么我們只有調(diào)用了moudule.hot.accept才可以實(shí)現(xiàn)熱更新, 翻看源碼 其實(shí)可以發(fā)現(xiàn)實(shí)現(xiàn)如下

          // 部分源碼
          acceptfunction (dep, callback, errorHandler{
              if (dep === undefined) hot._selfAccepted = true;
              else if (typeof dep === "function") hot._selfAccepted = dep;
              else if (typeof dep === "object" && dep !== null) {
               for (var i = 0; i < dep.length; i++) {
                hot._acceptedDependencies[dep[i]] = callback || function ({};
                hot._acceptedErrorHandlers[dep[i]] = errorHandler;
               }
              } else {
               hot._acceptedDependencies[dep] = callback || function ({};
               hot._acceptedErrorHandlers[dep] = errorHandler;
              }
             },
          // module.hot.accept 其實(shí)等價(jià)于 module.hot._acceptedDependencies('./child) = render
          // 業(yè)務(wù)邏輯實(shí)現(xiàn)
          module.hot.accept('./child', () => {
            console.log('老板好,child 模塊更新啦~')
          })

          accept 往hot._acceptedDependencies這個(gè)對象里存入局部更新的 callback, 當(dāng)模塊改變時(shí),對模塊需要做的變更,搜集到_acceptedDependencies中,同時(shí)當(dāng)被監(jiān)聽的模塊內(nèi)容發(fā)生了改變以后,父模塊可以通過_acceptedDependencies知道哪些內(nèi)容發(fā)生了變化。

          實(shí)現(xiàn) JS 模塊替換

          當(dāng)了解了 accpet 方法以后,其實(shí)我們要考慮的事情就非常簡單了,也就是如何實(shí)現(xiàn) cb 里的業(yè)務(wù)邏輯,其實(shí)當(dāng) accept 方法執(zhí)行了以后,在其回調(diào)里是可以獲取到最新的被修改了以后的模塊的函數(shù)內(nèi)容的

          // ./src/main.js
          import createChild from './child'

          console.log(createChild) // 未更新前的函數(shù)內(nèi)容
          module.hot.accept('./child', ()=> {
           console.log(createChild) // 此時(shí)已經(jīng)可以獲取更新以后的函數(shù)內(nèi)容
          })

          既然是可以獲取到最新的函數(shù)內(nèi)容 其實(shí)也就很簡單了 我們只需要移除之前的 dom 節(jié)點(diǎn) 并替換為最新的 dom 節(jié)點(diǎn)即可,同時(shí)我們也需要記錄節(jié)點(diǎn)里的內(nèi)容狀態(tài),當(dāng)節(jié)點(diǎn)替換為最新的節(jié)點(diǎn)以后,追加更新原本的內(nèi)容狀態(tài)

          // ./src/main.js
          import createChild from './child'

          const child = createChild()
          document.body.appendChild(child)

          // 這里需要額外注意的是,child 變量每一次都會(huì)被移除,所以其實(shí)我們一個(gè)記錄一下每次被修改前的 child
          let lastChild = child
          module.hot.accept('./child', ()=> {
            // 記錄狀態(tài)
            const value = lastChild.innerHTML
            // 刪除節(jié)點(diǎn)
           document.body.remove(child)
            // 創(chuàng)建最新節(jié)點(diǎn)
            lastChild = createChild()
            // 恢復(fù)狀態(tài)
            lastChild.innerHTMl = value
            // 追加內(nèi)容
            document.body.appendChild(lastChild)
          })

          到這里為止,對于如何手動(dòng)實(shí)現(xiàn)一個(gè) child 模塊的熱更新替換邏輯已經(jīng)全部實(shí)現(xiàn)完畢了,有興趣的同學(xué)可以自己也手動(dòng)實(shí)現(xiàn)一下~

          :::tips tips: 手動(dòng)處理 HMR 邏輯過程中 如果 HMR 過程中出現(xiàn)報(bào)錯(cuò) 導(dǎo)致的 HRM 失效,其實(shí)只需要在配置文件中將hot: true 修改為 hotOnly: true即可 :::

          寫在最后

          希望通過這篇文章,能夠幫助到大家加深對 HMR 的理解,同時(shí)解決一下開發(fā)場景會(huì)遇到的問題(例如 脫離框架自己實(shí)現(xiàn)模塊熱更新),最后,歡迎大家一鍵三連~??????



          往期推薦

          Vite 太快了,煩死了,是時(shí)候該小睡一會(huì)了。


          如何實(shí)現(xiàn)比 setTimeout 快 80 倍的定時(shí)器?


          萬字長文!總結(jié)Vue 性能優(yōu)化方式及原理


          90 行代碼的 webpack,你確定不學(xué)嗎?


          最后





          如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個(gè)小忙:

          1. 點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)

          2. 歡迎加我微信「huab119」拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...

          3. 關(guān)注公眾號「前端勸退師」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。




          點(diǎn)個(gè)在看支持我吧,轉(zhuǎn)發(fā)就更好了



          瀏覽 55
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  自拍偷拍五月天 | 无码人妻一区二区三区四区老鸭窝 | 毛片免费网站 | 变态骚逼人妻3p露脸合集 | 北条麻妃无码精品 |