For AI agents: the complete documentation index is available at https://rspack.rs/zh/llms.txt, the full documentation bundle is available at https://rspack.rs/zh/llms-full.txt, and this page is available as Markdown at https://rspack.rs/zh/api/runtime-api/hmr.md.
close

模块热替换

Rspack 提供了与 webpack 相同的接口来实现模块热替换。

如果你通过 HotModuleReplacementPlugin 启用了模块热替换,它的接口将暴露在 module.hot 和 import.meta.webpackHot 对象下。

注意,在 ESM 中请使用 import.meta.webpackHot 来代替 module.hot。

示例

要接受已加载模块的更新,先检查 HMR 接口是否可用:

if (module.hot) {
  module.hot.accept('./library.js', function () {
    // 对更新过的 library 模块做些事情...
  });
}

// 或者
if (import.meta.webpackHot) {
  import.meta.webpackHot.accept('./library.js', function () {
    // 使用更新后的 library 模块…
  });
}

更新如何传播

模块变化时,Rspack 会检查能否在不刷新页面的情况下应用更新。模块可以通过 accept() 接受自身更新,也可以由引用它的模块通过 accept(dependency, callback) 接受该依赖的更新。否则,更新会继续向引用它的模块传播。

例如,三个模块的引用关系如下:

app.js
└── view.js
    └── button.js

当 button.js 发生变化时:

  • 如果 view.js 接受 button.js 的更新,Rspack 会替换 button.js 并调用 view.js 中的回调,不会替换 view.js 或 app.js。
  • 如果只有 app.js 接受 view.js 的更新,Rspack 会替换 button.js 和 view.js,再调用 app.js 中的回调。

如果一个模块被多处引用,每条受影响的引用路径都需要接受更新。默认情况下,如果某个模块拒绝更新,或更新传播到入口模块仍未被接受,本次更新就会中止。此时 dev server 可以回退为刷新整个页面。

模块 API

module.hot 和 import.meta.webpackHot 支持以下方法。

accept

接受(accept)给定依赖模块(dependencies)的更新,并触发回调函数(callback)来响应更新,除此之外,你可以附加一个可选的 error 处理程序:

module.hot.accept(
  dependencies, // 可以是一个字符串或字符串数组
  callback, // 用于在模块更新后触发的函数
  errorHandler, // (err, {moduleId, dependencyId}) => {}
);

// 或者
import.meta.webpackHot.accept(
  dependencies, // 可以是一个字符串或字符串数组
  callback, // 用于在模块更新后触发的函数
  errorHandler, // (err, {moduleId, dependencyId}) => {}
);

当使用 ESM import 时,所有从 dependencies 中导入的符号都会自动更新。注意:依赖项字符串必须与 import 中的 from 字符串完全匹配。在某些情况下,甚至可以省略 callback。在 callback 中使用的 require() 在这里没有任何意义。

在使用 CommonJS 时,你应该通过 callback 中的 require() 手动更新依赖模块。省略 callback 在这里没有任何意义。

为 accept 设置错误处理器

(err, {moduleId, dependencyId}) => {}

  • err:当使用 ESM 依赖项时,回调函数在第二个参数中或在依赖项执行期间抛出的错误。
  • moduleId:当前模块 id。
  • dependencyId:(第一个)被更改依赖项的模块 id。

accept(self)

接受自身更新。

module.hot.accept(
  errorHandler, // 处理新版本模块执行时的错误
);

// 或者
import.meta.webpackHot.accept(
  errorHandler, // 处理新版本模块执行时的错误
);

在此模块或依赖模块更新时,可以在不通知父依赖的情况下,对此模块处理和重新取值。如果此模块没有导出(或以其他方式更新的导出),这是有意义的。

当执行此模块(或依赖模块)抛出异常时,会触发 errorHandler。

为 accept 自身设置错误处理器

(err, {moduleId, module}) => {}

  • err:计算新版本时的错误。
  • moduleId:当前模块 id。
  • module:当前模块实例。
    • module.hot:允许访问出错模块实例的 HMR API。一个常见的场景是再次自我接收(accept)。添加一个 dispose 处理程序来传递数据也是有意义的。注意,错误的模块可能已经部分执行,所以请确保不要进入不一致的状态。你可以使用 module.hot.data 存储部分状态。
    • module.exports:可以被重载,但是要小心,因为属性名在生产模式下可能会被破坏。

decline

拒绝给定依赖模块(dependencies)的更新,使用 'decline' 方法强制更新失败。

module.hot.decline(
  dependencies, // 可以是一个字符串或字符串数组
);

