跳至内容

热模块替换

热模块替换 (HMR) 是一种在运行中的应用程序中更新 JavaScript 模块的方法。这减少了开发过程中的反馈周期,因此您可以更快地查看和测试更改。在 Meteor 的实现中,应用程序可以在构建完成之前更新。

提示

hot-module-replacement 包在 Meteor 2.0 中引入。

要为应用程序启用 HMR,它应该使用 hot-module-replacement 包。目前不支持为包启用 HMR,但包可以依赖 hot-module-replacement 包以确保访问热 API。当 HMR 无法接受更改时,Meteor 使用热代码推送来更新应用程序,就像通常在不使用 HMR 时所做的那样。

HMR 目前支持现代 Web 架构。在其他架构和生产环境中始终禁用它。

应用程序如何更新

在重新构建受支持的架构时,Meteor 会检查哪些文件已修改,并将修改后的文件发送到客户端。然后客户端使用此过程

  1. 它检查修改后的模块是否接受或拒绝更新。如果模块既不接受也不拒绝,Meteor 会查看导入它的模块以查看它们是否接受或拒绝更新,然后查看导入这些模块的模块,依此类推。如果它遵循的所有路径都导致接受更新的模块,则使用 HMR。否则,它使用热代码推送。
  2. 许多 js 模块执行会对应用程序产生长期影响的操作。它们可能会创建 Tracker 自动运行,注册事件侦听器,或具有已呈现的 UI 组件。模块可以注册释放处理程序来清理模块的旧版本,以便它不再影响应用程序。在此更新过程的这一点上,这些释放处理程序将被调用。
  3. Meteor 运行模块的新版本、接受更新的模块以及它们之间所有模块。这确保了使用模块的新导出。因此,通常唯一接受更新的模块是没有导出或有其他方法更新父模块使用的导出的模块。此时,这些模块有两个版本正在运行,但如果已正确释放,则旧版本不再使用。

为了使 HMR 正确工作,必须有正确的模块接受或拒绝更新,并且必须编写释放处理程序。幸运的是,大多数应用程序不需要手动执行任何一项操作。相反,您可以使用自动检测可以接受更新的模块以及如何清理特定类型的模块的集成。例如,React 集成在 Meteor 应用程序中默认启用,并且能够自动更新 React 组件。

API

hot-module-replacement 包提供了一个 API 来控制 HMR 的使用方法。该 API 可在 module.hot 中使用。由于 API 并非始终可用(例如,在生产环境或 HMR 不支持的架构中),因此代码应该确保在使用它之前已定义 module.hot

js
if (module.hot) {
  module.hot.accept();
}

在未来的 Meteor 版本中,使用 if 语句将允许缩小器在缩小生产代码时删除此块。

使用 module.hot api 的包应使用 hot-module-reload 包以确保访问 API。

module.hot.accept

仅限客户端

摘要

接受此模块的更新。也适用于其依赖项,只要导入依赖项的其他模块也接受更新。

HMR 重新运行导入修改后的模块的文件,以便文件使用新的导出。除了配置哪些模块可以使用 HMR 更新外,module.hot.accept() 还会控制重新运行多少文件。Meteor 重新运行导入修改后的模块的文件、导入这些文件的文件,依此类推,直到到达接受更新的模块。因此,通常唯一接受更新的模块是没有导出或有其他方法更新其父模块使用的导出的模块。

module.hot.decline

仅限客户端

摘要

使用 HMR 禁用更新此模块或其依赖项。将改为使用热代码推送。无法通过稍后调用 module.hot.accept 来覆盖。

module.hot.dispose

仅限客户端

摘要

添加一个回调以在替换模块之前清理它

参数

源代码
名称类型描述必需
回调module.hot.DisposeFunction

在替换旧模块之前调用。

当此模块的实例不再使用时,将运行回调。主要用途是确保模块的实例不再影响应用程序。这是一个停止 Tracker 计算的示例

js
import { setLocale } from '/imports/utils/locale';

const computation = Tracker.autorun(() => {
  const user = Meteor.user();

  if (user && user.locale) {
    setLocale(user.locale);
  }
});

if (module.hot) {
  module.hot.dispose(() => {
    computation.stop();
  });
}

如果它没有停止计算,则每次为 HMR 重新运行模块时,都会有一个额外的计算。这可能导致意外行为,尤其是在我们修改了计算函数的情况下。

回调接收一个可以被修改以存储模块新实例信息的 data 对象。这可用于保留类实例、状态或其他数据。例如,此模块将保留 color 变量的值

js

let color = 'blue';

export function getColor() {
  return color;
}

export function changeColor(newColor) {
  color = newColor;
}

if (module.hot) {
  if (module.hot.data) {
    color = module.hot.data.color;
  }

  module.hot.dispose(data => {
    data.color = color;
  });
}

当模块首次运行时,module.hot.data 为 null,因此它将 color 设置为蓝色。最终,应用程序调用 changeColor 并将颜色设置为 purple。如果模块被重新运行,则模块的旧实例会将颜色存储在 data.color 中。新实例从 module.hot.data.color 中检索它,并为下次重新运行注册一个新的释放处理程序。

module.hot.data

仅限客户端

摘要

默认为 null。替换模块时,将其设置为传递给释放处理程序的对象。

module.hot.onRequire

仅限客户端

摘要

添加在模块被 require 之前和之后运行的回调

参数

源代码
名称类型描述必需
回调对象

可以有 before 和 after 方法,在 require 模块之前调用,以及在它完成评估之后调用

一些 HMR 集成使用它来检测可以使用 HMR 自动更新的文件,并处理清理旧模块实例和迁移状态。例如,React Fast Refresh 使用它来查找仅导出 React 组件的模块,并使这些模块接受更新。

js
if (module.hot) {
  module.hot.onRequire({
    // requiredModule is the same object available in the
    // required module as `module`, including access to `module.hot`
    // and `module.exports`
    //
    // parentId is a string with the path of the module that
    // imported requiredModule.
    before(requiredModule, parentId) {
      // Anything returned here is available to the
      // after callback as the data parameter.
      return {
        importedBy: parentId,
        previouslyEvaluated: !requiredModule.loaded
      }
    },
    after(requiredModule, data) {
      if (!data.previouslyEvaluated) {
        console.log(`Finished evaluating ${requiredModule.id}`);
        console.log(`It was imported by ${data.importedBy}`);
        console.log(`Its exports are ${requiredModule.exports}`);
      }

      // canAcceptUpdates would look at the exports, and maybe the imports
      // to check if this module can safely be updated with HMR.
      if (requiredModule.hot && canAcceptUpdates(requiredModule)) {
        requiredModule.hot.accept();
      }
    }
  });
}