乐趣区

关于前端:webpack5之HMR原理探究

一、概念介绍

模块热替换 (hot module replacement 或 HMR) 是 webpack 提供的最有用的性能之一。它容许在运行时更新所有类型的模块,而无需齐全刷新。

次要是通过以下几种形式,来显著放慢开发速度:

  • 保留在齐全从新加载页面期间失落的应用程序状态。
  • 只更新变更内容,以节俭贵重的开发工夫。
  • 在源代码中 CSS/JS 产生批改时,会立即在浏览器中进行更新,这简直相当于在浏览器 devtools 间接更改款式。

留神:HMR 只能被利用到开发环境中。

本文示例代码运行环境:
node:12.20.1、webpack:5.37.1、webpack-cli:4.7.0、webpack-dev-server:3.11.2。

二、应用形式

  1. 更新 webpack.config.js 配置。

  2. devServer: {

    • port: 9000,
    • host: ‘127.0.0.1’,
    • hot: true,
  3. },
    plugins: [
    // 定义了 devServer.hot:true 后,能够省略不写,具体起因能够查看本文四、源码剖析 1.1 -> webpack-dev-server/lib/utils/addEntries.js 的代码
    // new webpack.HotModuleReplacementPlugin()
    ]

  4. package.json 增加 scripts。

webpack-cli 提供了三个启动 webpack-dev-server 命令:webpack serve、webpack s、webpack server,它们的作用是雷同的。具体起因能够查看本文四、源码剖析 -> webpack-cli/lib/webpack-cli.js 的代码。

"scripts": {+ "dev": "webpack serve",}
  1. 在 index.js 中新增 module.hot?.accept 办法。
function render() {root.innerHTML = require('./print.js')
}
render()

+ module.hot?.accept(['./print.js'], render)
  1. 批改 print.js。
 function printMe() {- console.log('Updating print.js');
  + console.log('Updating print.js 1');
 }

 module.exports = printMe

三、源码调试

  1. package.json 中减少 scripts。

    "scripts": {+ "debug": "node ./node_modules/.bin/webpack serve"}
  2. vscode 中关上 debug 面板,新建一个 launch.json 配置,启动 debug。

    {
      "version": "0.2.0",
      "configurations": [
     {
       "type": "node",
       "request": "launch",
       "name": "debug",
       "runtimeExecutable": "npm",
       "runtimeArgs": [
         "run-script",
         "debug"
       ],
       "skipFiles": ["<node_internals>/**"],
       // cwd 设置为须要调试我的项目的根目录
       "cwd": "${workspaceFolder}/webpack/hmr/test"
     }
      ]
    }

    四、源码剖析

    以下内容次要是以执行 webpack serve 命令后,各个模块(文件)次要做了什么事件,来开展论述。

webpack serve
-> ./node_modules/./bin/webpack 中 runCli (line 48)

const runCli = cli => {const pkg = require('webpack-cli/package.json');
    require('webpack-cli/bin/cli.js');
};

-> webpack-cli/bin/cli.js (line 25)

  runCLI(process.argv);

-> webpack-cli/lib/bootstrap.js (line 4)

  const cli = new WebpackCLI();
  await cli.run(args);

-> webpack-cli/lib/webpack-cli.js (line 783)

  const externalBuiltInCommandsInfo = [
    {name: 'serve [entries...]',
      alias: ['server', 's'],
      pkg: '@webpack-cli/serve',
    }]
  const loadCommandByName = async (commandName) => {require(pkg);
  }
  this.webpack = require('webpack');
  // line 1834
  createCompiler(options, callback) {let compiler = this.webpack(options}
    return compiler;
  }

-> @webpack-cli/serve/index.js (line 6)

  const startDevServer_1 = __importDefault(require("./startDevServer"));
  // line 81
  const compiler = await cli.createCompiler();
  await startDevServer_1.default(compiler);

