👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
   我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 HarmonyOS 的过程都记录在这里。
  
  🛠️ 主要方向:ArkTS 语言基础、HarmonyOS 原生应用(Stage 模型、UIAbility/ServiceAbility)、分布式能力与软总线、元服务/卡片、应用签名与上架、性能与内存优化、项目实战,以及 Android → 鸿蒙的迁移踩坑与复盘。
  🧭 内容节奏:从基础到实战——小示例拆解框架认知、专项优化手记、实战项目拆包、面试题思考与复盘,让每篇都有可落地的代码与方法论。
  💡 我相信:写作是把知识内化的过程,分享是让生态更繁荣的方式。
  
   如果你也想拥抱鸿蒙、热爱成长,欢迎关注我,一起交流进步!🚀

前言

支付失败、登录失败、文件上传失败,这类问题有一个共同点:只看最终报错,往往回答不了“用户到底走到了哪一步”。

普通日志当然重要,但如果代码里只有零散的 console.info()、hilog.info(),排查时很容易变成按时间翻日志:进入页面了吗?点按钮了吗?参数检查通过了吗?请求发出去了吗?失败发生在哪个阶段?

对于这种有明确业务阶段的流程,更适合把关键节点记录为结构化应用事件。

HarmonyOS 的 Performance Analysis Kit 提供 HiAppEvent 应用事件打点能力,可以通过 write() 写入自定义应用事件,并通过 Watcher 在本地订阅应用事件。官方“订阅应用事件(ArkTS)”示例就是“打点 + 本地订阅 + Log 窗口观察”的完整流程。

这次不讨论复杂的线上监控平台,而是用一个最小的“提交订单”流程,把下面这条链串起来:

进入提交
    ↓
开始处理
    ↓
业务操作
    ↓
成功 / 失败

真正需要解决的问题不是“多打几行日志”,而是让每一次业务流程都有可以关联、过滤和还原的事件记录。

一、普通日志和业务事件,解决的不是同一个问题

假设提交订单时留下这些日志:

submit start
check sku
request start
request failed

单次调试时它们可能够用,但业务稍微复杂一点就会遇到几个问题。

第一,日志通常偏向代码执行过程,而不是业务语义。看到 request failed,还需要继续判断它属于登录、支付、订单还是上传。

第二,日志之间缺少稳定的结构。某个开发者写 orderId,另一个写 id,还有人直接把整个对象打印出来,后续很难统一过滤。

第三,同时发生多次操作时,不容易判断哪些日志属于同一次业务流程。

HiAppEvent 的思路不一样。应用事件本身包含 domain、name、eventType 和 params 等结构化信息。官方示例使用自定义 domain 和事件名称,通过 hiAppEvent.write() 写入事件,再由 Watcher 按 domain 过滤并订阅。

因此可以把两者理解成:

手段更适合解决的问题
HiLog / console 日志某段代码执行到了哪里、变量是什么、异常栈是什么
HiAppEvent 应用事件某个业务动作发生了什么、属于哪个流程、结果是什么

两者并不冲突。实际开发中,更合理的做法是:日志继续用于代码级调试,HiAppEvent 用于建立业务关键节点。

二、先把当前版本和接口边界确认清楚

本文以当前 HarmonyOS 7 开发环境作为目标背景。

华为官方升级适配资料明确说明,HarmonyOS 7.0 对应 API 26.0.0,官方建议升级到对应开发套件并完成应用适配。

HiAppEvent 属于 Performance Analysis Kit(性能分析服务)。当前官方 API 目录中仍将 @ohos.hiviewdfx.hiAppEvent 列为在维护的 ArkTS API,同时旧的 @ohos.hiAppEvent 已被放入“已停止维护的接口”分类,因此新代码不应继续按旧模块组织。

本文使用:

import { hiAppEvent, hilog } from '@kit.PerformanceAnalysisKit';

这也是华为官方现有 HiAppEvent ArkTS 示例采用的 Kit 导入方式。

本文涉及的核心接口只有几个:

hiAppEvent.write()
hiAppEvent.addWatcher()
hiAppEvent.removeWatcher()
hiAppEvent.EventType.BEHAVIOR
AppEventInfo
Watcher
AppEventPackageHolder

