HarmonyOS CommonEvent 事件订阅治理:动态注册、来源校验与安全退订

实际项目里,CommonEvent 最常见的问题不是事件发不出去,而是页面反复进入后订阅重复、离开页面后回调仍然触发、或者自定义事件被错误来源冒用。本文把事件名、权限、注册时机、回调校验和退订动作放在一条链路里处理,让公共事件只在该到达的地方到达。

请添加图片描述

本文先把事件失控点讲透

这一节先把目标说清楚:我们不是简单演示一次 publish 和 subscribe,而是把公共事件放到长期运行的应用结构里考虑。实际项目中,公共事件通常跨页面、跨服务、跨模块传播,一旦缺少来源约束和退订规则,问题会在页面切换、后台恢复和模块重构时集中暴露。

  • 订阅对象由服务层托管,页面进出不会重复注册。
  • 发布端和订阅端都能通过权限或包名收窄范围。
  • 回调入口先校验 payload,再让仓储层刷新业务数据。
  • 用 traceId 串联发布、接收、刷新三个排查点。

这几件事连在一起看,读者就能从配置、API 调用、状态维护和排查方法四个角度复用本文方案,而不是只复制某一段示例代码。

CommonEvent 资料与声明入口

项目 内容
官方能力 公共事件支持发布、订阅、退订,包含系统公共事件与自定义公共事件。
本地声明 D:/harmonyos/SDK/23/ets/api/@ohos.commonEventManager.d.ts
订阅配置 D:/harmonyos/SDK/23/ets/api/commonEvent/commonEventSubscribeInfo.d.ts
发布配置 D:/harmonyos/SDK/23/ets/api/commonEvent/commonEventPublishData.d.ts

事件链路的版本与权限边界

项目 内容
SDK HarmonyOS SDK 23,本地 d.ts 已核对 createSubscriber、subscribe、unsubscribe、publish。
适用场景 应用内模块广播、轻量跨组件通知、系统事件监听。
不适用场景 大数据传输、强一致业务事务、需要持久队列的消息流。
权限边界 敏感自定义事件应配合 publisherPermission、publisherBundleName 或 subscriberPermissions。

请添加图片描述

请添加图片描述

先把事件契约写成常量,避免散落字符串

事件名如果写在页面里,后期模块增多后很难知道谁在发、谁在收。更稳的做法是把动作、数据字段、来源包名和错误码集中到一个小文件,所有发布端和订阅端都引用它。

export const RouteCacheEvent = {
  ACTION_READY: 'com.example.trail.route.CACHE_READY',
  ACTION_EXPIRED: 'com.example.trail.route.CACHE_EXPIRED',
  FIELD_ROUTE_ID: 'routeId',
  FIELD_TRACE_ID: 'traceId',
  TRUSTED_BUNDLE: 'com.example.trail'
} as const;

export interface RouteCachePayload {
  routeId: string;
  traceId: string;
  version: number;
}

这段代码的边界是事件协议层。订阅端只读取这里声明过的字段,发布端也只写这些字段,可以减少拼写错误和跨模块含义漂移。

订阅信息要同时约束动作和来源

只订阅 action 还不够,尤其是自定义事件。来源包名或发布权限可以把事件接收范围收窄,避免同名事件在复杂集成环境里误触发。

import commonEventManager from '@ohos.commonEventManager';
import { RouteCacheEvent } from '../contract/RouteCacheEvent';

export function buildRouteCacheSubscribeInfo(): commonEventManager.CommonEventSubscribeInfo {
  return {
    events: [RouteCacheEvent.ACTION_READY, RouteCacheEvent.ACTION_EXPIRED],
    publisherBundleName: RouteCacheEvent.TRUSTED_BUNDLE,
    publisherPermission: 'com.example.trail.permission.ROUTE_EVENT'
  };
}

这段代码负责订阅入口。它信任的是配置过的动作集合和来源约束,防止业务回调被无关事件唤醒。

发布端也要收窄接收者

在多个模块或多个应用同时安装时,发布端可以通过 subscriberPermissions 限制接收方。不要把路线详情、用户标识等敏感数据直接塞进公共事件里,事件里只放业务 ID 和追踪 ID。

import commonEventManager from '@ohos.commonEventManager';
import { RouteCacheEvent, RouteCachePayload } from '../contract/RouteCacheEvent';

export async function publishRouteCacheReady(payload: RouteCachePayload): Promise<void> {
  await commonEventManager.publish(RouteCacheEvent.ACTION_READY, {
    code: 0,
    data: JSON.stringify(payload),
    subscriberPermissions: ['com.example.trail.permission.ROUTE_EVENT']
  });
}

发布代码只负责通知,不负责传输完整路线。订阅端拿到 routeId 后再走仓储层读取详情,降低事件泄露风险。

订阅生命周期必须幂等

页面 onPageShow 每次都创建订阅者,离开时又没有退订,是线上重复回调的常见来源。这里用一个小服务托管 subscriber,保证 start 多次调用也只注册一次。

import commonEventManager from '@ohos.commonEventManager';
import { buildRouteCacheSubscribeInfo } from './RouteCacheSubscribeInfo';

export class RouteEventCenter {
  private subscriber?: commonEventManager.CommonEventSubscriber;
  private started = false;

  async start(): Promise<void> {
    if (this.started) {
      return;
    }
    this.subscriber = await commonEventManager.createSubscriber(buildRouteCacheSubscribeInfo());
    await commonEventManager.subscribe(this.subscriber, this.onEvent);
    this.started = true;
  }