-> @webpack-cli/serve/startDevServer.js (line 92)

  Server = require('webpack-dev-server/lib/Server');
  const server = new Server(compiler, options);
  server.listen(options.port, options.host, (error) => {if (error) {throw error;}
  });

-> webpack-dev-server/lib/Server.js (line 92)

  // 批改 entry 和 plugins line 72
  updateCompiler(this.compiler, this.options);
  // 监听 compiler.hooks.done line 182
  const {done} = compiler.hooks;
  done.tap('webpack-dev-server', (stats) => {this._sendStats(this.sockets, this.getStats(stats));
    this._stats = stats;
  });
  // 实例化 express line 168
  this.app = new express();
  // 调用 webpack-dev-middleware line 207
  webpackDevMiddleware(
    this.compiler,
    Object.assign({}, this.options, { logLevel: this.log.options.level})
  );
  // 创立 http server line 688
  this.listeningApp = http.createServer(this.app);
  // 设置 http listen line 774
  listen(port, hostname, fn) {
    this.hostname = hostname;
    return this.listeningApp.listen(port, hostname, (err) => {
      // 创立 socket server
      this.createSocketServer();});
  }
  // 实例化 socket server line 696
  createSocketServer() {
    const SocketServerImplementation = this.socketServerImplementation;
    this.socketServer = new SocketServerImplementation(this);
    this.socketServer.onConnection((connection, headers) => {if (!connection) {return;}
      this.sockets.push(connection);
      if (this.hot) {
        // 通知 client 端,热更新服务启动结束
        this.sockWrite([connection], 'hot');
      }
    });
  }

1 -> webpack-dev-server/lib/utils/updateCompliler.js (line 48)

  addEntries(webpackConfig, options);
  compilers.forEach((compiler) => {
    const config = compiler.options;
    // 使 entry 批改失效
    compiler.hooks.entryOption.call(config.context, config.entry);
    // 使 plugins 批改失效
    providePlugin.apply(compiler);
  });

1.1 -> webpack-dev-server/lib/utils/addEntries.js (line 142)

  // config.entry 减少 webpack-dev-server/client/index.js line 39
  const clientEntry = `${require.resolve('../../client/')}?${domain}${sockHost}${sockPath}${sockPort}`;
  // config.entry 减少 webpack/hot/dev-server.js line 49
  hotEntry = require.resolve('webpack/hot/dev-server');
  config.entry = prependEntry(config.entry || './src', additionalEntries);
  // devServer.hot: true 会主动增加 HotModuleReplacementPlugin 插件 line 153
  config.plugins.push(new webpack.HotModuleReplacementPlugin());

1.1.1 -> webpack-dev-server/client/index.js (line 176)

代码会被打包进 app.bundle.js 中,在 client 端内实例化 websocket, 用来和 server 端的 ws 服务建设连贯,以便接管 webpack 每次编译的最新变动。

  var onSocketMessage = {
    // client 端接管 server 端发送 hot 指定,开启 HMR 模式 line 46
    hot: function hot() {
      options.hot = true;
      log.info('[WDS] Hot Module Replacement enabled.');
    },
    // 接管 webpack 每次编译的最新 hash line 63
    hash: function hash(_hash) {status.currentHash = _hash;},
    // 编译实现 line 107
    ok: function ok() {
      // 第一次编译不必 reloadApp
      if (options.initial) {return options.initial = false;}
      reloadApp(options, status);
    },
  }
  // client 端建设 webSocket 连贯, 有 sockjs、websocket、path 三种类型 line 176
  socket(socketUrl, onSocketMessage);

1.1.2 -> webpack-dev-server/client/utils/reloadApp.js (line 23)

  // 触发 webpackHotUpdate 事件 line 23
  var hotEmitter = require('webpack/hot/emitter');
  hotEmitter.emit('webpackHotUpdate', currentHash);

1.2.1 -> webpack/hot/dev-server.js (line 23)

