HarmonyOS掌上记账APP开发实践第5篇:鸿蒙 EventBus 设计模式 — MoneyTrack 跨模块通信解密
鸿蒙 EventBus 设计模式 — MoneyTrack 跨模块通信解密

概述
在多模块鸿蒙应用中,模块间通信是一个核心挑战。随着 MoneyTrack 功能不断扩展——从首页账单管理、统计图表、资产管理到家庭成员记账——模块间的数据依赖关系越来越复杂。如果采用模块直接引用的方式,会导致循环依赖和高耦合,使得任何一个模块的修改都可能引发连锁反应。
发布订阅模式(Publish-Subscribe Pattern)提供了一种优雅的解耦方案:发布者无需知道订阅者的存在,订阅者也不需要依赖发布者,双方仅通过事件通道进行间接通信。MoneyTrack 项目同时使用了三种跨模块通信机制:基于 EventHub 的自定义 EventBus 实现全局事件传递、基于 emitter 的系统级事件用于组件间通信,以及 @Prop/@Link/@Param 装饰器用于父子组件间的数据绑定。本文深入分析这三种机制的设计原理、适用场景与最佳实践。
核心知识点
1. 三种跨模块通信方式对比
MoneyTrack 在不同场景下选择不同的通信机制,三者各有所长:
| 通信方式 | 作用域 | 耦合度 | 适用场景 | 是否支持跨页面 |
|---|---|---|---|---|
| EventBus | 全局(整个应用进程) | 松耦合(事件名耦合) | 全局状态变更通知、跨模块刷新 | 是 |
| emitter | 进程内(同一 Ability) | 松耦合(事件 ID 耦合) | 轻量级事件通知、Widget 刷新 | 是 |
| @Prop/@Link/@Param | 父子组件树 | 紧耦合(直接数据绑定) | 父子组件数据传递、UI 自动更新 | 否 |
- EventBus 适合需要携带业务语义的全局事件,如"账单数据已变更,请刷新所有相关页面"。
- emitter 适合轻量级的系统级通知,如"资产数据已变更,请刷新资产卡片"。
- @Prop/@Link/@Param 适合组件树中上下游之间的数据绑定,由框架自动管理变更检测和 UI 重绘。
2. 基于 EventHub 的 EventBus 实现
MoneyTrack 在 commonlib 模块中封装了 EventBus 单例类,底层基于 UIAbility 的 EventHub:
import { common } from '@kit.AbilityKit';
import { contextUtil } from '../ContextUtil';
export enum EventKey {
GLOBAL_REFRESH_EVENT = 'globalRefreshEvent',
}
export class EventBus {
private _eventHub: common.EventHub = (
contextUtil.getContext() as common.UIAbilityContext
).eventHub;
private static _instance: EventBus;
public static get instance(): EventBus {
if (!EventBus._instance) {
EventBus._instance = new EventBus();
}
return EventBus._instance;
}
public emit(eventKey: EventKey | string, ...args: Object[]) {
this._eventHub.emit(eventKey, ...args);
}
public on(eventKey: string, callback: Function) {
this._eventHub.on(eventKey, callback);
}
public off(eventKey: string, callback?: Function) {
this._eventHub.off(eventKey, callback);
}
}
事件键集中定义在 EventKey 枚举中,避免了魔法字符串散落在各处。目前定义了 GLOBAL_REFRESH_EVENT = 'globalRefreshEvent',随着功能扩展可以按模块命名空间追加新的事件键。
3. emitter 系统级事件
除了自定义 EventBus,MoneyTrack 还使用 @kit.BasicServicesKit 的 emitter API。emitter 是鸿蒙系统提供的进程内事件机制,与 EventBus 相比,它不依赖于 UIAbility 的 EventHub,生命周期更独立:
import { emitter } from '@kit.BasicServicesKit';
// 在 Constants.ets 中定义事件 ID
export const ASSET_CHANGE_EVENT_ID = 123;
// 在 HomeVM 中发送事件
emitter.emit({ eventId: 1 });
// 在 MainEntryVM 中监听 Widget 刷新事件
emitter.on(
{ eventId: this._widgetEventId },
() => {
WidgetUtil.updateWidgetsWhenChange();
},
);
emitter 的事件 ID 是数值类型,适合用常量集中管理。在 MoneyTrack 中,ASSET_CHANGE_EVENT_ID = 123 用于资产数据变更通知,eventId: 1 用于 Widget 服务卡片刷新。
4. EventBus 与 emitter 的详细对比
| 对比维度 | EventBus | emitter |
|---|---|---|
| 实现机制 | 基于 UIAbility 的 EventHub | 基于 @kit.BasicServicesKit 的独立事件通道 |
| 事件标识 | 字符串(支持枚举) | 数值(EventId)或字符串 |
| 生命周期 | 跟随 UIAbility 生命周期 | 独立生命周期,可在非 UI 线程使用 |
| 携带数据 | 支持 ...args 任意参数 |
支持结构化数据对象 |
| 适合场景 | 全局 UI 刷新、跨页面通信 | 后台任务通知、服务卡片更新、跨线程通信 |
| 性能特点 | 同步调用,轻量级 | 支持异步发送,适合高频事件 |
| 单例管理 | 自定义单例(EventBus.instance) |
系统级全局单例(直接 import 使用) |
5. 全局刷新链
EventBus 最核心的应用场景是"全局刷新链"。当用户创建、删除或修改账单时,需要通知所有正在展示数据的页面刷新。
注册监听(HomeVM)
// HomeVM.ets - 初始化时注册监听
public async initData() {
await this.dataProcessing.accountProcessing.init();
await this.refreshBill();
EventBus.instance.on(EventKey.GLOBAL_REFRESH_EVENT, () => {
this.refreshBill();
});
}
注册监听(StatisticsVM)
// StatisticsVM.ets - 统计页面同样监听
public async initData() {
await this.dataProcessing.accountProcessing.init();
await this.dataProcessing.getBillReport();
this.getSelectedDateData(dayjs().format('YYYY-MM-DD'));
EventBus.instance.on(EventKey.GLOBAL_REFRESH_EVENT, () => {
this.refreshCalendarData();
});
}
触发事件(删除账单)
// HomeVM.ets - 删除账单后触发全局刷新
public async deleteBill(id: number) {
RouterModule.push({
url: DialogMap.COMMON_CONFIRM,
param: {
message: '删除后账单无法恢复,是否确认删除?',
},
onPop: async (value) => {
const result = value.result as DialogInfo<ConfirmParam>;
if (result.param?.isConfirm) {
await AccountingDB.deleteTransactions(id);
ToastDialog.showToast({ message: '账单删除成功~' });
EventBus.instance.emit(EventKey.GLOBAL_REFRESH_EVENT); // 触发刷新
}
},
});
}
触发事件(设置默认账本)
// AccountBookPage.ets - 设置默认账本后触发
await AccountingDB.editAccount(updateData);
ToastDialog.showToast({ message: '已设为默认账本' });
EventBus.instance.emit(EventKey.GLOBAL_REFRESH_EVENT);
触发事件(删除账本)
// AccountBookPage.ets - 删除账本后触发
private async deleteAccount(accountId: number) {
await AccountingDB.deleteAccount(accountId);
EventBus.instance.emit(EventKey.GLOBAL_REFRESH_EVENT);
}
6. 完整事件流示例:“删除账单"到"首页刷新”
下面用 mermaid 时序图展示"删除账单"操作触发的完整事件链:
7. 清理监听器
为了避免内存泄漏,在页面销毁时需要取消事件监听。MainEntryVM 在清理方法中同时清理了 emitter 和 EventBus 的监听:
public cleanListener() {
try {
emitter.off(this._widgetEventId);
} catch (error) {
WindowUtil.dealAllError(error);
}
EventBus.instance.off(EventKey.GLOBAL_REFRESH_EVENT);
}
EventBus 事件命名规范建议
随着项目规模增长,事件命名规范直接影响代码的可维护性。以下是 MoneyTrack 实践中总结的命名建议:
- 使用枚举集中管理:所有事件键定义在
EventKey枚举中,避免字符串散落在代码中。 - 采用领域前缀:
GLOBAL_REFRESH_EVENT以GLOBAL前缀标识全局事件;资产相关可用ASSET_前缀,如ASSET_CHANGED。 - 动词过去式表示已发生:事件名表示"已发生的事情",如
GLOBAL_REFRESH_EVENT(已触发全局刷新)、BILL_CREATED(已创建账单)。 - 命名空间分层:按模块划分命名空间,例如
HOME_BILL_CREATED、STATISTICS_DATA_READY、MEMBER_STATUS_CHANGED。 - 避免过于通用:不要使用
update、refresh这类过于宽泛的词,最好带上业务领域信息。
最佳实践与注意事项
1. 警惕"事件风暴"
事件风暴(Event Storming)是指系统中事件数量失控,大量事件相互触发形成网状依赖,最终导致难以追踪和调试。过度使用 EventBus 会使数据流变得不透明,排错困难。建议遵循以下原则:
- 控制事件种类:全局事件不超过 5~8 种,超过此数量应审视是否有更好的通信方式。
- 避免事件链式触发:不要在事件回调中再次 emit 另一个事件,这会形成隐式的链式调用,使数据流难以追踪。
- 记录事件日志:在开发环境中记录所有 EventBus 的 emit 和 on 操作,便于调试。
2. EventBus 与 emitter 的选择策略
- 全局 UI 刷新场景:优先使用 EventBus,因为其基于 EventHub,与 UIAbility 生命周期一致,页面活跃时才接收事件。
- 后台服务通知场景:优先使用 emitter,不依赖 UIAbility,可在非 UI 线程中使用。
- 高频触发场景:优先使用 emitter,它支持异步发送,对主线程影响更小。
- 需要携带结构化数据:emitter 的
on回调可以接收结构化的数据对象,适合需要传递复杂参数的场景。
3. 生命周期管理
- 配对注册与注销:
on()和off()必须成对出现,在aboutToAppear/onPageShow中注册,在aboutToDisappear/onPageHide中注销。 - 避免重复注册:
initData()如果可能被多次调用,需要先off()再on(),防止回调被执行多次。 - 单例中的监听:在 ViewModel 单例中注册的监听,必须在应用退出或模块卸载时清理。
4. 避免过度使用 EventBus
EventBus 虽然解耦了模块间的直接依赖,但过度使用会带来新的问题:
- 隐式依赖:事件名成为隐式的接口契约,修改事件名需要追踪所有使用方。
- 数据流不透明:全局事件的传播路径难以静态分析,只能在运行时追踪。
- 调试困难:事件触发后难以确定哪些模块会响应,增加了调试难度。
推荐做法:优先使用 @Prop/@Link/@Param 进行父子组件通信,只在真正的跨模块场景使用 EventBus。
总结
EventBus 是 MoneyTrack 跨模块通信的核心基础设施,它基于 HarmonyOS 的 EventHub 封装,提供了全局事件发布订阅能力。与 emitter 系统事件、@Prop/@Link 组件绑定一起,构成了分层的通信体系。EventBus 负责全局业务状态同步,emitter 负责轻量级系统通知,@Prop/@Link 负责组件树内的数据传递。三者各司其职、互补共存。
在实际应用中,合理的做法是严格控制 EventBus 的事件种类和触发频率,结合生命周期管理防止内存泄漏,并优先使用更精确的通信机制(如 emitter 或 @Prop),只在必要场景使用全局 EventBus。只有这样,才能在享受解耦带来的灵活性的同时,避免陷入"事件风暴"的泥潭。
参考文档
- @kit.BasicServicesKit emitter API
- UIAbility EventHub 使用指南
- 发布订阅模式在鸿蒙中的实现
- HarmonyOS @ObservedV2 和 @Trace 装饰器文档
更多推荐



所有评论(0)