【学习笔记】nodejs 对模块进行热更新
nodejs 可以使用两种方式加载模块,分别是 CommonJS(cjs) 和 ECMAScript(esm) 模块。
本文将分别介绍如何通过清除缓存进行热更新。
CommonJS 模块热更新
CommonJS 使用 require 导入模块,使用 module.exports 导出模块。
默认情况:
模块只有在第一次导入时才会加载文件,再次导入只能获取到之前的缓存。
例如:
require("./log.js"); // 控制台打印信息require("./log.js"); // 没有打印信息console.log("导入模块");可以看到,第二次导入没有重新加载模块,而是直接读取缓存。
删除缓存:
CommonJS 使用 require.cache 保存缓存,所以只需要删除对应模块的缓存就能重新加载。
const path = require("path"); require("./log.js"); // 控制台打印信息delete require.cache[path.resolve("./log.js")]; require("./log.js"); // 再次打印信息注意:缓存使用绝对路径。
ECMAScript 模块热更新
ECMAScript 使用 import 导入模块,使用 export 导出模块。
要让 nodejs 启用 esm 模块加载器,需要在 package.json 加上 "type": "module"。
默认情况:
模块只有在第一次导入时才会加载文件,再次导入只能获取到之前的缓存。
例如:
import "./log.js"; // 控制台打印信息await import("./log.js"); // 没有打印信息console.log("导入模块");删除缓存:
esm 模块的缓存默认不暴露出来,需要在启动 node 时,使用 --expose-internals 标志。
注意:请使用最新版本的 node
参考信息:#49442
import path from "node:path"; import url from "node:url"; import module from "internal/process/esm_loader"; import "./log.js"; // 控制台打印信息const file = url.pathToFileURL(path.resolve("./log.js")).href; module.esmLoader.loadCache.delete(file); await import("./log.js"); // 再次打印信息注意:缓存使用 file:/// 路径
实际应用
在开发 nodejs 应用的时候,为了方便可能会使用 nodemon 监听文件改变,然后重启应用。
可是这样每次修改文件都是完全重启,效率实在太低。
所以使用模块热更新可以更精准的达到马上生效的效果。
CommonJS 热更新接口
服务器接口为 koa,通过 chokidar 监听文件改变,然后热更新接口。
const chokidar = require("chokidar");const path = require("path");const tree = new Map();const loadCache = require.cache;exports.watch = function watch(dir, callback) { const cwd = path.resolve(dir); const root = path.resolve(cwd, "index.js"); chokidar .watch(["**/*.js"], { cwd, ignoreInitial: true }) .on("all", (_type, filename) => { delCache(path.resolve(cwd, filename)); try { callback(); } catch {} buildTree(root); }); buildTree(root);};function buildTree(path) { tree.set(path, [path]); const module = loadCache[path]; if (module) { (function read(current) { for (const child of current.children) { if (!/node_modules/.test(child.filename)) { let parents = tree.get(child.filename); if (!parents) tree.set(child.filename, (parents = [])); parents.push(current.filename); if (child.children) read(child); } } })(module); }}function delCache(path) { const parents = tree.get(path); if (parents) { tree.delete(path); delete loadCache[path]; console.log("删除缓存", path); if (parents[0] != path) { for (const path of parents) { delCache(path); } } }}监听 api 文件夹,每次修改都会重新导入所有 api 接口:
const http = require("http");const { watch } = require("./utils.js");let api = require("./api/index.js");http .createServer((req, res) => { api(req, res); }) .listen(80);watch("./api", () => { api = require("./api/index.js");});在 api 文件夹下面包含多个路由文件:
├─index.js├─package.json├─utils.js├─api| ├─a.js| ├─b.js| ├─index.js| └─router.jsconst Koa = require("koa");const Router = require("koa-router");const app = new Koa();const desktop = new Router();const router = require("./router.js");// app.use(...middleware);for (const key in router) { desktop.use("/" + key, router[key].routes(), router[key].allowedMethods());}app.use(desktop.routes());app.use(desktop.allowedMethods());module.exports = app.callback();路由文件:
exports.a = require("./a.js");exports.b = require("./b.js");const Router = require("koa-router");const router = new Router();router.get("/", (ctx) => { ctx.body = "a.js";});module.exports = router;const Router = require("koa-router");const router = new Router();router.get("/", (ctx) => { ctx.body = "b.js";});module.exports = router;修改 api 文件夹下面的文件,就会让 index.js 的 api 变量更新。
从而达到了热更新的目的。
ECMAScript 热更新接口
esm 模块的缓存默认不暴露出来,需要在启动 node 时,使用 --expose-internals 标志。
注意:请使用最新版本的 node,需要在 package.json 加上 "type": "module"。
跟 CommonJS 的差异:require.cache 相当于 esmLoader.loadCache,类型从 Object 变成 Mapmodule.filename 相当于 module.url,id 从 绝对路径 变成 file:/// 路径module.children 相当于 module.linked,类型从 Array 变成 Promise<ArrayLike>
import chokidar from "chokidar";import path from "node:path";import url from "node:url";import module from "internal/process/esm_loader";const tree = new Map();const loadCache = module.esmLoader.loadCache;export function watch(dir, callback) { const cwd = path.resolve(dir); const root = url.pathToFileURL(path.resolve(cwd, "index.js")).href; chokidar .watch(["**/*.js"], { cwd, ignoreInitial: true }) .on("all", async (_type, filename) => { delCache(url.pathToFileURL(path.resolve(cwd, filename)).href); try { await callback(); } catch {} buildTree(root); }); buildTree(root);}function buildTree(path) { tree.set(path, [path]); const module = loadCache.get(path); if (module) { (async function read(current) { const children = await current.linked; for (let i = 0; i < children.length; i++) { if (!/node_modules/.test(children[i].url)) { let parents = tree.get(children[i].url); if (!parents) tree.set(children[i].url, (parents = [])); parents.push(current.url); read(children[i]); } } })(module); }}function delCache(path) { const parents = tree.get(path); if (parents) { tree.delete(path); loadCache.delete(path); console.log("删除缓存", path); if (parents[0] != path) { for (const path of parents) { delCache(path); } } }}监听 api 文件夹,每次修改都会重新导入所有 api 接口:
import http from "node:http";import { watch } from "./utils.js";import { app } from "./api/index.js";let api = app.callback();http .createServer((req, res) => { api(req, res); }) .listen(80);watch("./api", async () => { const { app } = await import("./api/index.js"); api = app.callback();});在 api 文件夹下面包含多个路由文件:
├─index.js├─package.json├─utils.js├─api| ├─a.js| ├─b.js| ├─index.js| └─router.jsimport Koa from "koa";import Router from "koa-router";import * as router from "./router.js";export const app = new Koa();const desktop = new Router();// app.use(...middleware);for (const key in router) { desktop.use("/" + key, router[key].routes(), router[key].allowedMethods());}app.use(desktop.routes());app.use(desktop.allowedMethods());路由文件:
export * from "./a.js";export * from "./b.js";import Router from "koa-router";export const a = new Router();a.get("/", (ctx) => { ctx.body = "a.js";});import Router from "koa-router";export const b = new Router();b.get("/", (ctx) => { ctx.body = "b.js";});修改 api 文件夹下面的文件,就会让 index.js 的 api 变量更新。
从而达到了热更新的目的。
参考:
Hot Module Replacement for Node.js
Invalidate cache when using import #49442