  async stop(): Promise<void> {
    if (!this.started || !this.subscriber) {
      return;
    }
    await commonEventManager.unsubscribe(this.subscriber);
    this.subscriber = undefined;
    this.started = false;
  }

  private onEvent = (err: BusinessError, data: commonEventManager.CommonEventData) => {
    if (err) {
      return;
    }
    RouteEventCenter.consume(data);
  };

  private static consume(data: commonEventManager.CommonEventData): void {
    // 这里交给后面的数据校验函数处理。
  }
}

服务层拥有订阅对象,页面只调用 start 和 stop。这样生命周期变化不会把 subscriber 泄漏到页面状态里。

回调里先验 payload,再碰业务状态

CommonEventData 的 data 字段本质上是外部输入。解析失败、字段缺失、版本不匹配都应该在回调入口被挡住,而不是让页面刷新逻辑承担异常。

function parseRoutePayload(raw?: string): RouteCachePayload | undefined {
  if (!raw) {
    return undefined;
  }
  try {
    const value = JSON.parse(raw) as Partial<RouteCachePayload>;
    if (!value.routeId || !value.traceId || value.version !== 1) {
      return undefined;
    }
    return { routeId: value.routeId, traceId: value.traceId, version: 1 };
  } catch (_) {
    return undefined;
  }
}

function handleRouteCacheEvent(data: commonEventManager.CommonEventData): void {
  const payload = parseRoutePayload(data.data);
  if (!payload) {
    return;
  }
  RouteRepository.refreshById(payload.routeId, payload.traceId);
}

这一层只把不可信字符串转成可信对象。只有通过校验的数据才进入仓储层,后面的页面刷新就不需要重复处理 JSON 异常。

把有序、无序、粘性事件分清

路线缓存就绪适合普通自定义事件;如果需要多个接收者按顺序处理,再考虑有序事件;粘性事件权限更敏感,普通业务不要为了省一次查询就滥用。判断时可以先问两个问题:新订阅者是否必须拿到过去的状态,多个订阅者之间是否存在严格先后顺序。如果答案都是否,普通自定义事件通常更容易维护。

项目 内容
普通自定义事件 适合状态通知,事件过去后新订阅者不再收到。
有序事件 适合多个订阅者需要前后顺序处理的链路。
粘性事件 需要权限约束,适合系统级或明确授权的持久状态。

页面只订阅业务结果,不直接处理底层事件

页面层要关心的是“路线是否需要刷新”,不是 CommonEvent 的错误码和权限字段。把底层事件先收口到状态中心,页面只监听可渲染状态。

@Observed
export class RouteListState {
  refreshing: boolean = false;
  lastTraceId: string = '';
}

export class RouteListStore {
  readonly state = new RouteListState();

  markRefreshing(traceId: string): void {
    this.state.refreshing = true;
    this.state.lastTraceId = traceId;
  }

  finishRefreshing(): void {
    this.state.refreshing = false;
  }
}

这段代码把事件结果转换为页面状态。它不暴露 subscriber,也不让 UI 层解析事件字段。

用追踪 ID 把发布、接收、刷新串起来

排查重复订阅时,日志里只看 action 没有意义。每次发布都带 traceId,接收端和仓储刷新也打印同一个 traceId,才能判断是发多了、收多了,还是刷新层重复执行。

export class RouteEventLog {
  static publish(action: string, traceId: string): void {
    console.info(`[RouteEvent] publish action=${action} traceId=${traceId}`);
  }

  static receive(action: string, traceId: string): void {
    console.info(`[RouteEvent] receive action=${action} traceId=${traceId}`);
  }
}

日志只记录定位必需的信息,不记录用户路线详情。这样既能查问题,也不会把敏感数据写入日志。

CommonEvent 排查表:从重复回调倒查

现象 优先查看 处理方式
页面返回后仍收到事件 RouteEventCenter.stop 是否执行 把订阅绑定到明确生命周期,stop 内部做幂等保护。
事件被收到多次 start 是否重复调用并创建多个 subscriber 服务层保存 started 标记,页面不要持有订阅对象。
订阅收不到自定义事件 publisherPermission 与 subscriberPermissions 是否匹配 先去掉权限本地联调,再逐项加回约束。
解析 data 报错 发布端 JSON 字段和版本 在回调入口 parse,失败直接丢弃并记录 traceId。

事件发布前的核对清单

这份清单建议在提交代码、写入团队文档或交给测试同学前逐项过一遍。它不是形式化备注,而是把本文的配置边界、运行时行为、异常兜底和可观测信息压成可以执行的确认项。

  • 事件名全局唯一,并放在 contract 文件中。
  • 发布端只传业务 ID,不传完整敏感数据。
  • 订阅端配置了来源约束或权限约束。
  • start/stop 多次调用不会重复注册或重复退订。
  • 日志能用同一个 traceId 串起发布、接收和刷新。

如果其中任意一项还没有办法给出明确证据,优先回到对应实现小节补日志、补校验或补生命周期处理,再进入下一轮联调。

事件治理小结

CommonEvent 的工程重点不是会调用 publish,而是把事件当作一条有边界的输入链路处理。事件契约、来源约束、幂等订阅、payload 校验和日志追踪都补齐后,公共事件才适合放进长期维护的项目。

CommonEvent 参考资料

下面列出的资料用于核对 API 名称、能力范围和版本边界。实际落地时还需要结合项目使用的 SDK 版本、设备 API 级别以及团队已有封装做一次复核。

  • 公共事件能力说明:https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/common-event-overview
  • HarmonyOS SDK 23 本地 API 声明:@ohos.commonEventManager.d.ts
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