webpack-dev-server 源碼解析

webpack-dev-server

簡介

Use webpack with a development server that provides live reloading. This should be used for development only.
It uses webpack-dev-middleware under the hood, which provides fast in-memory access to the webpack assets.
將webpack與提供實時從新加載的開發服務器一塊兒使用。 這應該僅用於開發。
它使用了引擎蓋下的webpack-dev-middleware,它提供了對webpack資產的快速內存訪問。

應用

Getting Startedphp

源碼分析解讀

1. 結論:熱更新的流程

  1. webpack在構建項目時會建立服務端(server基於node)和客戶端(client一般指瀏覽器),項目正式啓動運行時雙方會經過socket保持鏈接,用來知足先後端實時通信。
  2. 當咱們保存代碼時,會被webpack監聽到文件變化,觸發新一輪編譯生成新的編譯器compiler(就是咱們所說的Tapable實例。它混合在一塊兒Tapable來吸取功能來註冊和調用插件自己,能夠理解爲一個狀態機。)。
  3. 在compiler的’done’鉤子函數(生命週期)裏調用_sendStats發放向client發送okwarning消息,並同時發送向client發送hash值,在client保存下來。
  4. client接收到okwarning消息後調用reloadApp發佈客戶端檢查更新事件(webpackHotUpdate
  5. webpack/hot部分監聽到webpackHotUpdate事件,調用check方法進行hash值對比以及檢查各modules是否須要更新。如需更新會調用hotDownloadManifest方法下載json(manifest)文件。
  6. hotDownloadManifest完成後調用hotDownloadUpdateChunk方法,經過jsonp的方式加載最新的chunk,以後分析對比文件進行文件的更新替換,完成整個熱更新流程。

注:如下源碼分析採用倒敘分析方式node

2. webpack/hot 源碼解讀

在webpack構建項目時,webpack-dev-server會在編譯後js文件加上兩個依賴文件:webpack

/***/ 
(function(module, exports, __webpack_require__) {
  // 創建socket鏈接,保持先後端實時通信
  __webpack_require__("./node_modules/webpack-dev-server/client/index.js?http://localhost:8080");
    // dev-server client熱更新的方法
  __webpack_require__("./node_modules/webpack/hot/dev-server.js");
  module.exports = __webpack_require__("./src/index.js");
  /***/
})
  • webpack/hot/dev-server.js的文件內容:git

    /*
      MIT License http://www.opensource.org/licenses/mit-license.php
      Author Tobias Koppers @sokra
    */
    /*globals window __webpack_hash__ */
    if (module.hot) {
      var lastHash;
      //__webpack_hash__是每次編譯的hash值是全局的,就是放在window上
      var upToDate = function upToDate() {
        return lastHash.indexOf(__webpack_hash__) >= 0;
      };
      var log = require("./log");
      var check = function check() {
        module.hot
          .check(true)  // 這裏的check方法最終進入到webpack\lib\HotModuleReplacement.runtime.js文件中
          .then(function(updatedModules) {
            //檢查全部要更新的module,若是沒有module要更新那麼返回null
            if (!updatedModules) {
              log("warning", "[HMR] Cannot find update. Need to do a full reload!");
              log(
                "warning",
                "[HMR] (Probably because of restarting the webpack-dev-server)"
              );
              window.location.reload();
              return;
            }
            //檢測時候還有module須要更新,若是有=>check()
            if (!upToDate()) {
              check();
            }
            //打印更新結果,全部須要更新的module和已經被更新的module都是updatedModules
            require("./log-apply-result")(updatedModules, updatedModules);
    
            if (upToDate()) {
              log("info", "[HMR] App is up to date.");
            }
          })
          .catch(function(err) {
            //若是報錯直接全局reload
            var status = module.hot.status();
            if (["abort", "fail"].indexOf(status) >= 0) {
              log(
                "warning",
                "[HMR] Cannot apply update. Need to do a full reload!"
              );
              log("warning", "[HMR] " + (err.stack || err.message));
              window.location.reload();
            } else {
              log("warning", "[HMR] Update failed: " + (err.stack || err.message));
            }
          });
      };
      //獲取MyEmitter對象
      var hotEmitter = require("./emitter");
      //監聽‘webpackHotUpdate’事件
      hotEmitter.on("webpackHotUpdate", function(currentHash) {
        lastHash = currentHash;
        //根據兩點判斷是否須要檢查modules以及更新
        // 1 對比服務端傳過來的最新hash值和客戶端的__webpack_hash__是否一致
        // 2 調用module.hot.status方法獲取狀態 是否爲 ‘idle’
        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.");
    }
  • 加載最新chunk的源碼分析:github

    • 方法調用關係:module.hot.check=>HotModuleReplacement=>hotDownloadManifest=>hotEnsureUpdateChunk=>hotDownloadUpdateChunk
    • HotModuleReplacement模塊內容:web

      ...
      function hotCheck(apply) {
        ...
        return hotDownloadManifest(hotRequestTimeout).then(function(update) {
          ...
          // 獲取到manifest後經過jsonp加載最新的chunk 
          /*foreachInstalledChunks*/
          // eslint-disable-next-line no-lone-blocks
          {
            /*globals chunkId */
            hotEnsureUpdateChunk(chunkId);
          }
          ...
        });
      }
      ...
      
      function hotEnsureUpdateChunk(chunkId) {
        if (!hotAvailableFilesMap[chunkId]) {
          hotWaitingFilesMap[chunkId] = true;
        } else {
          hotRequestedFilesMap[chunkId] = true;
          hotWaitingFiles++;
          hotDownloadUpdateChunk(chunkId);
        }
      }
    • webpack會根據不一樣運行環境(node / webworker / web)調用對應的方法(hotDownloadManifest & hotDownloadUpdateChunk)來加載chunk:,咱們主要考慮web環境下的方法json

      // eslint-disable-next-line no-unused-vars
          function webpackHotUpdateCallback(chunkId, moreModules) {
            //...
          } 
          //$semicolon
      
          // eslint-disable-next-line no-unused-vars
          //jsonp方法加載chunk
          function hotDownloadUpdateChunk(chunkId) {
            var script = document.createElement("script");
            script.charset = "utf-8";
            script.src = $require$.p + $hotChunkFilename$;
            if ($crossOriginLoading$) script.crossOrigin = $crossOriginLoading$;
            document.head.appendChild(script);
          }
      
          // eslint-disable-next-line no-unused-vars
          function hotDownloadManifest(requestTimeout) {
            //...
          }
    • log-apply-result模塊內容:後端

      /*
      MIT License http://www.opensource.org/licenses/mit-license.php
      Author Tobias Koppers @sokra
      */
      module.exports = function(updatedModules, renewedModules) {
        //renewedModules表示哪些模塊被更新了,剩餘的模塊表示,哪些模塊因爲 ignoreDeclined,ignoreUnaccepted配置沒有更新
        var unacceptedModules = updatedModules.filter(function(moduleId) {
          return renewedModules && renewedModules.indexOf(moduleId) < 0;
        });
        var log = require("./log");
        //哪些模塊沒法HMR,打印log
        //哪些模塊因爲某種緣由沒有更新成功。其中沒有更新的緣由多是以下的:
        //  ignoreUnaccepted
        //  ignoreDecline
        //  ignoreErrored
        if (unacceptedModules.length > 0) {
          log(
            "warning",
            "[HMR] The following modules couldn't be hot updated: (They would need a full reload!)"
          );
          unacceptedModules.forEach(function(moduleId) {
            log("warning", "[HMR]  - " + moduleId);
          });
        }
        //沒有模塊更新,表示模塊是最新的
        if (!renewedModules || renewedModules.length === 0) {
          log("info", "[HMR] Nothing hot updated.");
        } else {
          log("info", "[HMR] Updated modules:");
          //更新的模塊
          renewedModules.forEach(function(moduleId) {
            if (typeof moduleId === "string" && moduleId.indexOf("!") !== -1) {
              var parts = moduleId.split("!");
              log.groupCollapsed("info", "[HMR]  - " + parts.pop());
              log("info", "[HMR]  - " + moduleId);
              log.groupEnd("info");
            } else {
              log("info", "[HMR]  - " + moduleId);
            }
          });
          //每個moduleId都是數字那麼建議使用NamedModulesPlugin
          var numberIds = renewedModules.every(function(moduleId) {
            return typeof moduleId === "number";
          });
          if (numberIds)
            log(
              "info",
              "[HMR] Consider using the NamedModulesPlugin for module names."
            );
        }
      };

3. webpack-dev-server源碼解讀

webpack構建過程觸發module更新的時機瀏覽器

  • webpack-dev-server客戶端源碼的關鍵部分服務器

    ...
    ...
    const onSocketMsg = {
    ...
      ok() {
        sendMsg('Ok');
        if (useWarningOverlay || useErrorOverlay) overlay.clear();
        if (initial) return (initial = false); // eslint-disable-line no-return-assign
        reloadApp();
      },
      warnings(warnings) {
        log.warn('[WDS] Warnings while compiling.');
        const strippedWarnings = warnings.map((warning) => stripAnsi(warning));
        sendMsg('Warnings', strippedWarnings);
        for (let i = 0; i < strippedWarnings.length; i++) {
          log.warn(strippedWarnings[i]);
        }
        if (useWarningOverlay) overlay.showMessage(warnings);
    
        if (initial) return (initial = false); // eslint-disable-line no-return-assign
          reloadApp();
        },
      ...
      };
    
    ...
    
    function reloadApp() {
      if (isUnloading || !hotReload) {
        return;
      }
      if (hot) {
        log.info('[WDS] App hot update...');
        // eslint-disable-next-line global-require
        const hotEmitter = require('webpack/hot/emitter');
        hotEmitter.emit('webpackHotUpdate', currentHash);
        //從新啓動webpack/hot/emitter,同時設置當前hash
        if (typeof self !== 'undefined' && self.window) {
          // broadcast update to window
          self.postMessage(`webpackHotUpdate${currentHash}`, '*');
        } 
      } else {
        //若是不是Hotupdate那麼咱們直接reload咱們的window就能夠了
        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('[WDS] App updated. Reloading...');
      rootWindow.location.reload();
    }

    根據以上代碼 能夠看出:當客戶端接收到服務器端發送的ok和warning信息的時候,同時支持HMR的狀況下就會要求檢查更新,同時還收到服務器端本次編譯的hash值。咱們再看一下服務端是在什麼時機發送’ok’和’warning’消息。

  • webpack-dev-server服務端源碼的關鍵部分

    class Server {
      ...
      _sendStats(sockets, stats, force) {
        if (
          !force &&
          stats &&
          (!stats.errors || stats.errors.length === 0) &&
          stats.assets &&
          //每個asset都是沒有emitted屬性,表示沒有發生變化。若是發生變化那麼這個assets確定有emitted屬性
          stats.assets.every((asset) => !asset.emitted)
        ) {
          
          return this.sockWrite(sockets, 'still-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');
        }
      }
      ...
    }

    上面的代碼是發送‘ok’和‘warning’消息的方法,那具體在什麼時機調用此方法呢?

    class Server {
      constructor(compiler, options = {}, _log) {
        ...
        const addHooks = (compiler) => {
          const { compile, invalid, done } = compiler.hooks;
    
          compile.tap('webpack-dev-server', invalidPlugin);
          invalid.tap('webpack-dev-server', invalidPlugin);
          done.tap('webpack-dev-server', (stats) => {
            this._sendStats(this.sockets, stats.toJson(STATS));
            this._stats = stats;
          });
        };
    
        if (compiler.compilers) {
          compiler.compilers.forEach(addHooks);
        } else {
          addHooks(compiler);
        }
        ...
      }
    ...
    }

    再看這部分代碼,就能夠理解了,每次在compiler的’done’鉤子函數(生命週期)被調用的時候就會經過socket向客戶端發送消息,要求客戶端去檢查模塊更新完成HMR工做。

總結:最近正處於換工做階段,有些閒暇時間,正好拔草,以前工做的過程就很想看一下webpack-dev-server 的實現原理,趁此機會粗略學習了一下它的源碼,有什麼不對的地方還望指教,互相學習,加深理解。

相關文章
相關標籤/搜索