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

          【W(wǎng)ebpack 進(jìn)階】聊聊 Webpack 熱更新以及原理

          共 19772字,需瀏覽 40分鐘

           ·

          2021-03-17 11:28

          什么是熱更新

          模塊熱替換 (hot module replacement 或 HMR) 是 webpack 提供的最有用的功能之一。它允許在運(yùn)行時(shí)更新所有類型的模塊,而無需完全刷新

          一般的刷新我們分兩種:

          • 一種是頁面刷新,不保留頁面狀態(tài),就是簡單粗暴,直接 window.location.reload()。
          • 另一種是基于 WDS (Webpack-dev-server) 的模塊熱替換,只需要局部刷新頁面上發(fā)生變化的模塊,同時(shí)可以保留當(dāng)前的頁面狀態(tài),比如復(fù)選框的選中狀態(tài)、輸入框的輸入等。

          可以看到相比于第一種,熱更新對于我們的開發(fā)體驗(yàn)以及開發(fā)效率都具有重大的意義

          HMR 作為一個(gè) Webpack 內(nèi)置的功能,可以通過 HotModuleReplacementPlugin--hot 開啟。

          具體我們?nèi)绾卧? webpack  中使用這個(gè)功能呢?

          熱更新的使用以及簡單分析

          如何使用熱更新

          npm install webpack webpack-dev-server --save-dev

          設(shè)置 HotModuleReplacementPlugin,HotModuleReplacementPluginwebpack  是自帶的

          plugins: {
              HotModuleReplacementPluginnew webpack.HotModuleReplacementPlugin()
          }

          再設(shè)置一下 devServer

          devServer: {
              contentBase: path.resolve(__dirname, 'dist'),
              hottrue// 重點(diǎn)關(guān)注
              historyApiFallbacktrue,
              compresstrue
          }
          • hottrue,代表開啟熱更新

          兩個(gè)重要的文件

          當(dāng)我們改變我們項(xiàng)目的文件的時(shí)候,比如我修改 Vue 的一個(gè) 方法:

          更改前:

          clickMe() {
            console.log('我是 Gopal,歡迎關(guān)注「前端雜貨鋪」');
          }

          更改后:

          clickMe() {
            console.log('我是 Gopal,歡迎關(guān)注「前端雜貨鋪」,一起學(xué)習(xí)成長吧');
          }

          瀏覽器會(huì)去請求兩個(gè)文件

          接下來我們看看這兩個(gè)文件:

          • JSON 文件,h  代表本次新生成的 Hash 值為 0c256052432b51ed32c8—— 本次輸出的 Hash 值會(huì)被作為下次熱更新的標(biāo)識(shí)。c 表示當(dāng)前要熱更新的文件對應(yīng)的是哪個(gè)模塊,可以讓  webpack  知道它要更新哪個(gè)模塊
          {
              "h""0c256052432b51ed32c8",
              "c": {
                  "201"true
              }
          }
          • js 文件,就是本次修改的代碼,重新編譯打包后的,大致是下面這個(gè)樣子(已刪減一些并格式化過,這里看不懂沒關(guān)系的,就記住是返回要更新的模塊就好了),webpackHotUpdate 方法就是用來更新模塊的,201 對應(yīng)的是哪個(gè)模塊(我們稱它為模塊標(biāo)識(shí)),其他的就是要更新的模塊的內(nèi)容了
          webpackHotUpdate(201, {
            "./src/views/moveTransfer/list/index.vue?vue&type=script&lang=js&"function (
              module,
              exports,
              __webpack_require__
            
          {
              "use strict";

              var _Object$defineProperty = __webpack_require__(
                /*! @babel/runtime-corejs3/core-js-stable/object/define-property */ "./node_modules/@babel/runtime-corejs3/core-js-stable/object/define-property.js"
              );

              _Object$defineProperty(exports, "__esModule", {
                valuetrue,
              });

              exports.default = void 0;

              var _default = {
                datafunction data({
                  return {};
                },
                computed: {},
                methods: {
                  clickMefunction clickMe({
                    console.log("我是 Gopal,歡迎關(guān)注「前端雜貨鋪」,一起學(xué)習(xí)成長吧");
                  },
                },
              };
              exports.default = _default;
            },
          });

          那么問題來了,我修改了文件,瀏覽器是怎么知道要更新的呢?

          了解一下 Websocket

          熱更新使用到了 Websocket,這里不會(huì)細(xì)講 Websocket,可以看下阮一峰老師的  WebSocket 教程 [1],下面是一個(gè) 簡單的例子 [2]

          // 執(zhí)行上面語句之后,客戶端就會(huì)與服務(wù)器進(jìn)行連接。
          var ws = new WebSocket("wss://echo.websocket.org");

          // 實(shí)例對象的 onopen 屬性,用于指定連接成功后的回調(diào)函數(shù)
          ws.onopen = function(evt
            console.log("Connection open ..."); 
            ws.send("Hello WebSockets!");
          };

          // 實(shí)例對象的 onmessage 屬性,用于指定收到服務(wù)器數(shù)據(jù)后的回調(diào)函數(shù)??梢越邮芏M(jìn)制數(shù)據(jù),blob 對象或者 Arraybuffer 對象
          ws.onmessage = function(evt{
            console.log( "Received Message: " + evt.data);
            ws.close();
          };

          // 實(shí)例對象的 onclose 屬性,用于指定連接關(guān)閉后的回調(diào)函數(shù)。
          ws.onclose = function(evt{
            console.log("Connection closed.");
          };      

          上面通過 new Websocket 創(chuàng)建一個(gè)客戶端與服務(wù)端通信的實(shí)例,并通過 onmessage 屬性,接受指定服務(wù)器返回的數(shù)據(jù),并進(jìn)行相應(yīng)的處理。

          這里大概解釋下,為什么是 Websocket ?因?yàn)?Websocket 是一種雙向協(xié)議,它最大的特點(diǎn)就是 服務(wù)器可以主動(dòng)向客戶端推送消息,客戶端也可以主動(dòng)向服務(wù)器發(fā)送信息。這是 HTTP 不具備的,熱更新實(shí)際上就是服務(wù)器端的更新通知到客戶端,所以選擇了 Websocket

          接下來讓我們進(jìn)一步的討論關(guān)于熱更新的原理

          熱更新原理

          熱更新的過程

          幾個(gè)重要的概念(這里有一個(gè)大致的概念就好,后面會(huì)把它們串起來):

          • Webpack-complierwebpack 的編譯器,將 JavaScript 編譯成 bundle(就是最終的輸出文件)
          • HMR Server:將熱更新的文件輸出給 HMR Runtime
          • Bunble Server:提供文件在瀏覽器的訪問,也就是我們平時(shí)能夠正常通過 localhost 訪問我們本地網(wǎng)站的原因
          • HMR Runtime:開啟了熱更新的話,在打包階段會(huì)被注入到瀏覽器中的 bundle.js,這樣 bundle.js 就可以跟服務(wù)器建立連接,通常是使用 websocket ,當(dāng)收到服務(wù)器的更新指令的時(shí)候,就去更新文件的變化
          • bundle.js:構(gòu)建輸出的文件

          啟動(dòng)階段

          文件經(jīng)過 Webpack-complier 編譯好后傳輸給 Bundle Server,Bundle Server 可以讓瀏覽器訪問到我們打包出來的文件

          下面流程圖中的 1、2、A、B 階段

          文件熱更新階段

          文件經(jīng)過 Webpack-complier 編譯好后傳輸給 HMR ServerHMR Server 知道哪個(gè)資源 (模塊) 發(fā)生了改變,并通知 HMR Runtime 有哪些變化(也就是上面我們看到的兩個(gè)請求),HMR Runtime 就會(huì)更新我們的代碼,這樣我們?yōu)g覽器就會(huì)更新并且不需要刷新

          下面流程圖的 1、2、3、4、5 階段

          參考 19 | webpack 中的熱更新及原理分析 [3]

          深入 —— 源碼閱讀

          我們還看回上圖,其中啟動(dòng)階段圖中的 1、2、A、B 階段就不講解了,主要看熱更新階段主要講 3、4 和 5 階段

          在開始接下開的閱讀前,我們再回到最初的問題上我本地修改了文件,瀏覽器是怎么知道要更新的呢?

          通過上面的流程圖,其實(shí)我們可以猜測,本地實(shí)際上啟動(dòng)了一個(gè) HMR Server 服務(wù),而且在啟動(dòng) Bundle Server 的時(shí)候已經(jīng)往我們的 bundle.js 中注入了 HMR Runtime(主要用來啟動(dòng) Websocket,接受 HMR Server 發(fā)來的變更)

          所以我們聚焦以下幾點(diǎn):

          • Webpack 如何啟動(dòng)了 HMR Server
          • HMR Server 如何跟 HMR Runtime 進(jìn)行通信的
          • HMR Runtime 接受到變更之后,如何生效的

          以下的源碼解析分別對應(yīng)的版本是:

          • webpack——5.24.3
          • webpack-dev-server——4.0.0-beta.0
          • webpack-dev-middleware——4.1.0

          啟動(dòng) HMR Server

          這個(gè)工作主要是在 webpack-dev-server 中完成的

          lib/Server.js setupApp 方法,下面的 express 服務(wù)實(shí)際上對應(yīng)的是 Bundle Server

          setupApp() {
            // Init express server
            // eslint-disable-next-line new-cap
            // 初始化 express 服務(wù)
            // 使用 express 框架啟動(dòng)本地 server,讓瀏覽器可以請求本地的靜態(tài)資源。
            this.app = new express();
          }

          啟動(dòng)服務(wù)結(jié)束之后就通過 createSocketServer 創(chuàng)建 websocket 服務(wù)

          listen(port, hostname, fn) {
            this.hostname = hostname;
            return (
              findPort(port || this.options.port)
                .then((port) => {
                  this.port = port;
                  return this.server.listen(port, hostname, (err) => {
                    if (this.options.hot || this.options.liveReload) {
                      // 啟動(dòng) express 服務(wù)之后,啟動(dòng) websocket 服務(wù)
                      this.createSocketServer();
                    }
                  });
                })
            );
          }
          createSocketServer() {
            this.socketServer = new this.SocketServerImplementation(this);

            this.socketServer.onConnection((connection, headers) => {
           
            });
          }

          HMR Server 和 HMR Runtime 的通信

          首先要通信的第一個(gè)問題在于 —— 通信的時(shí)機(jī),什么時(shí)候我去通知客戶端我的文件更新。通過 webpack 創(chuàng)建的 compiler 實(shí)例(監(jiān)聽本地文件的變化、文件改變自動(dòng)編譯、編譯輸出),可以往 compiler.hooks.done 鉤子(代表 webpack 編譯完之后觸發(fā))注冊事件, 當(dāng)監(jiān)聽到一次 webpack 編譯結(jié)束,就會(huì)調(diào)用 sendStats 方法

          lib/Server.js 中的 setupHooks 方法

          // lib/Server.js
          // 綁定監(jiān)聽事件
          setupHooks() {
            // ...
            const addHooks = (compiler) => {
              // 監(jiān)聽 webpack 的 done 鉤子,tapable 提供的監(jiān)聽方法
              // done 標(biāo)識(shí)編譯結(jié)束
              const { compile, invalid, done } = compiler.hooks;
              compile.tap('webpack-dev-server', invalidPlugin);
              invalid.tap('webpack-dev-server', invalidPlugin);
              done.tap('webpack-dev-server', (stats) => {
                // 當(dāng)監(jiān)聽到一次webpack編譯結(jié)束,就會(huì)調(diào)用 sendStats 方法
                this.sendStats(this.sockets, this.getStats(stats));
                this.stats = stats;
              });
            };
          }

          當(dāng)監(jiān)聽到一次 webpack 編譯結(jié)束,就會(huì)調(diào)用 sendStats 方法,里面會(huì)向客戶端發(fā)送 hashok 事件

          // lib/Server.js
          // send stats to a socket or multiple sockets
          sendStats(sockets, stats, force) {
            // ok和 hash
            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-src/default/index.js 中,會(huì)去更新 hash,并且在 ok 的時(shí)候去進(jìn)行檢查更新 reloadApp

          // client-src/default/index.js 
          const onSocketMessage = {
            // 更新 current Hash
            hash(hash) {
              status.currentHash = hash;
            },
            'progress-update'function progressUpdate(data{
              if (options.useProgress) {
                log.info(`${data.percent}% - ${data.msg}.`);
              }

              sendMessage('Progress', data);
            },
            ok() {
              sendMessage('Ok');

              if (options.useWarningOverlay || options.useErrorOverlay) {
                overlay.clear();
              }

              if (options.initial) {
                return (options.initial = false);
              }
              // 進(jìn)行更新檢查等操作
              reloadApp(options, status);
            }

          };

          接下來我們看看 client-src/default/utils/reloadApp.js 中的 reloadApp。這里又利用 node.jsEventEmitter,發(fā)出 webpackHotUpdate 消息。這里又將更新的事情給回了 webpack(為了更好的維護(hù)代碼,以及職責(zé)劃分的更明確。)

          function reloadApp(
            { hotReload, hot, liveReload },
            { isUnloading, currentHash }
          {
            // ...
            if (hot) {
              log.info('App hot update...');
              //  hotEmitter 其實(shí)就是 EventEmitter 的實(shí)例
              const hotEmitter = require('webpack/hot/emitter');
              // 又利用 node.js 的 EventEmitter,發(fā)出 webpackHotUpdate 消息。
              // websocket 僅僅用于客戶端(瀏覽器)和服務(wù)端進(jìn)行通信。而真正做事情的活還是交回給了 webpack。
              hotEmitter.emit('webpackHotUpdate', currentHash);
              if (typeof self !== 'undefined' && self.window) {
                // broadcast update to window
                self.postMessage(`webpackHotUpdate${currentHash}`'*');
              }
            }
            // ...
          }

          module.exports = reloadApp;

          webpack  的 hot/dev-server.js 中,監(jiān)聽 webpackHotUpdate 事件,并執(zhí)行 check 方法。并在 check 方法中調(diào)用 module.hot.check 方法進(jìn)行熱更新。

          // hot/dev-server.js
          // 監(jiān)聽webpackHotUpdate事件
          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({
            //  moudle.hot.check 開始熱更新
            // 之后的源碼都是HotModuleReplacementPlugin塞入到bundle.js中的哦,我就不寫文件路徑了
            module.hot
              .check(true)
              .then(function (updatedModules{
                // ...
              })
              .catch(function (err{
                // ...
              });
          };

          至于 module.hot.check ,實(shí)際上通過 HotModuleReplacementPlugin 已經(jīng)注入到我們 chunk 中了(也就是我們上面所說的 HMR Runtime),所以后面就是它是如何更新 bundle.js 的呢?

          HMR Runtime 中更新 bundle.js

          如果我們仔細(xì)看我們的打包后的文件的話,開啟熱更新之后生成的代碼會(huì)比不開啟多出很多東西(為了更加直觀看到,可以將其輸出到本地),這些就是幫助 webpack 在瀏覽器端去更新 bundle.jsHMR Runtime 代碼

          來看打包后的代碼中新增了一個(gè) createModuleHotObject

          module.hot = createModuleHotObject(options.id, module);

          實(shí)際上這個(gè)函數(shù)就是用來返回一個(gè) hot 對象,所以調(diào)用 module.hot.check 的時(shí)候,實(shí)際上就是執(zhí)行 hotCheck 函數(shù)

          function createModuleHotObject(moduleId, me{
            var hot = {
              // Module API
              addDisposeHandlerfunction (callback{
                hot._disposeHandlers.push(callback);
              },
              removeDisposeHandlerfunction (callback{
                var idx = hot._disposeHandlers.indexOf(callback);
                if (idx >= 0) hot._disposeHandlers.splice(idx, 1);
              },
              // Management API
              check: hotCheck,
              apply: hotApply,
              statusfunction (l{
                if (!l) return currentStatus;
                registeredStatusHandlers.push(l);
              },
              addStatusHandlerfunction (l{
                registeredStatusHandlers.push(l);
              },
              removeStatusHandlerfunction (l{
                var idx = registeredStatusHandlers.indexOf(l);
                if (idx >= 0) registeredStatusHandlers.splice(idx, 1);
              },
            };
            currentChildModule = undefined;
            return hot;
          }

          其中就有 hotCheck 中調(diào)用了 __webpack_require__.hmrM

          function hotCheck(applyOnUpdate{
            setStatus("check");
              return __webpack_require__.hmrM().then(function (update{
            }
          }

          __webpack_require__.hmrM—— 加載.hot-update.json

          來看 __webpack_require__.hmrM, 其中 __webpack_require__.p 指的是我們本地服務(wù)的域名,類似 http://0.0.0.0:9528 , 另外 __webpack_require__.hmrF 去獲取 .hot-update.json 文件的地址,就是我們之前提到的重要文件之一

          __webpack_require__.hmrM = () => {
            if (typeof fetch === "undefined"throw new Error("No browser support: need fetch API");
            return fetch(__webpack_require__.p + __webpack_require__.hmrF()).then((response) => {
              if(response.status === 404return// no update available
              if(!response.ok) throw new Error("Failed to fetch update manifest " + response.statusText);
              return response.json();
            });
          };
          /* webpack/runtime/get update manifest filename */
          (() => {
            __webpack_require__.hmrF = () => ("main." + __webpack_require__.h() + ".hot-update.json");
          })();

          加載要更新的模塊

          下面來看如何加載我們要更新的模塊的,可以看到打包出來的代碼中有 loadUpdateChunk

          function loadUpdateChunk(chunkId{
            return new Promise((resolve, reject) => {
              var url = __webpack_require__.p + __webpack_require__.hu(chunkId);
              // create error before stack unwound to get useful stacktrace later
              var error = new Error();
              var loadingEnded = (event) => {
                // ...加載后的處理
              };
              __webpack_require__.l(url, loadingEnded);
            });
          }

          再來看 __webpack_require__.l,主要通過類似 JSONP 的方式進(jìn)行,因?yàn)?JSONP 獲取的代碼可以直接執(zhí)行。

          __webpack_require__.l = (url, done, key, chunkId) => {
            // ...
            if (!script) {
              script = document.createElement("script");

              script.charset = "utf-8";
              script.timeout = 120;
              if (__webpack_require__.nc) {
                script.setAttribute("nonce", __webpack_require__.nc);
              }
              script.setAttribute("data-webpack", dataWebpackPrefix + key);
              script.src = url;
            }
            // ...
            needAttach && document.head.appendChild(script);
          };

          還記得我們一開始提到的返回的 JS 中就是一個(gè) webpackHotUpdate 函數(shù)么?實(shí)際上在我們的 HMR Runtime 中就是全局定義了(下面的名稱是 webpackHotUpdatelearn_hot_reload,應(yīng)該是 webpack 版本不一樣導(dǎo)致的,不影響理解)至于生成的代碼是如何生效的,請移步我的另外一篇文章 ——【W(wǎng)ebpack 進(jìn)階】Webpack 打包后的代碼是怎樣的?[4]

          // webpackHotUpdate + 項(xiàng)目名
          self["webpackHotUpdatelearn_hot_reload"] = (chunkId, moreModules, runtime) => {
            for(var moduleId in moreModules) {
              if(__webpack_require__.o(moreModules, moduleId)) {
                currentUpdate[moduleId] = moreModules[moduleId];
                if(currentUpdatedModulesList) currentUpdatedModulesList.push(moduleId);
              }
            }
            if(runtime) currentUpdateRuntime.push(runtime);
            if(waitingUpdateResolves[chunkId]) {
              waitingUpdateResolves[chunkId]( "chunkId");
              waitingUpdateResolves[chunkId] = undefined;
            }
          };

          所以,客戶端接受到服務(wù)器端推動(dòng)的消息后,如果需要熱更新,瀏覽器發(fā)起 http 請求去服務(wù)器端獲取新的模塊資源解析并局部刷新頁面

          以上整體的流程如下所示:

          總結(jié)

          本文介紹了 webpack 熱更新的簡單使用、相關(guān)的流程以及原理。小結(jié)一下,webpack 如果開啟了熱更新的時(shí)候

          • HMR Runtime 通過 HotModuleReplacementPlugin 已經(jīng)注入到我們 chunk 中了

          • 除了開啟一個(gè) Bundle Server,還開啟了 HMR Server,主要用來和 HMR Runtime 中通信

          • 在編譯結(jié)束的時(shí)候,通過 compiler.hooks.done,監(jiān)聽并通知客戶端

          • 客戶端接收到之后,就會(huì)調(diào)用  module.hot.check 等,發(fā)起 http 請求去服務(wù)器端獲取新的模塊資源解析并局部刷新頁面

          參考

          • 模塊熱替換 (hot module replacement)[5]
          • 輕松理解 webpack 熱更新原理 [6]
          • WebSocket 教程 [7]
          • 搞懂 webpack 熱更新原理 [8]
          • 看完這篇,再也不怕被問 Webpack 熱更新 [9]
          • 從零實(shí)現(xiàn) webpack 熱更新 HMR[10]

          參考資料

          [1]

          WebSocket 教程: https://www.ruanyifeng.com/blog/2017/05/websocket.html

          [2]

          簡單的例子: https://jsbin.com/muqamiqimu/edit?js,console

          [3]

          19 | webpack 中的熱更新及原理分析: https://time.geekbang.org/course/detail/100028901-98391

          [4]

          【W(wǎng)ebpack 進(jìn)階】Webpack 打包后的代碼是怎樣的?: https://juejin.cn/post/6937086236926410783

          [5]

          模塊熱替換 (hot module replacement): https://webpack.docschina.org/concepts/hot-module-replacement/

          [6]

          輕松理解 webpack 熱更新原理: https://juejin.cn/post/6844904008432222215

          [7]

          WebSocket 教程: https://www.ruanyifeng.com/blog/2017/05/websocket.html

          [8]

          搞懂 webpack 熱更新原理: https://juejin.cn/post/6844903933157048333#heading-29

          [9]

          看完這篇,再也不怕被問 Webpack 熱更新: https://www.infoq.cn/article/dioufdrtt3rocojvlrcl

          [10]

          從零實(shí)現(xiàn) webpack 熱更新 HMR: https://time.geekbang.org/course/detail/100028901-98391


          瀏覽 50
          點(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>
                  国产又大又黄 | 无码国精品一区二区免费蜜桃 | 影音先锋女人av鲁色资源久久 | 影音先锋成人资源AV在线观看 | 五月天激情久久 |