当前场景只是应用自己的自定义事件写入与本地订阅,不涉及网络访问、定位、相机等能力。官方订阅应用事件示例也没有要求为这组操作额外增加 module.json5 权限声明。本文因此不增加无关权限配置。

三、先设计事件,再写代码

这里比较容易出现一个理解偏差:用了 HiAppEvent,不等于把原来的每一行日志都改成事件。

如果页面打开、组件刷新、循环开始、循环结束全部打点,得到的仍然只是一堆更昂贵的“结构化日志”。

业务事件应该围绕业务状态变化设计。

这次把提交订单压缩成三个事件:

order_submit_start
order_submit_action
order_submit_result

其中:

  • order_submit_start:用户正式发起一次提交;
  • order_submit_action:记录当前进入的关键操作阶段;
  • order_submit_result:记录整个流程结束以及成功或失败。

三个事件使用同一个:

flow_id

进行关联。

这样,即使用户连续提交两次订单,也可以根据 flow_id 把事件拆回两条独立业务链。

事件 domain 统一定义为:

order_flow

最终希望看到的是:

flow_id=xxx
order_submit_start
        ↓
order_submit_action stage=validate
        ↓
order_submit_action stage=create_order
        ↓
order_submit_result result=success

或者:

flow_id=yyy
order_submit_start
        ↓
order_submit_action stage=validate
        ↓
order_submit_action stage=create_order
        ↓
order_submit_result result=fail reason=network_error

这已经比“start / request / failed”更接近真正可排查的业务过程。

四、定义一个最小事件上报器

这段代码解决的是一件事:统一所有订单事件的 domain、事件类型和公共字段,避免业务代码到处手写 AppEventInfo。

import { BusinessError } from '@kit.BasicServicesKit';
import { hiAppEvent, hilog } from '@kit.PerformanceAnalysisKit';

const DOMAIN = 'order_flow';
const LOG_DOMAIN = 0x0000;
const LOG_TAG = 'OrderEvent';

export class OrderEventReporter {
  static async writeStart(flowId: string, source: string): Promise<void> {
    await this.writeEvent('order_submit_start', {
      flow_id: flowId,
      source: source
    });
  }

  static async writeAction(flowId: string, stage: string): Promise<void> {
    await this.writeEvent('order_submit_action', {
      flow_id: flowId,
      stage: stage
    });
  }

  static async writeResult(
    flowId: string,
    result: string,
    reason: string
  ): Promise<void> {
    await this.writeEvent('order_submit_result', {
      flow_id: flowId,
      result: result,
      reason: reason
    });
  }

  private static async writeEvent(
    name: string,
    params: Record<string, string>
  ): Promise<void> {
    const eventInfo: hiAppEvent.AppEventInfo = {
      domain: DOMAIN,
      name: name,
      eventType: hiAppEvent.EventType.BEHAVIOR,
      params: params
    };

    try {
      await hiAppEvent.write(eventInfo);
    } catch (err) {
      const error = err as BusinessError;
      hilog.error(
        LOG_DOMAIN,
        LOG_TAG,
        `write event failed, code=${error.code}, message=${error.message}`
      );
    }
  }
}

这里真正需要关注的是 AppEventInfo 的四个部分:

domain
name
eventType
params

官方应用事件示例同样通过这几个字段构造事件,并使用 EventType.BEHAVIOR 表示行为类事件。write(info) 返回 Promise<void>,因此可以直接使用 Promise 或 async/await 处理结果。

为什么不把成功和失败全部做成不同 domain?

因为 domain 更适合作为一个相对稳定的业务领域,而事件名和参数负责表达具体动作。这样 Watcher 可以直接订阅整个 order_flow,而不需要为每一种结果重复配置过滤规则。

五、把“开始→操作→结果”接到业务流程上

为了让示例可以独立理解,这里不虚构真实服务器,也不引入网络框架,只展示打点应该插在哪里。

import { OrderEventReporter } from './OrderEventReporter';

