热模块替换

热模块替换

热模块替换(HMR)指的是在修改 JavaScript 模块后重新加载它们,而无需重启整个进程的过程。HMR 通常会带来更快的反馈循环,因为在文件变更后,你无需等待整个进程重启。

HMR 这个术语在前端生态中已经使用多年,在 Vite 等工具中,它可以热重载模块并将变更应用到网页上,同时保持其现有状态。

然而,AdonisJS 执行的 HMR 要简单得多,而且与 Vite 或 Webpack 等工具有着很大的不同。我们进行 HMR 的目标只是提供更快的重载,仅此而已。

关键概念

不会向浏览器传播任何更新

由于 AdonisJS 是一个后端框架,我们并不负责维护前端应用的状态,也不负责向网页应用 CSS。因此,我们的 HMR 集成无法与你的前端应用对话并调和其状态。

事实上,并非每个 AdonisJS 应用都是浏览器渲染的 Web 应用。许多应用使用 AdonisJS 来创建纯 JSON API,它们同样可以从我们的 HMR 集成中获益。

仅适用于动态导入

大多数 HMR 工具会使用代码转换,将额外的代码注入到编译输出中。在 AdonisJS,我们并不热衷于转译器,始终努力如实地拥抱平台本身。因此,我们实现 HMR 的方式使用了 Node.js loader hooks,并且仅适用于动态导入。

好消息是,你的 AdonisJS 应用中所有关键部分默认都是动态导入的。例如,控制器、中间件和事件监听器都是动态导入的,因此,你今天就可以利用 HMR,而无需改动应用中的任何一行代码。

值得一提的是,被动态导入的模块的导入语句可以位于顶层。例如,一个控制器(在路由文件中被动态导入)可以为校验器、TSX 文件、模型和服务使用顶层导入,它们都能从 HMR 中获益。

用法

所有官方启动套件都已更新为默认使用 HMR。不过,如果你有一个既有的应用,可以按如下方式配置 HMR。

安装 hot-hook npm 包作为开发依赖。这个包由 AdonisJS 核心团队创建,也可以在 AdonisJS 应用之外使用。

npm i -D hot-hook

接下来,将以下配置复制粘贴到 package.json 文件中。boundaries 属性接受一个 glob 模式数组,这些模式所匹配的文件都应被纳入 HMR 的考量范围。

{
"hotHook": {
"boundaries": [
"./app/controllers/**/*.ts",
"./app/middleware/*.ts"
]
}
}

配置完成后,你可以使用 --hmr 标志启动开发服务器。

node ace serve --hmr

另外,你可能想要更新 package.json 文件中的 dev 脚本以使用这个新标志。

{
"scripts": {
"dev": "node ace serve --hmr"
}
}

完全重载 vs HMR

本节讲解 hot-hook 的底层工作机制。如果你没有心情阅读扩展的技术理论,可以随意跳过 🤓

或者,如果你想了解更深入的说明,可以阅读该包的 README 文件

让我们了解一下 AdonisJS 何时会执行一次完整的重载(重启进程),以及何时会热重载模块。

创建依赖树

当使用 --hmr 标志时,AdonisJS 会使用 hot-hookbin/server.ts 文件开始创建你应用的依赖树,并会监控属于该依赖树的所有文件。

这意味着,如果你在应用源代码中创建了一个 TypeScript 文件,但从未在任何地方导入它,这个文件将不会触发任何重载。它会被忽略,就好像该文件不存在一样。

识别边界

接下来,hot-hook 会使用配置中的 boundaries 数组来识别有资格进行 HMR 的文件。

作为经验法则,你绝不应该把配置文件、服务提供者或预加载文件注册为边界。这是因为这些文件通常会产生一些副作用,如果我们在不清除副作用的情况下重新加载它们,这些副作用会再次发生。以下是一些示例:

  • config/database.ts 文件负责与数据库建立连接。热重载该文件意味着关闭现有连接并重新创建它。同样的效果可以通过重启整个进程来实现,而无需增加额外的复杂度。

  • start/routes.ts 文件用于注册路由。热重载该文件意味着移除已向框架注册的现有路由并重新注册它们。同样,重启进程更简单。

换句话说,我们可以说在 HTTP 请求期间被导入/执行的模块应当是 HMR 边界的一部分,而启动应用所需的模块则不应是。

执行重载

一旦 hot-hook 识别出了边界,它就会对属于边界、且被动态导入的模块执行 HMR,并对其余文件重启进程。