CommonJS模块需延迟导出直到异步初始化完成,核心是用initPromise封装初始化、导出带ready()方法的对象或Proxy代理拦截调用,并提供isReady()/waitForReady()双API确保可靠性。

CommonJS 模块本身是同步加载和执行的,无法原生等待异步初始化完成后再导出模块。因此,**直接在 module.exports 中返回未 resolve 的 Promise 或依赖尚未就绪的异步结果,会导致其他模块拿到的是“空壳”或过早的中间状态**。要拦截未完成的异步初始化、防止误用,核心思路是:**不让模块导出“可用接口”,直到初始化真正完成;并主动暴露初始化状态与等待能力**。
用 Promise 封装初始化,并延迟导出可操作对象
不要在模块顶层立即赋值 module.exports,而是先定义一个待 resolve 的 Promise,把初始化逻辑包裹进去,在 resolve 后才将真实功能对象挂载上去。外部模块必须显式等待该 Promise 才能安全使用。
- 模块内部维护一个
initPromise,在首次调用初始化时启动(如数据库连接、配置加载) -
module.exports导出的是一个带ready()方法的对象,或直接导出该 Promise - 所有业务方法都包装为返回
initPromise.then(() => {...}),确保串行等待
示例:
// db.jslet initPromise;
let client;
function init() {
if (initPromise) return initPromise;
initPromise = new Promise(async (resolve, reject) => {
try {
client = await createConnection();
resolve();
} catch (err) {
reject(err);
}
});
return initPromise;
}
module.exports = {
init,
query: (...args) => init().then(() => client.query(...args)),
ready: () => init()
};
导出一个“代理对象”,运行时动态拦截调用
利用 JavaScript 的 Proxy 创建一个占位对象,在初始化完成前拦截所有方法调用,统一返回 pending Promise 或抛出提示错误,避免静默失败。
立即学习“Java免费学习笔记(深入)”;
- 初始化完成前,所有方法调用都被 Proxy 拦截,返回
initPromise.then(() => realObj[method](...args)) - 初始化失败时,拦截器可统一 throw 错误,便于上层捕获
- 相比手动包装每个方法,Proxy 更易维护,尤其模块方法较多时
示例片段:
// service.jslet realService;
const initPromise = init().then(res => { realService = res; });
const handler = {
get(target, prop) {
if (typeof realService?.[prop] === 'function') {
return function (...args) {
return initPromise.then(() => realService[prop](...args));
};
}
return realService?.[prop];
}
};
module.exports = new Proxy({}, handler);
提供同步检查 + 显式等待 API,兼顾调试与可靠性
开发阶段容易忽略异步依赖,导致“有时能跑通,有时报错”。建议同时提供:isReady()(同步返回布尔)、waitForReady()(返回 Promise),让调用方按需选择策略。
-
isReady()用于快速判断(如 CLI 命令中做健康检查) -
waitForReady()是推荐入口,强制等待,避免竞态 - 可在初始化失败后缓存错误,后续调用
waitForReady()直接 reject,不重复尝试
避免常见陷阱
CommonJS 的缓存机制会让 require() 总是返回首次执行后的 module.exports 对象——即使它当时还是个空对象或 pending Promise。因此:
- ❌ 不要写
module.exports = { foo: init().then(...) }—— 这导出的是一个含 pending Promise 的对象,但foo属性本身不会自动更新 - ❌ 不要在多个地方重复调用
init()而不做幂等控制 —— 可能触发多次异步操作 - ✅ 推荐统一通过
waitForReady()或ready()入口协调,让使用者明确感知初始化生命周期


