代码会被打包进 app.bundle.js 中,设置 webpackHotUpdate 事件监听,在 webpackHotUpdate 事件被触发时执行热更新查看 check。

  // 触发 webpackHotUpdate 事件 line 23
  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();}
    });
  // 触发 module.hot.check() line 12
  var check = function check() {
        module.hot
            .check(true)
            .then(function (updatedModules) {if (!updatedModules) {window.location.reload();
                    return;
                }
            });
    };

2 -> webpack-dev-server/lib/utils/getSocketServerImplementation.js (line 3)

  // 提供 sockjs、ws、path 三种类型的 socket 服务
  if (options.transportMode.server === 'sockjs') {ServerImplementation = require('../servers/SockJSServer');
  } else if (options.transportMode.server === 'ws') {ServerImplementation = require('../servers/WebsocketServer');
  } else {
    try {
      // eslint-disable-next-line import/no-dynamic-require
      ServerImplementation = require(options.transportMode.server);
    } catch (e) {serverImplFound = false;}
  }

2.1 -> webpack-dev-server/lib/servers/WebsocketServer.js (line 12)

  // 实例化 ws 服务
  this.wsServer = new ws.Server({
    noServer: true,
    path: this.server.sockPath,
  });
  // http server 被 client 拜访后,ws server 触发 connection 事件 line 17
  this.server.listeningApp.on('upgrade', (req, sock, head) => {this.wsServer.handleUpgrade(req, sock, head, (connection) => {this.wsServer.emit('connection', connection, req);
    });
  });

3 -> webpack-dev-middleware/index.js (line 67)

  // 开启 watching 模式编译 line 41
  context.watching = compiler.watch(options.watchOptions, (err) => {if (err) {context.log.error(err.stack || err);
      if (err.details) {context.log.error(err.details);
      }
    }
  });
  // 设置文件系统 line 65
  setFs(context, compiler);
  // 提供动态资源服务 line 67
  middleware(context)

3.1 -> webpack-dev-middleware/middleware.js (line 96)

  // server content line 96
  let content = context.fs.readFileSync(filename);
  res.setHeader('Content-Type', contentType);
  res.statusCode = res.statusCode || 200;
  if (res.send) {res.send(content);
  } else {res.end(content);
  }