async function submitOrder(
  flowId: string,
  shouldSucceed: boolean
): Promise<void> {
  await OrderEventReporter.writeStart(flowId, 'confirm_page');

  await OrderEventReporter.writeAction(flowId, 'validate');

  // 此处放真实项目中的参数校验逻辑。

  await OrderEventReporter.writeAction(flowId, 'create_order');

  // 此处放真实项目中的创建订单逻辑。
  if (shouldSucceed) {
    await OrderEventReporter.writeResult(
      flowId,
      'success',
      ''
    );
  } else {
    await OrderEventReporter.writeResult(
      flowId,
      'fail',
      'create_order_failed'
    );
  }
}

这段代码故意没有模拟 HTTP 请求,也没有声称能够直接替换真实订单代码。

迁移到项目里时,只需要把事件放到已经存在的业务边界:

用户确认提交
        ↓
order_submit_start

本地校验开始
        ↓
order_submit_action

真正调用下单能力
        ↓
order_submit_action

拿到最终结果
        ↓
order_submit_result

如果业务中还有支付,则可以继续拆:

create_order
request_payment
confirm_payment

但不要把几十个函数调用都变成 stage。事件数量应该围绕“排查时我需要知道哪一步完成了”来设计。

六、使用 Watcher 在本地订阅自己的应用事件

写入事件只是第一步。

如果开发阶段仍然只能去翻事件文件,调试体验并不会很好。HiAppEvent 提供 Watcher,可以在本地订阅应用事件。

官方“订阅应用事件(ArkTS)”示例通过 addWatcher() 注册观察者,使用 appEventFilters 按 domain 过滤,并设置 triggerCondition: { row: 1 },然后在 onTrigger 中通过 AppEventPackageHolder.takeNext() 读取事件包。

可以把下面代码放到应用初始化阶段,例如 EntryAbility 的 onCreate() 中组织:

import { hiAppEvent, hilog } from '@kit.PerformanceAnalysisKit';

const LOG_DOMAIN = 0x0000;
const LOG_TAG = 'OrderWatcher';

const orderWatcher: hiAppEvent.Watcher = {
  name: 'order_flow_watcher',

  appEventFilters: [
    {
      domain: 'order_flow',
      names: [
        'order_submit_start',
        'order_submit_action',
        'order_submit_result'
      ]
    }
  ],

  triggerCondition: {
    row: 1
  },

  onTrigger: (
    curRow: number,
    curSize: number,
    holder: hiAppEvent.AppEventPackageHolder
  ) => {
    if (holder == null) {
      hilog.error(LOG_DOMAIN, LOG_TAG, 'holder is null');
      return;
    }

    hilog.info(
      LOG_DOMAIN,
      LOG_TAG,
      `event triggered, row=${curRow}, size=${curSize}`
    );

    let eventPackage: hiAppEvent.AppEventPackage | null = null;

    while ((eventPackage = holder.takeNext()) != null) {
      for (const eventInfo of eventPackage.data) {
        hilog.info(
          LOG_DOMAIN,
          LOG_TAG,
          `event=%{public}s`,
          eventInfo
        );
      }
    }
  }
};

hiAppEvent.addWatcher(orderWatcher);

如果观察者不再需要,可以调用:

hiAppEvent.removeWatcher(orderWatcher);

官方文档明确提供了 addWatcher() 与 removeWatcher() 作为事件订阅和取消订阅接口。

七、这里不要把 onTrigger 和 onReceive 混在一起

这是 HiAppEvent 使用时很值得单独说明的一点。

官方“订阅应用事件”示例采用:

onTrigger

并通过:

AppEventPackageHolder

逐批获取应用事件。

而应用终止、主线程超时等系统事件的官方示例中,可以看到另一种回调:

onReceive

回调参数是:

Array<hiAppEvent.AppEventGroup>

例如,官方应用终止事件示例就是通过 onReceive 接收 APP_KILLED 系统事件。

所以本文的“提交订单”属于应用自己定义并由 write() 写入的应用事件,最小实践继续使用官方应用事件示例中的:

onTrigger
    ↓
AppEventPackageHolder
    ↓
takeNext()

不要因为看到系统故障事件代码,就直接把 onReceive 搬到自定义业务事件示例里。

八、怎么在本地观察一条完整业务链

