事件总线与消息提示
本节介绍前端框架中的事件总线 EventManager 及其在应用中的扩展示例,以及如何结合 AbstractAppPage 统一实现 SnackBar 消息提示。
1. 事件总线:EventManager
EventManager 基于 Angular 的 EventEmitter 与 RxJS Subscription 实现,提供:
- 统一的事件枚举(
EventNameEnum及其子类) - 事件发布/订阅接口
- 订阅生命周期托管(通过订阅 ID 集合统一销毁)
@Injectable({ providedIn: 'root' })
export class EventManager {
private _eventMap = new Map<EventNameEnum, EventEmitter<any>>();
private _subscriptionMap = new Map<string, Subscription>();
constructor() {
// 预注册框架内置事件
EventNameEnum.values<EventNameEnum>().forEach((obj) => {
this._eventMap.set(obj, new EventEmitter<any>());
});
}
publish(eventName: EventNameEnum, body?: any): void {
const emitter = this.ensureEmitter(eventName);
emitter.emit(body);
}
subscribe(eventName: EventNameEnum, callback: (event: any) => void): string {
const emitter = this.ensureEmitter(eventName);
const subscription = emitter.subscribe(callback);
const subscriptionId = this.generateSubscriptionId();
this._subscriptionMap.set(subscriptionId, subscription);
subscription.add(() => this._subscriptionMap.delete(subscriptionId));
return subscriptionId;
}
unsubscribe(subscriptionId: string|Set<string>): void {
// 省略实现细节,支持单个或集合取消
}
}
2. 应用级事件枚举:AppEvent
在 cross-platform-app 中,通过扩展枚举的方式定义了应用层事件:
// src/app/core/constants/app.event.ts(示意)
export class AppEvent extends EventNameEnum {
static readonly SHOW_GLOBAL_LOADING = new AppEvent('SHOW_GLOBAL_LOADING');
static readonly HIDE_GLOBAL_LOADING = new AppEvent('HIDE_GLOBAL_LOADING');
// ... 其它业务事件
}
这样可以保证事件名在类型层面是封闭的,避免字符串常量到处乱飞。
3. 全局 Loading 示例
在服务层,通过继承 AbstractAppService 将 Loading 行为与 HTTP 调用绑定在一起(详情见「抽象服务」章节):
export abstract class AbstractAppService extends AbstractService {
private readonly eventManager = inject(EventManager);
override async request<T>(url: Url, body?: any, header?: RequestHeader): Promise<ResultVO<T>> {
this.eventManager.publish(AppEvent.SHOW_GLOBAL_LOADING, true);
return await super.request<T>(url, body, header, async () => {
this.eventManager.publish(AppEvent.SHOW_GLOBAL_LOADING, false);
});
}
}
在某个布局组件或根组件中订阅该事件,即可实现全局 Loading 遮罩:
export class AppLayoutComponent extends AbstractAppPage {
loading = false;
async ngOnInit() {
this.registerEvent(AppEvent.SHOW_GLOBAL_LOADING, (flag: boolean) => {
this.loading = flag;
});
}
}
4. 统一消息提示
AbstractAppPage 内部通过 Material 的 MatSnackBar 实现了 info/error/warn/success 等方法:
override info(message: string, options?: MessageOptions): Promise<void> {
this.openSnack(message, options, 'mat-elevation-z2');
return Promise.resolve();
}
override error(message: string, options?: MessageOptions): Promise<void> {
this.openSnack(message, options, 'mat-elevation-z4');
return Promise.resolve();
}
在页面或服务中使用时,只需要传入「消息键 + 国际化参数」即可:
await this.info('users.save.success', {
isKey: true,
params: { name: user.name },
});
MessageOptions支持指定显示位置(顶部/底部)、是否为国际化 key、参数等,详情可参考MessageOptions类型定义。
5. 与传统模式的对比
| 维度 | 传统写法 | 框架写法(EventManager + 抽象基类) |
|---|---|---|
| 事件组织方式 | 字符串常量分散在各处 | 统一的 EventNameEnum / AppEvent |
| 全局 Loading | 每个页面/Service 各自维护一个 isLoading 状态 | 所有请求通过事件总线控制,全局布局统一展示 |
| 消息提示 | 每个页面自行注入 MatSnackBar 并实现样式 | 抽象基类封装 info/error/warn/success 四套样式 |
| 订阅管理 | 需要手动在 ngOnDestroy 里 unsubscribe | AbstractAppPage 内部维护订阅 ID 集合,统一销毁 |
| 可测试性与可观测性 | 难以集中观察「系统中有哪些事件」 | 事件名集中管理,易于做日志与埋点 |
整体来看,事件总线与消息系统的组合,使得前端应用在 可维护性、可观测性与一致性 方面,比传统组件各自为政的写法更容易演进和治理。