3.2 -> compiler.watch 首次编译生成 dist/app.bundle.js

  // 拦挡__webpack_require__申请,重写 require 申请,在 module 上新增 hot、parents、children 属性 webpack/lib/hmr/HotModuleReplacement.runtime.js line 39
  __webpack_require__.i.push(function (options) {
    var module = options.module;
    var require = createRequire(options.require, options.id);
    module.hot = createModuleHotObject(options.id, module);
    module.parents = currentParents;
    module.children = [];
    currentParents = [];
    options.require = require;
  });
  // 收集模块间的依赖关系,寄存在模块的 parents、children 属性里 webpack/lib/hmr/HotModuleReplacement.runtime.js line 52
  function createRequire(require, moduleId) {var me = installedModules[moduleId];
    if (!me) return require;
    var fn = function (request) {if (me.hot.active) {if (installedModules[request]) {var parents = installedModules[request].parents;
          if (parents.indexOf(moduleId) === -1) {parents.push(moduleId);
          }
        } else {currentParents = [moduleId];
          currentChildModule = request;
        }
        if (me.children.indexOf(request) === -1) {me.children.push(request);
        }
      } else {currentParents = [];
      }
      return require(request);
    };
    return fn;
  }
  // 创立 module.hot 对象 webpack/lib/hmr/HotModuleReplacement.runtime.js line 103
  function createModuleHotObject(moduleId, me) {
    var hot = {_acceptedDependencies: {}, // 收集 module.hot.accept 依赖
      active: true,
      accept: function (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 () {};
          }
        } else {hot._acceptedDependencies[dep] = callback || function () {};
        }
      },
      check: hotCheck,
      apply: hotApply,
    };
    return hot;
  }
  // 开启热更新查看 webpack/lib/hmr/HotModuleReplacement.runtime.js line 242
  function hotCheck(applyOnUpdate) {// __webpack_require__.hmrM = hmrDownloadManifest, fetch 申请 app.[hash].hot-update.json 文件
    return __webpack_require__.hmrM().then(function (update) {var updatedModules = [];
      currentUpdateApplyHandlers = [];
      return Promise.all(Object.keys(__webpack_require__.hmrC).reduce(function (
          promises,
          key
        ) {// __webpack_require__.hmrC = hmrDownloadUpdateHandlers,  创立 script 标签,用 jsonp 的形式去加载 app.[hash].hot-update.js 文件
          __webpack_require__.hmrC[key](
            update.c,
            update.r,
            update.m,
            promises,
            // 收集 applyHandlers
            currentUpdateApplyHandlers,
            updatedModules
          );
          return promises;
        },
          [])
      ).then(function () {return waitForBlockingPromises(function () {if (applyOnUpdate) {
            // 触发 applyHandlers
            return internalApply(applyOnUpdate);
          }
        });
      });
    });
  }
  // fetch 申请 app.[hash].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 === 404) return;
      if (!response.ok) throw new Error("Failed to fetch update manifest" + response.statusText);
      return response.json();});
  };
  __webpack_require__.hmrC.jsonp = function (
    chunkIds,
    removedChunks,
    removedModules,
    promises,
    applyHandlers,
    updatedModulesList
  ) {// 收集加载 app.[hash].hot-update.js 文件后的解决办法
    applyHandlers.push(applyHandler);
    chunkIds.forEach(function (chunkId) {
      if (__webpack_require__.o(installedChunks, chunkId) &&
        installedChunks[chunkId] !== undefined
      ) {
        // 创立 jsonp 工作
        promises.push(loadUpdateChunk(chunkId, updatedModulesList));
      }
    });
  };
  // 触发 module.hot._acceptedDependencies   webpack/lib/hmr/JavascriptHotModuleReplacement.runtime.js line 24
  function applyHandler(options) {
    var moduleOutdatedDependencies;
    return {apply: function (reportError) {for (var outdatedModuleId in outdatedDependencies) {if (__webpack_require__.o(outdatedDependencies, outdatedModuleId)) {var module = __webpack_require__.c[outdatedModuleId];
            if (module) {
              moduleOutdatedDependencies =
                outdatedDependencies[outdatedModuleId];
              for (var j = 0; j < moduleOutdatedDependencies.length; j++) {var dependency = moduleOutdatedDependencies[j];
                var acceptCallback =
                  // 清空 module.hot._acceptedDependencies 中的办法
                  module.hot._acceptedDependencies[dependency];
              }
            }
          }
        }
        return outdatedModules;
      }
    };
  }
  // 拼接 jsonp 申请 app.[hash].hot-update.js 门路
  function loadUpdateChunk(chunkId) {return new Promise((resolve, reject) => {var url = __webpack_require__.p + __webpack_require__.hu(chunkId);
      __webpack_require__.l(url, loadingEnded);
    });
  }
  // 创立 script 标签来加载 js 文件
  __webpack_require__.l = (url, done, key, chunkId) => {
    var script, needAttach;
    if (!script) {
      needAttach = true;
      script = document.createElement('script');
      script.charset = 'utf-8';
      script.timeout = 120;
      script.src = url;
    }
    needAttach && document.head.appendChild(script);
  };
  // 定义 self["webpackHotUpdate"]办法,用 currentUpdate 来收集须要热更新模块的模块 webpack/lib/web/JsonpChunkLoadingRuntimeModule.js line 303
  self["webpackHotUpdate"] = (chunkId, moreModules, runtime) => {for (var moduleId in moreModules) {if (__webpack_require__.o(moreModules, moduleId)) {currentUpdate[moduleId] = moreModules[moduleId];
        if (currentUpdatedModulesList) currentUpdatedModulesList.push(moduleId);
      }
    }
  };
  // 应用须要热更新的模块  webpack/lib/hmr/HotModuleReplacement.runtime.js line 298
  function internalApply(options) {var results = currentUpdateApplyHandlers.map(function (handler) {return handler(options);
    });
    results.forEach(function (result) {if (result.apply) {// 调用 applyHandler.apply()
        var modules = result.apply(reportError);
      }
    });
  }

