
本文介绍一种面向依赖注入与延迟初始化的设计模式,通过将异步数据获取逻辑从构造函数剥离、交由容器(如 application 实例)统一缓存与分发,解决“应用强依赖异步初始化数据”这一常见架构难题。
本文介绍一种面向依赖注入与延迟初始化的设计模式,通过将异步数据获取逻辑从构造函数剥离、交由容器(如 application 实例)统一缓存与分发,解决“应用强依赖异步初始化数据”这一常见架构难题。
在构建可扩展的插件化系统时,一个典型挑战是:核心插件(Extension)的元信息(如 configuration、data schema 等)必须从远程 API 异步加载,且整个应用功能高度依赖这些数据——但 JavaScript/TypeScript 不允许在 constructor 中 await,也无法保证插件实例化顺序与数据就绪时机一致。 若强行将异步逻辑塞入构造函数(如传入 Promise
✅ 推荐方案:依赖倒置 + 容器级缓存 + 延迟解析
核心思想是 反转控制权:不再让 Extension 自行持有或等待 ExtensionDefinition,而是将其设计为“无状态能力载体”,通过构造函数接收一个具备数据获取能力的 IApplication 实例,并由该容器统一负责定义的异步加载、内存缓存与生命周期管理。
1. 重构 Extension 类:移除定义依赖,注入应用上下文
export abstract class Extension<TConfig extends object, TData extends object>
implements IExtension<TConfig, TData> {
constructor(public application: IApplication) {
// 构造即完成,无需 await
}
// 返回 Promise<this> 支持异步初始化(如预热连接、校验 schema)
abstract initialize(): Promise<this> | this;
abstract APIFetchSessionByID(sessionId: string): Promise<ISession<TConfig, TData>>;
abstract APIFetchSessionByToken(token: string): Promise<ISession<TConfig, TData>>;
}✅ 优势:类职责清晰(只关注业务逻辑),构造函数纯同步、可测试;初始化时机可控(如在 APIApplication.initialize() 内统一 await Promise.all(extensions.map(e => e.initialize())))。
2. 在 Application 中实现智能缓存定义获取器
export class APIApplication implements IApplication {
private readonly _definitions = new Map<string, Promise<IExtensionProperties<any, any>>>();
constructor(private readonly config: IApplicationProperties) {}
// 首次调用触发 API 请求,后续命中内存缓存(自动去重)
async getCachedDefinition<TConfig extends object, TData extends object>(
extensionId: string,
): Promise<IExtensionProperties<TConfig, TData>> {
if (!this._definitions.has(extensionId)) {
const promise = this.fetchDefinitionFromAPI<TConfig, TData>(extensionId);
this._definitions.set(extensionId, promise);
}
return this._definitions.get(extensionId)! as Promise<IExtensionProperties<TConfig, TData>>;
}
private async fetchDefinitionFromAPI<TConfig extends object, TData extends object>(
extensionId: string,
): Promise<IExtensionProperties<TConfig, TData>> {
const res = await fetch(`${this.config.apiBase}/extensions/${extensionId}`);
if (!res.ok) throw new Error(`Failed to fetch extension ${extensionId}: ${res.status}`);
return res.json();
}
}✅ 优势:
- 自动去重:并发调用 getCachedDefinition('x') 多次,仅发起一次网络请求;
- 透明缓存:消费者(Extension)无需感知数据来源,调用即得;
- 类型安全:泛型保留,返回值精确匹配插件所需配置结构。
3. Extension 内部按需获取定义(示例)
class ExampleExtension extends Extension<{ timeout: number }, { version: string }> {
private _definition!: IExtensionProperties<{ timeout: number }, { version: string }>;
async initialize(): Promise<this> {
// 延迟到首次需要时再获取(或在 initialize 中预加载)
this._definition = await this.application.getCachedDefinition<{ timeout: number }, { version: string }>('example');
console.log('Loaded config:', this._definition.configuration.timeout);
return this;
}
async APIFetchSessionByID(sessionId: string): Promise<ISession<{ timeout: number }, { version: string }>> {
// 使用已解析的 definition 驱动具体逻辑
const timeout = this._definition.configuration.timeout || 5000;
const data = await fetch(`/api/session/${sessionId}`, { signal: AbortSignal.timeout(timeout) });
return { config: this._definition.configuration, data: await data.json() };
}
}⚠️ 关键注意事项
- 避免过早调用:确保 Extension 的任何使用(如 APIFetchSessionByID)均发生在 initialize() 完成之后,推荐在 APIApplication.initialize() 中统一协调;
- 缓存失效策略:若定义支持运行时更新,需提供 invalidateDefinition(id) 方法并清除对应 Map 键;
- 错误隔离:单个插件定义加载失败不应阻塞全局启动,建议捕获异常并提供降级机制(如默认配置、空会话);
-
类型擦除风险:Map
> 中泛型在运行时丢失,务必通过 as 断言或封装 getCachedDefinition 的返回类型保障安全。
✅ 总结
该方案以“构造即就绪、使用即加载、容器统管缓存”为原则,彻底规避了构造函数异步化陷阱,同时赋予系统更强的可观测性、可测试性与可维护性。它不仅适用于插件配置场景,亦可推广至路由元数据、权限策略、国际化资源等所有“启动前必需、来源异步、需全局共享”的单例数据管理需求。









