Webpack是怎么实现热更新的?涉及那些模块?

在文章的开头,我想先抛出一些问题:

  1. webpack的热更新功能,需要哪些模块的支持?
  2. 一次热更新周期中,各个模块都做了哪些事情?
  3. webpack中的watchpatch模块做了什么事情?
  4. webpack-dev-server 和 webpack 中的 watchpack 是如何一起工作的?
  5. webpack一般会将构建的产物写入文件系统,但webpack-dev-server是将构建后的内容存在内存里了,这个是怎么实现的?(memfs)
  6. webpack-dev-server是将构建后的内容存在内存里了,我如何能看到构建后的产物呢?(好像不能,官方的做法是提供了一个writeToDisk相当于内存和磁盘里面写了两遍)
  7. 为什么要推荐react-refresh来替换react-hot-reload呢?支持hooks?
  8. react-refresh和@pmmmwh/react-refresh-webpack-plugin分别做了什么事情来支持React的热更新?
  9. 为什么某些情况下,我修改了文件内容,页面上的某个表单内容并没有保留?
  10. 为什么某些情况下,我的页面甚至直接刷新了?

Webpack实现热更新依赖的几个核心模块

  1. watchpack : webpack内置的watching能力,基于EventEmitter。当文件内容发生变化时,通知webpack触发重新构建。
  2. webpack-dev-server: 一个基于Express的服务,托管静态资源,并且通过WebSocket与客户端建立连接。HotModuleReplacementPlugin是webpack-dev-server内置的插件
  3. HotModuleReplacementPlugin : 向应用的主Chunk注入一系列热更新用的Runtime代码,用于建立WebSoket连接,处理hash消息、加载热更新的资源、提供用于处理更新策略的 module.hot.accept 接口
  4. react-refresh:react官方提供给react-native的hmr,由于其核心实现与平台无关,所以也适用于 Web。
  5. @pmmmwh/react-refresh-webpack-plugin : 这个插件通过 Webpack 的构建流程,使 react-refresh 的功能能够与 Webpack 构建集成。它会在 Webpack 配置中添加必要的插件和加载器,以便启用 React 组件的热更新。

热更新主流程

  1. **webpack-dev-server **中的webpack-dev-middleware会开启webpack 内置的watch模式,监听文件变化
  2. 同时 webpack-dev-server 中的 HotModuleReplacementPlugin 插件(webpack插件)会向应用的主 Chunk 注入一系列 HMR Runtime
  3. 当文件发生变动 webpack 的watch模块监听到文件变化触发webpack的重新构建
  4. webpack-dev-server 监听到webpack的this.compiler.hooks.done事件
  5. webpack-dev-server 在构建完成的回调中通过 WebSocket 向客户端发送模块hash消息
  6. 客户端的HMR Runtime在收到hash消息之后,发出mainfest请求获取本轮热更新涉及的chunk
  7. 客户端HMR Runtime开始下载变化的文件
  8. HMR Runtime触发变更模块的 module.hot.accept 回调
  9. 在module.hot.accept 中声明了如何将模块安全地替换为最新代码,替换的逻辑比较复杂,幸运的是大部分webpack loader已经帮我们做了这些事情,比如:
    • style-loader 内置 Css 模块热更
    • vue-loader 内置 Vue 模块热更

我们结合源代码来看一下整个过程

  1. 我们使用webpack构建我们的react项目、并且使用webpack-dev-server来托管我们的静态资源
js 复制代码
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
// Create a webpack compiler that is configured with custom messages.
const compiler = createCompiler({
  appName,
  config,
  urls,
  useYarn,
  useTypeScript,
  webpack,
});
...
// webpack-dev-server的配置
const serverConfig = {
  ...createDevServerConfig(proxyConfig, urls.lanUrlForConfig),
  host: HOST,
  port,
};

// 创建webpack-dev-server实例的时候,需要把webpack的compiler的示例传进去,这样才能共享一些状态,做一些联动
const devServer = new WebpackDevServer(serverConfig, compiler);
  1. webpack-dev-server内置的HotModuleReplacementPlugin插件会向应用的主 Chunk 注入一系列 HMR Runtime(热更新所需要的一些代码)

这是 HotModuleReplacementPlugin 向chunk注入runtime代码的源代码:

js 复制代码
// node_modules/webpack/lib/HotModuleReplacementPlugin.js
compilation.hooks.additionalTreeRuntimeRequirements.tap(
  PLUGIN_NAME,
  // 在这里添加runtime代码
  (chunk, runtimeRequirements) => {
    // 这个方法用来请求 mainfest 文件
    runtimeRequirements.add(RuntimeGlobals.hmrDownloadManifest);
    // 这个方法用来修改下载修改的文件
    runtimeRequirements.add(RuntimeGlobals.hmrDownloadUpdateHandlers);
    runtimeRequirements.add(RuntimeGlobals.interceptModuleExecution);
    runtimeRequirements.add(RuntimeGlobals.moduleCache);
    compilation.addRuntimeModule(
      chunk,
      new HotModuleReplacementRuntimeModule()
    );
  }
);

注入代码之后的效果:

  1. 通过调试,可以看到webpack-dev-server → webpack-dev-middleware 在启动的时候调用了webpack构建实例的watch方法: compiler.watch

这里也解释了webpack-dev-server和webpack的watch是如何一起工作的

  1. compiler的watching会创建一个Watching的实例,而这个Watching,最终使用的是watchpack,我们重点来看这个watchpack

调用链路:compiler.watching(Watching实例) → compiler.watchFileSystem → NodeWatchFileSystem → Watchpack

js 复制代码
// node_modules/webpack/lib/Compiler.js
watch(watchOptions, handler) {
  if (this.running) {
    return handler(new ConcurrentCompilationError());
  }

  this.running = true;
  this.watchMode = true;
  // compiler上有个watching属性,是Watching的实例
  this.watching = new Watching(this, watchOptions, handler);
  return this.watching;
}


// node_modules/webpack/lib/Watching.js
// Watching中有个watcher方法是用了 compiler.watchFileSystem.watch
this.watcher = this.compiler.watchFileSystem.watch(
	// ... 
);

// lib/node/NodeEnvironmentPlugin.js
// watchFileSystem 又是 NodeWatchFileSystem的实例
compiler.watchFileSystem = new NodeWatchFileSystem(
  compiler.inputFileSystem
);

// lib/node/NodeWatchFileSystem.js
// NodeWatchFileSystem的watcher方法又是 WatchPack的实例,有点绕。
const Watchpack = require("watchpack");
// ...
class NodeWatchFileSystem {
  // ...
  this.watcher = new Watchpack(options);
}
  1. watchpack是一个基于发布订阅的模块,继承自 events 模块的 EventEmitter,在实例化的时候会注册changeaggregated事件。一旦监听到文件的变化,就会执行回调,触发webpack的重新编译(看下面代码)。

为什么是 aggregated 事件呢?由于修改操作可能在同一时间触发多次,所以将这些操作聚合一下

js 复制代码
this.watcher.once("aggregated", (changes, removals) => {
  // 暂停发出事件,避免在超时时清除已聚合的更改和删除
  this.watcher.pause();

  // 如果存在输入文件系统并支持清除操作,执行清除操作
  if (this.inputFileSystem && this.inputFileSystem.purge) {
    const fs = this.inputFileSystem;
    for (const item of changes) {
      fs.purge(item);
    }
    for (const item of removals) {
      fs.purge(item);
    }
  }

  // 获取文件和目录的时间信息
  const { fileTimeInfoEntries, contextTimeInfoEntries } = fetchTimeInfo();

  // 调用用户提供的回调函数
  callback(
    null,
    fileTimeInfoEntries,
    contextTimeInfoEntries,
    changes,
    removals
  );
});

// node_modules/webpack/lib/Watching.js 
// callback 回调函数最终执行到了这里,触发了webpack的重新编译
_go(fileTimeInfoEntries, contextTimeInfoEntries, changedFiles, removedFiles) {
  this.compiler.hooks.watchRun.callAsync(this.compiler, err => {
				if (err) return this._done(err);
				const onCompiled = () => {};
				// 执行编译
				this.compiler.compile(onCompiled);
	});
}
  1. 当webpack重新编译完成,webpack-dev-server监听到this.compiler.hooks.done事件后,会通过WebSocket向客户端发送消息