// 或者
import.meta.webpackHot.decline(
  dependencies, // 字符串或字符串数组
);

将依赖模块标记为不可更新(not-update-able)。在处理「依赖的导出正在更新」或「尚未实现处理」时,这是有意义的。取决于你的 HMR 管理代码,此依赖模块(或其未接受的依赖模块)更新,通常会导致页面被完全重新加载。

decline(self)

拒绝自身更新。

module.hot.decline();

// 或者
import.meta.webpackHot.decline();

将依赖模块标记为不可更新(not-update-able)。当此模块具有无法避免的副作用(side-effect),或者尚未对此模块进行 HMR 处理时,这是有意义的。取决于你的 HMR 管理代码,此依赖模块(或其未接受的依赖模块)更新,通常会导致页面被完全重新加载。

dispose(或 addDisposeHandler)

注册一个处理函数,在模块被替换前清理定时器、事件监听等资源。将需要保留的状态写入 data 参数,更新后的模块就能通过 import.meta.webpackHot.data(CommonJS 中为 module.hot.data)读取它。

module.hot.dispose((data) => {
  // 清理并将 data 传递到更新后的模块...
});

// 或者
import.meta.webpackHot.dispose((data) => {
  // 清理并将 data 传递到更新后的模块...
});

下面的示例在更新时清除旧定时器,并从保存的数值继续计数:

let count = import.meta.webpackHot?.data?.count ?? 0;
const timer = setInterval(() => {
  console.log(++count);
}, 1000);

if (import.meta.webpackHot) {
  import.meta.webpackHot.accept();
  import.meta.webpackHot.dispose((data) => {
    clearInterval(timer);
    data.count = count;
  });
}

invalidate

当前模块无法自行处理热更新时,可以调用 invalidate(),让引用它的模块处理这次更新。即使当前模块已经调用 accept() 接受自身更新,更新也会继续向引用它的模块传播,详见更新如何传播。

更新被接受并开始应用后,Rspack 会执行当前模块的 dispose 回调,清理旧模块的资源。

如果希望拒绝当前模块的更新,请使用 decline()。

在 idle 状态下调用时,将创建一个包含此模块的新 HMR 更新。HMR 将进入 ready 状态。

在 ready 或 prepare 状态下调用时,此模块将添加到当前 HMR 的更新中。

在 check 状态期间被调用时,如果有可用更新,则此模块将添加到更新中。如果没有可用的更新,它将创建一个新更新。HMR 将进入 ready 状态。

在 dispose 或 apply 状态下调用时,HMR 将在退出这些状态后将其拾取。

用例

无法处理依赖更新时

在 accept() 回调中,如果发现无法处理依赖的更新,可以调用 invalidate(),将更新交给引用当前模块的模块处理:

import { x, y } from './dep.js';
import { processX, processY } from 'anotherDep';

const oldY = y;

processX(x);
export default processY(y);

import.meta.webpackHot.accept('./dep.js', () => {
  if (y !== oldY) {
    // 无法处理,冒泡给父级
    import.meta.webpackHot.invalidate();
    return;
  }
  // 可以处理
  processX(x);
});

无法处理自身更新时

下面的模块接受自身更新,并在更新时比较新旧导出的字符串。如果字符串发生变化,就调用 invalidate(),让引用它的模块也参与更新:

const message = 'Hello';

export default message;

const previousMessage = import.meta.webpackHot.data?.message;

if (previousMessage !== undefined && previousMessage !== message) {
  import.meta.webpackHot.invalidate();
} else {
  import.meta.webpackHot.dispose((data) => {
    data.message = message;
  });
  import.meta.webpackHot.accept();
}
Tip
  • 在 dispose 回调中,把需要保留的数据写入 data 对象。更新后的模块可以通过 import.meta.webpackHot.data(CommonJS 中为 module.hot.data)读取这些数据。
  • 首次执行时还没有旧状态,因此这个值是 undefined。热更新后,如果旧版本没有保存任何数据,它就是一个空对象。
  • 不要在模块每次执行时都调用 invalidate()。否则,一次热更新可能又触发下一次热更新,形成循环。

removeDisposeHandler

删除由 dispose 或 addDisposeHandler 添加的回调函数。

module.hot.removeDisposeHandler(callback);

// 或者
import.meta.webpackHot.removeDisposeHandler(callback);

API 管理

status

获取当前模块热替换进程的状态。

module.hot.status(); // 返回以下字符串之一...