3.3 -> compiler.watch 从新编译, 由 webpack.HotModuleReplacementPlugin 生成 dist/app.[hash].hot-update.json

  // c 为须要替换的 chunkIds
  {"c":["app"],"r":[],"m":[]}

3.4 -> compiler.watch 从新编译, 由 webpack.HotModuleReplacementPlugin 生成 dist/app.[hash].hot-update.js

  // 调用 self["webpackHotUpdate"](chunkId, moreUpdates)
  self["webpackHotUpdate"]("app", {
    "./src/print.js":
      ((module) => {function printMe() {console.log('Updating print.js.3');
        }
        module.exports = printMe
      })
  },
    function (__webpack_require__) {
      "use strict";
      (() => {
        // 为每次编译最新的 hash
        __webpack_require__.h = () => ("4bbb5237e2809cf139b2")
      })();}
  );

五、工作原理

HMR 工作流程剖析

webpack 监听到我的项目中文件或者模块的代码有批改后,由 webpack.HotModuleReplacementPlugin 插件生成 hot-update.json 和 hot-update.js 补丁文件,而后客户端通过 socket 连贯失去补丁文件,最初由
HMR runtime 将补丁文件的内容利用到模块零碎中。

HMR 工作原理图


上图展现了从咱们批改代码,到模块热更新实现的一个 HMR 残缺工作流程,图中已用箭头将流程标识进去。

  1. webpack-dev-server 提供 http server 服务和 socket server 服务,在 webpack-dev-middleware 中间件中启用 webpack 的 watch 模式。
  2. 当文件系统中某一个文件产生批改,webpack 监听到文件变动,依据配置文件对模块从新编译打包,并将打包后的代码保留在文件系统中。
  3. webpack-dev-server/client/index.js 在浏览器端和服务端之间建设一个 websocket 长连贯,将 webpack 编译打包的状态信息告知浏览器端。
  4. 浏览器端通过 webpack/hot/dev-server.js 接管到最新的编译信息,从而触发 module.hot.check 办法来开始热更新查看。
  5. 执行 webpack/lib/hmr/HotModuleReplacement.runtime.js 中的 hotCheck 办法,通过 hmrDownloadManifest 以 fetch 形式申请 hot-update.json 文件,而后执行 loadScript 以 jsonp 的形式加载 hot-update.js 文件。
  6. 执行 hot-update.js 文件中的 self[‘webpackHotUpdate’]办法,在 webpack/lib/web/JsonpChunkLoadingRuntimeModule.js 文件的 self[‘webpackHotUpdate’]办法定义中去收集到须要更新的模块依赖。
  7. 调用 internalApply 办法,用新模块替换掉旧模块,而后执行 module.hot.__acceptedDependencies 中的回调函数,从而实现模块替换的成果。

六、源码实现

代码地址

页面拜访地址:http://127.0.0.1:9001/hmr.html

已实现相应性能的模块列表:

  • @webpack-cli/serve/lib/index.js 启动服务
  • webpack-dev-server/lib/Server.js 设置 http 和 socket 服务监听
  • webpack-dev-server/client/index.js 提供 websocket 连贯
  • webpack/hot/dev-server.js 触发 module.hot.check
  • webpack-dev-middleware/index.js 启用 compiler.watch()和提供动态资源服务
  • static/hmr.js 收集模块依赖、下载 hot-update.json、hot-update、js 文件,执行模块热替换

七、总结

本文和大家次要分享 Webpack 的 HMR 源码剖析和工作原理,在工作原理剖析中,通过一张“webpack hmr 工作原理图”图让大家对 HMR 整个工作流程有所理解,HMR 自身源码内容较多,许多细节之处本文没有残缺写出,须要各位读者本人缓缓浏览和了解源码。

参考文章

1. 官网文档

退出移动版