js 复制代码
// node_modules/webpack-dev-server/lib/Server.js
this.compiler.hooks.done.tap(
  "webpack-dev-server",
  /**
   * @param {Stats | MultiStats} stats
   */
  (stats) => {
    if (this.webSocketServer) {
      this.sendStats(this.webSocketServer.clients, this.getStats(stats));
    }

    /**
     * @private
     * @type {Stats | MultiStats}
     */
    this.stats = stats;
  }
);
  1. 客户端收到webpack-dev-server发送过来的ok事件后,会向服务端请求main.48ce033e1a20d1c2a793.hot-update.json 文件,确认需要重新加载的范围

为什么webpack重新构建完成之后,除了会向client发送hash事件之后,还会向client发送一个ok事件?

webpack-dev-server在重新构建完成后发送"ok"事件的目的是通知客户端构建过程已经完成且没有出现错误或警告。这个事件是作为一种确认机制,用于确保客户端知道它所依赖的资源已经准备好,可以继续工作。

  1. main.48ce033e1a20d1c2a793.hot-update.json返回修改范围,客服端的Runtime代码开始下载变化的文件
  1. 这时候,客服端已经成功拿到了最新构建的内容,那么如何将更新的内容应用到页面上呢?一般都是由loader来进行处理的(react没有提供这种能力,需要使用react-refresh)

    拿css文件为例,style-loader在处理文件的时候,会往编译产物中添加module.hot.accept的相关代码,当该文件发生变化的时候,会触发回调,在回调中更新css,以下是style-loader注入热更新相关的代码:

js 复制代码
// style-loader/src/utils.js
function getStyleHmrCode(esModule, loaderContext, request, lazy) {
  const modulePath = stringifyRequest(loaderContext, `!!${request}`);

  return `
if (module.hot) {
  if (!content.locals || module.hot.invalidate) {
    var isEqualLocals = ${isEqualLocals.toString()};
    var isNamedExport = ${esModule ? "!content.locals" : false};
    var oldLocals = isNamedExport ? namedExport : content.locals;

    module.hot.accept(
      ${modulePath},
      function () {
        ${
          esModule
            ? `if (!isEqualLocals(oldLocals, isNamedExport ? namedExport : content.locals, isNamedExport)) {
                module.hot.invalidate();

                return;
              }

              ...

              ${
                lazy
                  ? `if (update && refs > 0) { update(content); }`
  // 最终会走到 update 里面会重新加载最新的css文件,实现热更新
                  : `update(content);`
              }`
        }
      }
    )
  }

  module.hot.dispose(function() {
    ${
      lazy
        ? `if (update) {
            update();
          }`
        : `update();`
    }
  });
}
`;
}

至此,一次完整的热更新流程就结束了。

回答上述问题

  1. webpack的热更新功能,需要哪些模块的支持?

    watchpack监听文件变化触发重新编译、webpack-dev-server提供静态资源服务器和客户端通信、各种loader在拿到最新的资源后自行执行更新逻辑

  2. 一次热更新周期中,各个模块都做了哪些事情?

    参考上述的热更新主流程

  3. webpack中的watchpatch模块做了什么事情?

    监听文件变化,触发webpack重新编译

  4. webpack-dev-server 和 webpack 中的 watchpack 是如何一起工作的?

    webpack-dev-server在启动的时候,内置的webpack-dev-middleware会启用compiler的watch功能

  5. webpack一般会将构建的产物写入文件系统,但webpack-dev-server是将构建后的内容存在内存里了,这个是怎么实现的?

    webpack-dev-server

  6. webpack-dev-server是将构建后的内容存在内存里了,我如何能看到构建后的产物呢?(好像不能,官方的做法是提供了一个writeToDisk相当于内存和磁盘里面写了两遍)

  7. 为什么要推荐react-refresh来替换react-hot-reload呢?支持hooks?

  8. react-refresh和@pmmmwh/react-refresh-webpack-plugin分别做了什么事情来支持React的热更新?

  9. 为什么某些情况下,我修改了文件内容,页面上的某个表单内容并没有保留?

  10. 为什么某些情况下,我的页面甚至直接刷新了?

参考资料

一分钟用上热更新 React Fast Refresh(react-refresh)

How should we set up apps for HMR now that Fast Refresh replaces react-hot-loader? #16604

相关推荐
喵叔哟30 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django