Skip to main content

事件总线与消息提示

本节介绍前端框架中的事件总线 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 里 unsubscribeAbstractAppPage 内部维护订阅 ID 集合,统一销毁
可测试性与可观测性难以集中观察「系统中有哪些事件」事件名集中管理,易于做日志与埋点

整体来看,事件总线与消息系统的组合,使得前端应用在 可维护性、可观测性与一致性 方面,比传统组件各自为政的写法更容易演进和治理。