【学习笔记】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.js
const 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
变成 Map
module.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.js
import 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