加入 Watcher 后,可以在 DevEco Studio 的 Log 窗口按 tag 查看事件。

官方应用事件示例中,事件被取出后会以字符串形式出现在 eventPkg.data 中,其中能够看到 domain、name、事件类型以及自定义参数。

在本文设计中,重点不是观察某一条事件,而是按 flow_id 连起来看。

例如:

flow_id=order_10001
name=order_submit_start
source=confirm_page

flow_id=order_10001
name=order_submit_action
stage=validate

flow_id=order_10001
name=order_submit_action
stage=create_order

flow_id=order_10001
name=order_submit_result
result=fail
reason=create_order_failed

看到这组数据时,即使页面只告诉用户“提交失败”,开发者已经可以确认:

用户确实发起了提交
→ 本地校验阶段已经进入
→ 创建订单阶段已经进入
→ 最终失败发生在 create_order

这就是业务事件链真正比零散日志更有价值的地方。

需要强调的是,上面的输出是根据本文事件设计整理出的预期观察形式,不是声称已经在具体 HarmonyOS 设备上运行得到的真实测试结果。实际项目仍应在目标 API 版本和设备环境中编译、运行并检查事件输出。

九、事件字段不是越多越好

HiAppEvent 对事件字段本身存在明确约束。

官方应用事件打点错误码文档规定:

  • domain 最多 32 个字符,并有字符组成规则;
  • event name 最多 48 个字符;
  • 单个事件参数数量超过 32 个时,多余参数会被丢弃;
  • 参数名最多 32 个字符;
  • write() 写入的字符串参数值超过 8 × 1024 个字符会触发相应参数错误处理;
  • 数组参数超过 100 个元素时,多余部分会被丢弃。

因此,不适合这么做:

params: {
  entire_order_json: JSON.stringify(hugeOrderObject)
}

一方面字段失去结构,另一方面也很容易碰到长度限制。

更推荐只放对定位有直接意义的字段:

flow_id
stage
result
reason
source

事件设计追求的不是“把上下文全部保存下来”,而是回答几个固定问题:

哪一次流程?
到了哪一步?
结果是什么?
为什么失败?
从哪里进入?

十、哪些数据不能随便写进业务事件

业务事件虽然用于排障,也不能变成另一个“什么都往里塞”的日志容器。

华为官方 HarmonyOS 隐私保护指导强调数据最小化原则:只收集和处理与目的相关且必要的数据,并建议通过匿名化或化名方式降低风险;本地无法处理而需要上传的数据同样应遵循最小化原则。

因此,像下面这些信息不应该为了“方便排障”直接塞进事件:

登录密码
短信验证码
完整身份信息
支付凭证
访问 Token
Session
完整银行卡信息
不必要的手机号、地址等个人信息

以订单流程为例,真正用于排查的是:

{
  flow_id: '...',
  stage: 'create_order',
  result: 'fail',
  reason: 'network_error'
}

而不是:

{
  phone: '...',
  token: '...',
  address: '...',
  card_number: '...'
}

如果某个标识确实是关联链路所必需的,也应该优先设计内部流程标识或经过适当处理的标识,而不是直接复制业务对象中的个人信息。

十一、几个很容易忽略的约束

1. domain 和 name 不是随便写字符串

HiAppEvent 会校验事件 domain、name 以及参数名。

例如 Watcher 名称同样存在规则:只允许数字、字母和下划线,需要以字母开头、不能以下划线结尾,最大长度为 32。非法名称会对应 11102001 错误。

所以:

name: 'order_flow_watcher'

比在这里使用包含空格、中文标点或动态拼接字符串更稳妥。

2. write() 成功不能等同于“业务成功”

这两个成功完全不是一回事。

await hiAppEvent.write(...)

成功,只表示这次应用事件写入操作完成。

订单创建是否成功,应该由业务代码自己决定,然后再写:

result=success

或者:

result=fail

不要把“事件写成功”与“业务执行成功”混为一谈。

3. 事件打点本身也要处理异常

官方示例明确对 write() 的 Promise 使用 catch 捕获 BusinessError。

所以事件封装层里至少应该保留:

try {
  await hiAppEvent.write(eventInfo);
} catch (err) {
  // 使用 HiLog 记录打点失败
}