// 或者
import.meta.webpackHot.status();
StatusDescription
idle该进程正在等待调用 check(见下文)
check该进程正在检查以更新
prepare    该进程正在准备更新(例如,下载已更新的模块)
ready此更新已准备并可用
dispose该进程正在调用将被替换模块的 dispose 处理函数
apply该进程正在调用 accept 处理函数,并重新执行自我接受(self-accepted)的模块
abort更新已中止,但系统仍处于之前的状态
fail更新已抛出异常,系统状态已被破坏

check

检查是否有可用的热更新,并下载更新内容。调用时,HMR 必须处于 idle(空闲)状态。

module.hot
  .check(autoApply)
  .then((outdatedModules) => {
    // 受本次更新影响的模块...
  })
  .catch((error) => {
    // 捕获错误
  });

// 或者
import.meta.webpackHot
  .check(autoApply)
  .then((outdatedModules) => {
    // 受本次更新影响的模块...
  })
  .catch((error) => {
    // 捕获错误
  });

autoApply 用于控制下载完成后是否立即应用更新:

  • 不传或设为 false:只下载更新,随后进入 ready 状态。准备好应用更新时,再调用 apply()。
  • 设为 true:下载完成后立即应用更新。
  • 传入选项对象:下载完成后立即应用更新,并将该对象作为 apply() 的选项。

在 .then() 回调中可以获取本次更新涉及的模块 ID 数组;如果没有更新,则收到 null。下载或应用更新出错时,可以在 .catch() 中处理错误。

apply

继续更新进程(当 module.hot.status() === 'ready' 时)。

module.hot
  .apply(options)
  .then((outdatedModules) => {
    // 受本次更新影响的模块...
  })
  .catch((error) => {
    // 捕获错误
  });

// 或者
import.meta.webpackHot
  .apply(options)
  .then((outdatedModules) => {
    // 受本次更新影响的模块...
  })
  .catch((error) => {
    // 捕获错误
  });

可选的 options 对象可以包含以下属性:

  • ignoreUnaccepted(boolean):忽略对不可接受的模块所做的更改。
  • ignoreDeclined(boolean):忽略对已拒绝的模块所做的更改。
  • ignoreErrored(boolean):忽略在接受处理程序、错误处理器以及重新评估模块时抛出的错误。
  • onDeclined(function(info)):拒绝模块的通知者。
  • onUnaccepted(function(info)):不可接受的模块的通知程序。
  • onAccepted(function(info)):可接受模块的通知者。
  • onDisposed(function(info)):废弃模块的通知者。
  • onErrored(function(info)):错误通知者。

info 参数将是一个包含以下某些值的对象:

{
  type: 'self-declined' | 'declined' |
        'unaccepted' | 'accepted' |
        'disposed' | 'accept-errored' |
        'accept-error-handler-errored' |
        'self-accept-errored' | 'self-accept-error-handler-errored',
  moduleId: 4, // 相关模块的 ID,具体含义见下方示例。
  dependencyId: 3, // 当 accept 回调函数报错时,表示被更新的依赖模块的 ID。
  chain: [1, 2, 3, 4], // 对于拒绝/接受/不接受:传播更新的 `chain`。
  parentId: 5, // 对于拒绝:下降的父模块 ID。
  outdatedModules: [1, 2, 3, 4], // 对于接受:已过时且将被处置的模块。
  outdatedDependencies: { // 对于接受:将处理更新的接受处理程序的位置。
    5: [4]
  },
  error: new Error(...), // 对于错误:抛出错误
  originalError: new Error(...) // 对于 accept-error-handler-errored 或 self-accept-error-handler-errored:
                                // 第一次抛出的错误,错误处理函数收到它后又抛出了新的错误。
}

例如,app.js 调用 module.hot.accept('./button.js', callback) 后,如果 callback 在 button.js 更新时抛出错误,moduleId 是 app.js 的 ID,dependencyId 是 button.js 的 ID。

如果错误处理函数随后又抛出一个新错误,type 为 'accept-error-handler-errored',error 是这个新错误,originalError 则是 callback 抛出的错误。

addStatusHandler

注册一个函数来监听 status 的变化。

module.hot.addStatusHandler((status) => {
  // 响应当前状态...
});

// 或者
import.meta.webpackHot.addStatusHandler((status) => {
  // 响应当前状态...
});

请记住,当 status 处理函数返回一个 Promise 时,直到 Promise 完成兑现,HMR 系统都将继续等待。

removeStatusHandler

移除一个注册的状态处理函数。

module.hot.removeStatusHandler(callback);

// 或者
import.meta.webpackHot.removeStatusHandler(callback);

本页改编自 webpack 文档,遵循 CC BY 4.0,且已作修改。