Node.js中TypeScript如何清除模块缓存实现热重载动态导入
动态导入ES模块并清除缓存的解决方案
问题背景
在ES6模块普及前,常用CommonJS的缓存清除方法实现模块重载:
function requireUncached(module) { delete require.cache[require.resolve(module)]; return require(module); }
但在Node.js v18.16.0中,项目设置"type": "module"启用ES模块特性后,运行包含混用require和动态import的代码时,出现ERR_REQUIRE_ESM错误:
报错代码
import fs from 'fs' boot() async function boot() { const path = JSON.parse(JSON.stringify('./foo.ts')) save(path, "const x = 20"); let file = await load(path) console.log(String(file)) save(path, 'const x = 100'); file = await load(path); console.log(String(file)); } async function load(path: string) { delete require.cache[require.resolve(path)] // CommonJS API,ES模块环境下不支持 return (await import(path)) as unknown } function save(path: string, code: string) { fs.writeFileSync(path, code) }
错误信息
./node_modules/ts-node/dist-raw/node-internal-errors.js:46 const err = new Error(getErrRequireEsmMessage(filename, parentPath, packageJsonPath)) ^ Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: ./test.ts require() of ES modules is not supported. require() of ./test.ts is an ES module file as it is a .ts file whose nearest parent package.json contains "type": "module" which defines all .ts files in that package scope as ES modules. Instead change the requiring code to use import(), or remove "type": "module" from ./package.json. at createErrRequireEsm (./node_modules/ts-node/dist-raw/node-internal-errors.js:46:15)
核心原因
项目启用"type": "module"后,所有.ts文件被识别为ES模块,此时禁止使用任何CommonJS的require相关API(包括require.resolve和require.cache),这是报错的直接原因。此外,Node.js并未暴露ES模块的缓存对象,无法像CommonJS那样直接删除缓存。
解决方案
1. 通过URL查询参数绕过缓存
Node.js会将带有不同查询参数的模块URL视为独立模块,不会复用缓存。我们可以在动态导入时添加唯一标识(如文件修改时间、时间戳)作为查询参数,强制加载最新模块:
基于文件修改时间的实现(推荐)
async function load(path: string) { // 获取文件最后修改时间 const stat = await fs.promises.stat(path); const mtime = stat.mtimeMs.toString(); // 构造带缓存击穿参数的模块URL const moduleUrl = new URL(path, import.meta.url); moduleUrl.searchParams.set('cache-bust', mtime); return (await import(moduleUrl.href)) as unknown; }
基于时间戳的简化实现
async function load(path: string) { const moduleUrl = new URL(path, import.meta.url); // 用当前时间戳作为唯一参数,每次导入都触发重载 moduleUrl.searchParams.set('t', Date.now().toString()); return (await import(moduleUrl.href)) as unknown; }
2. 自定义缓存管理层
如果需要更精细的缓存控制(比如手动触发失效),可以自己实现缓存逻辑,结合文件修改时间判断是否需要重新加载:
// 自定义缓存:存储模块路径、加载结果、最后修改时间 const moduleCache = new Map<string, { data: unknown; mtime: number }>(); async function load(path: string) { const stat = await fs.promises.stat(path); const currentMtime = stat.mtimeMs; // 检查缓存是否有效 const cached = moduleCache.get(path); if (cached && cached.mtime === currentMtime) { return cached.data; } // 重新加载最新模块 const moduleUrl = new URL(path, import.meta.url); moduleUrl.searchParams.set('mtime', currentMtime.toString()); const moduleData = (await import(moduleUrl.href)) as unknown; // 更新缓存 moduleCache.set(path, { data: moduleData, mtime: currentMtime }); return moduleData; }
3. 临时修改文件扩展名(不推荐)
如果上述方法不适用,可临时将目标文件改为非ES模块扩展名(如.cjs),但这种方式会破坏项目的ES模块一致性,仅作为临时 workaround。
注意事项
- 动态导入返回的是Promise,必须用
await处理 import.meta.url是ES模块特有的变量,用于获取当前模块的URL,构造相对路径时必须依赖它- 频繁重载模块可能影响性能,建议仅在开发环境或特定需求场景下使用
内容的提问来源于stack exchange,提问作者Lance Pollard
相关产品推荐
相关产品推荐