这里继续保留 HiLog 很合理。

HiAppEvent 并不是为了“消灭日志”,而是让业务状态拥有结构化事件,而 HiLog 仍负责报告打点过程本身是否异常。

4. 不要让排障事件反过来绑死业务代码

如果每个页面都直接写:

hiAppEvent.write(...)

时间久了以后,事件名称、domain、字段名很容易不一致。

更容易维护的方式是像本文一样包一层:

OrderEventReporter

页面和业务服务只表达:

writeStart()
writeAction()
writeResult()

以后如果需要调整字段,修改的也是统一封装,而不是全项目搜索字符串。

十二、实际项目中建议按这个顺序排查

当业务事件没有按预期出现时,可以按下面顺序检查。

先检查版本。

HarmonyOS 7 对应 API 26.0.0;如果工程正在做版本升级,先确认实际使用的 SDK/API 配置与目标设备环境,不要只根据旧文章里的 API 编号判断。

再检查导入方式。

本文使用当前 Kit 化写法:

import { hiAppEvent } from '@kit.PerformanceAnalysisKit';

不要把已经停止维护的旧 @ohos.hiAppEvent 模块与当前 API 混在同一套示例中。

接着检查事件名称。

重点检查:

domain
name
param name
watcher name

是否满足官方命名约束。

然后检查 write() 自身是否失败。

不要只写:

hiAppEvent.write(eventInfo);

而不处理 Promise 异常。

再看 Watcher 的过滤条件。

例如写入:

domain: 'order_flow'

Watcher 却订阅:

domain: 'orders'

自然不会看到同一组事件。

最后才看业务链是否缺节点。

如果只有:

start
result

中间没有任何 stage,那么即使 HiAppEvent 工作完全正常,也无法回答“失败发生在哪个阶段”。

这属于事件设计问题,不是 API 问题。

十三、从一次打点变成真正可用的事件链

单独记录:

order_failed

价值其实有限。

比较完整的设计至少应该包含三个维度。

维度一:同一次流程能关联

使用:

flow_id

把开始、阶段和结果关联起来。

维度二:阶段要有业务语义

不要写:

step1
step2
step3

改成:

validate
create_order
request_payment
confirm_payment

这样事件本身就能说明业务状态。

维度三:结果必须闭环

如果已经记录:

order_submit_start

就应该尽量保证业务正常结束路径最终会出现:

order_submit_result

否则看到只有开始没有结果的链路时,很难判断是应用流程被中断、异常分支漏打点,还是事件设计本来就不完整。

最终,一条可用的业务事件链应该能回答:

用户从哪里进入?
      ↓
发起了哪一次操作?
      ↓
业务推进到了哪个阶段?
      ↓
最终成功还是失败?
      ↓
失败属于什么业务原因?

这比单纯增加 console.log() 数量更接近可维护的故障定位方案。

开发经验总结

HiAppEvent 的 API 本身并不复杂,真正需要设计的是“什么才值得成为一个业务事件”。

这次的提交订单示例可以整理成四点:

  1. 日志和业务事件不要互相替代。 HiLog 适合代码级信息,HiAppEvent 更适合结构化业务节点。

  2. 应用自定义事件使用 write() 记录。 通过 domain、name、eventType 和 params 建立统一格式;HarmonyOS 官方应用事件示例使用 EventType.BEHAVIOR 记录用户行为类事件。

  3. 本地调试可以配合 Watcher。 对自定义应用事件,可以按照官方示例使用 onTrigger、AppEventPackageHolder 和 takeNext() 获取订阅数据。

  4. 链路比单个事件更重要。 通过 flow_id + stage + result + reason,把“进入→操作→结果”连接起来,排查时才能知道用户究竟走到了哪里。

如果正在整理登录、支付、上传或下单流程,可以先检查一个很简单的问题:

当用户只告诉你“失败了”时,现有日志能不能在几分钟内还原出这次操作经历了哪些阶段?

如果答案是否定的,那么比继续增加零散日志更值得做的,可能就是先把关键业务节点变成一条稳定的事件链。

📝 写在最后

如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!

我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!

感谢你的阅读,我们下篇文章再见~👋

✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