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

前言

通知能够正常显示,并不代表通知链路已经做完。

订单状态变化、聊天新消息、审批结果这类场景里,用户点击通知时真正期待的是直接看到对应业务内容,而不是先打开应用首页,再自己寻找订单或会话。如果通知展示的是“订单 #20260923 已发货”,点击后却只进入首页,通知和业务上下文实际上已经断开了。

HarmonyOS 的 Notification Kit 支持把 Ability Kit 提供的 WantAgent 封装到通知中。用户点击通知后,系统可以触发这个 WantAgent,再由其中的 Want 拉起目标 UIAbility。Want 本身又可以通过 parameters 携带业务参数,因此“通知 → UIAbility → 指定业务页”可以形成完整链路。华为官方当前的通知文档明确提供了“为通知添加行为意图”的开发方式。

这篇文章用一个最小的“订单状态通知”场景,把这条链路拆开实现。

一、为什么“通知发出来了”还只是第一步

假设应用收到订单状态变化:

订单号:ORDER_20260923_001
状态:已发货

我们希望用户最终获得这样的体验:

发布通知
   ↓
用户点击通知
   ↓
WantAgent 被触发
   ↓
拉起 EntryAbility
   ↓
从 Want.parameters 取出 orderId
   ↓
Navigation 跳转 OrderDetail
   ↓
显示 ORDER_20260923_001

如果通知里只配置标题、正文,没有关联 WantAgent,那么“通知展示”和“业务导航”就是两件互不关联的事。

真正需要设计的是两层路由:

系统层路由:
Notification → WantAgent → UIAbility

应用层路由:
UIAbility → Navigation → OrderDetail

很多“点通知只进首页”的问题,恰恰不是通知没有发成功,而是第二层没有把第一层传进来的业务参数继续消费。

二、先把官方能力边界弄清楚

本文使用的核心能力主要来自两个 Kit:

能力Kit本文用途
notificationManagerNotification Kit发布本地通知
NotificationRequest.wantAgentNotification Kit指定通知点击后的行为
wantAgent.getWantAgent()Ability Kit创建 WantAgent
Want.parametersAbility Kit携带 orderId 等业务参数
UIAbility.onCreate() / onNewWant()Ability Kit接收启动 Want
NavPathStackArkUI在应用内部跳到详情页

当前华为官方 API 示例已经采用:

import { notificationManager } from '@kit.NotificationKit';
import { Want, wantAgent } from '@kit.AbilityKit';

并通过 notificationManager.NotificationRequest 的 wantAgent 字段关联点击行为。官方示例中的 WantAgentInfo.actionType 使用 wantAgent.OperationType.START_ABILITY,表示触发后启动带 UI 的 Ability。

wantAgent.getWantAgent(info) 返回 Promise<WantAgent>;官方参考同时明确,第三方应用只能指定自身的 Ability。

这里还需要特别说明版本问题。

本文不把“HarmonyOS 7”简单等同为某一个固定 API Level,然后据此推断所有接口是否可用。华为当前文档已经按具体接口以及 Phone、Tablet、PC/2in1、TV、Wearable 等设备类型分别标注支持版本,开发时应该以目标 SDK 中接口页面的标注为准。例如当前 UIAbilityContext 文档本身从 API version 9 开始提供,并会对后续新增接口单独标记起始版本。

因此迁移这套代码时,建议在 DevEco Studio 中以工程实际 compileSdkVersion、目标设备和当前 SDK API Reference 再做一次确认,而不是只看系统产品版本名称。

三、搭一个最小订单通知场景

本文只做一件事:

发布一条“订单已发货”的本地通知,用户点击以后把 orderId 传入应用,并打开 OrderDetail 页面。

为了让例子聚焦,假设项目只有:

EntryAbility
Index
OrderDetail

其中:

  • EntryAbility 负责接收通知传来的 Want;
  • Index 持有应用主 NavPathStack;
  • OrderDetail 根据订单 ID 加载实际业务数据。

通知里不传整个订单对象,只传:

orderId: 'ORDER_20260923_001'

这样做还有一个重要的业务含义:通知只是一个入口,不应该被当成业务数据的最终真相。

四、第一步:创建带业务 ID 的 Want

Want 是这条链路里非常关键的一层。

我们希望系统点击通知后启动当前应用的 EntryAbility,同时携带订单 ID:

import { Want } from '@kit.AbilityKit';

const orderId: string = 'ORDER_20260923_001';

const want: Want = {
  bundleName: 'com.example.notificationdemo',
  abilityName: 'EntryAbility',
  parameters: {
    orderId: orderId,
    targetPage: 'OrderDetail'
  }
};

真正需要关注的是 parameters。

华为官方 API 示例中,Want 可以直接通过 parameters 携带业务字段;UIAbility 一侧也可以从:

want.parameters

读取对应内容。

这里额外放一个 targetPage,是为了让通知路由具有一点扩展空间。以后聊天通知可以传:

targetPage: 'ChatDetail'
conversationId: '...'

订单通知则传:

targetPage: 'OrderDetail'
orderId: '...'

但不要让服务端或外部输入直接决定任意 ArkUI 页面名称。实际项目里更适合维护一份白名单映射,例如:

type NotificationTarget = 'OrderDetail' | 'ChatDetail';

再由应用内部把它映射到真实路由。

五、第二步:把 Want 包装成 WantAgent

接下来创建 WantAgent。

根据官方当前接口定义,WantAgentInfo 可以包含目标 Want、操作类型以及 requestCode;启动 UIAbility 时使用:

wantAgent.OperationType.START_ABILITY

官方文档将这个枚举值定义为“启动带 UI 的 Ability”。

最小代码可以写成:

import { Want, wantAgent } from '@kit.AbilityKit';

async function createOrderWantAgent(
  orderId: string
): Promise<wantAgent.WantAgent> {
  const targetWant: Want = {
    bundleName: 'com.example.notificationdemo',
    abilityName: 'EntryAbility',
    parameters: {
      targetPage: 'OrderDetail',
      orderId: orderId
    }
  };

  const info: wantAgent.WantAgentInfo = {
    wants: [targetWant],
    actionType: wantAgent.OperationType.START_ABILITY,
    requestCode: 100
  };

  return await wantAgent.getWantAgent(info);
}

这里没有额外创造通知跳转 API。

核心仍然是官方定义的:

wantAgent.getWantAgent(...)

官方当前文档提供 Promise 形式:

getWantAgent(info: WantAgentInfo): Promise<WantAgent>

并且官方 2026 年更新的其他 Kit 示例仍在使用同一套 WantAgentInfo + START_ABILITY + getWantAgent() 组合。

六、第三步:发布本地通知

拿到 WantAgent 后,把它设置到 NotificationRequest.wantAgent。

import { notificationManager } from '@kit.NotificationKit';

async function publishOrderNotification(orderId: string): Promise<void> {
  const orderWantAgent = await createOrderWantAgent(orderId);

  const request: notificationManager.NotificationRequest = {
    id: 1001,
    content: {
      notificationContentType:
        notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
      normal: {
        title: '订单已发货',
        text: `订单 ${orderId} 已进入配送流程`
      }
    },
    notificationSlotType:
      notificationManager.SlotType.SOCIAL_COMMUNICATION,
    wantAgent: orderWantAgent
  };

  await notificationManager.publish(request);
}

这段代码的结构可以直接在华为官方现行示例中找到对应关系:

notificationManager.NotificationRequest
notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT
notificationManager.SlotType.SOCIAL_COMMUNICATION
wantAgent
notificationManager.publish(...)

官方 Location Kit 的最新 API 示例也展示了完全相同的通知发布结构:先通过 getWantAgent() 获取 WantAgent,再写入 NotificationRequest.wantAgent,最后调用 notificationManager.publish()。

这里的 SlotType 不应该机械照抄。实际产品应根据真实通知业务分类选择符合官方通知规范的类型,示例使用该值只是为了保持最小代码与官方现有示例结构一致。

另外,本地通知是否能够正常展示还受用户通知授权和系统通知设置影响。华为 Notification Kit 将“申请通知授权”作为独立开发步骤,Push Kit 官方文档也明确建议在发送通知前调用 requestEnableNotification() 引导用户允许通知。

因此,如果代码执行了 publish() 但通知没有出现,排查顺序不要第一时间放到 WantAgent 上,先确认通知授权和通知本身是否已经成功展示。

七、点击通知以后,UIAbility 到底从哪里拿参数

点击通知后,系统触发 WantAgent,其中的 Want 被用于启动目标 UIAbility。

UIAbility 侧需要处理两类入口。

先抽一个公共函数:

import { Want } from '@kit.AbilityKit';

function handleNotificationWant(want: Want): void {
  const parameters = want.parameters;
  if (!parameters) {
    return;
  }

  const targetPage = parameters['targetPage'] as string | undefined;
  const orderId = parameters['orderId'] as string | undefined;

  if (targetPage !== 'OrderDetail' || !orderId) {
    return;
  }

  AppStorage.setOrCreate<string>('notificationOrderId', orderId);
}

然后在 UIAbility 中同时处理:

import {
  AbilityConstant,
  UIAbility,
  Want
} from '@kit.AbilityKit';

export default class EntryAbility extends UIAbility {
  onCreate(
    want: Want,
    launchParam: AbilityConstant.LaunchParam
  ): void {
    handleNotificationWant(want);
  }

  onNewWant(
    want: Want,
    launchParam: AbilityConstant.LaunchParam
  ): void {
    handleNotificationWant(want);
  }
}

这里非常容易出现一个理解偏差:

不要只在 onCreate() 里读取通知参数。

华为当前 UIAbility 相关 API 示例明确同时展示了从 onCreate(want, ...) 和 onNewWant(want, ...) 中读取 want.parameters 的方式。官方其他开发指南也分别把 onCreate() 用于冷启动数据接收、onNewWant() 用于已有实例再次获得新 Want 的处理。

这正是“第一次点通知能进去,应用已经开着以后再点却不跳”的高频原因之一。

八、为什么不能直接在 UIAbility 里操作页面栈

UIAbility 收到的只是系统层启动参数。

实际的 Navigation 和 NavPathStack 属于 ArkUI 页面层。

华为当前 Navigation 文档说明,页面路由操作建立在 NavPathStack 上,由它负责页面跳转、替换、删除、参数获取等操作;从 API version 10 开始,官方更推荐使用基于 NavPathStack 的 Navigation 页面路由。

因此一个比较清晰的结构是:

EntryAbility
    ↓
把通知参数写入应用级状态
    ↓
ArkUI 根页面监听状态
    ↓
NavPathStack 执行业务路由

这里用 AppStorage 做最小桥接。

华为官方文档中,UIAbility 把运行时状态写入 AppStorage,ArkUI 组件再通过 @StorageLink 监听变化,是已经存在的官方用法。

九、ArkUI 收到订单 ID 后真正进入详情页

在根页面创建 Navigation:

@Entry
@Component
struct Index {
  @Provide('navPathStack')
  navPathStack: NavPathStack = new NavPathStack();

  @StorageLink('notificationOrderId')
  @Watch('onNotificationOrderChanged')
  notificationOrderId: string = '';

  private lastHandledOrderId: string = '';

  onNotificationOrderChanged(): void {
    const orderId = this.notificationOrderId;

    if (!orderId || orderId === this.lastHandledOrderId) {
      return;
    }

    this.lastHandledOrderId = orderId;

    this.navPathStack.pushPath({
      name: 'OrderDetail',
      param: {
        orderId: orderId
      }
    });
  }

  build() {
    Navigation(this.navPathStack) {
      Column() {
        Text('订单首页')
      }
      .width('100%')
      .height('100%')
    }
  }
}

这里使用的是官方 Navigation 文档中给出的基础跳转模型:

this.pageStack.pushPath({
  name: 'pageOne',
  param: ...
});

NavPathStack 可以通过页面名称和 param 携带页面参数。

@StorageLink + @Watch 的组合也有官方现行示例:UIAbility 或其他模块调用 AppStorage.setOrCreate() 改变应用级状态后,绑定该 key 的 ArkUI 属性收到变化,再执行相应 UI 逻辑。

需要提醒的是,上面的代码展示的是路由桥接机制,并没有包含完整 Navigation 路由表配置。OrderDetail 必须已经按照项目使用的 Navigation 路由方式注册,否则根据官方 Navigation 规则,页面名称找不到对应 NavDestination 时会跳转失败。

十、冷启动和应用已经运行,区别到底在哪里

把整个链路放到一起看,会清楚很多。

冷启动

应用进程和 UIAbility 都不存在:

点击通知
→ WantAgent
→ 创建 EntryAbility
→ onCreate(want)
→ 保存 orderId
→ 创建页面
→ Navigation
→ OrderDetail

这个场景里,通知参数首次出现在:

onCreate(want, ...)

已经运行

如果目标 UIAbility 已经存在并在相应启动模式下被复用,那么新的 Want 不应该只依赖首次创建逻辑处理,而要关注:

onNewWant(want, ...)

官方 API 示例同样在这两个生命周期入口中读取 Want 参数。

因此最安全的业务组织方式并不是复制两套逻辑,而是像前面的代码一样:

onCreate(want: Want): void {
  handleNotificationWant(want);
}

onNewWant(want: Want): void {
  handleNotificationWant(want);
}

让两个入口最终落到同一个解析函数。

十一、怎样避免连续点击通知反复打开同一个详情页

通知路由做好以后,很快还会碰到第二个问题:

OrderDetail(123)
   ↓
再次点击订单123通知
   ↓
OrderDetail(123)
   ↓
再次点击
   ↓
OrderDetail(123)

如果每次都无条件 pushPath(),页面栈就可能不断增加相同详情页。

这里至少应该做两层防重。

第一层是“同一通知事件不要重复消费”。

前面的:

private lastHandledOrderId: string = '';

就是一个最小示例。

实际项目里更合适的是组合:

业务类型 + 业务 ID

例如:

const routeKey = `order:${orderId}`;

聊天则可以是:

const routeKey = `chat:${conversationId}`;

第二层是利用 Navigation 自己的页面栈语义处理“详情页已经存在”的情况。

华为当前 Navigation 文档明确提供页面栈查询、页面移动、替换和单例跳转等能力,并建议通过 NavPathStack 自身维护和查询实际路由栈,而不是依靠监听页面生命周期自行维护一份假的路由栈。

因此正式项目可以把:

pushPath(...)

再封装成统一的:

openOrderDetail(orderId)

由这个方法检查当前路由栈,再决定是入栈、复用还是移动已有页面。

这样通知、首页订单列表、搜索结果、历史记录等入口最终都使用同一套路由规则,不会出现“从通知打开是一套逻辑,从订单列表打开又是一套逻辑”。

十二、通知内容和落地页数据不要各管各的

还有一个很实际的问题:

通知产生时:

订单状态:已发货

用户过十分钟才点击。

这十分钟里订单可能已经变成:

配送中

如果通知点击后把通知里的整个旧订单对象直接渲染出来,就可能出现:

通知:已发货
详情页:仍显示已发货旧数据

更稳妥的设计是通知只承担定位作用:

parameters: {
  orderId: 'ORDER_20260923_001'
}

进入 OrderDetail 后,再由详情页按照当前项目的数据层设计读取最新订单状态。

这样 Want 中携带的是“我要看哪个对象”,而不是“这个对象现在所有字段是什么”。

这也意味着 parameters 应保持轻量。订单 ID、会话 ID、文章 ID 这类定位字段更适合做通知路由参数。

十三、几个特别容易理解错的地方

1. WantAgent 不是 ArkUI 页面路由

WantAgent 能解决的是:

通知 → Ability

它不能替你完成:

Ability → NavDestination

后半段仍然是应用自己的 Navigation 路由。

2. abilityName 不是详情页面名称

这一点很重要。

abilityName: 'EntryAbility'

代表 UIAbility。

而:

name: 'OrderDetail'

代表 Navigation 中注册的 NavDestination。

两者属于完全不同的层级,不要把:

abilityName: 'OrderDetail'

当成“直接打开 OrderDetail 页面”的方式,除非项目本来就真的存在一个名为 OrderDetail 的 UIAbility。

3. 不要只处理首次启动参数

只处理:

onCreate()

会让逻辑天然偏向冷启动。

通知点击这种入口需要同时考虑 UIAbility 已存在时新的 Want 如何进入业务逻辑,因此应把参数解析逻辑复用到 onNewWant()。

4. requestCode 不要完全当成无意义字段

WantAgentInfo 本身包含 requestCode。如果应用同时创建大量不同业务目的的 WantAgent,不要在没有设计的情况下所有地方永远写同一个固定值。

本文为了突出通知路由主线使用:

requestCode: 100

实际项目应建立稳定的 WantAgent 创建策略,并结合 WantAgentFlags 的官方语义决定已有 WantAgent 是更新、取消还是保持不变。官方枚举中当前提供 UPDATE_PRESENT_FLAG、CANCEL_PRESENT_FLAG、CONSTANT_FLAG 等行为。

如果暂时不需要这些行为,不要为了“代码看起来高级”随便添加 flag。

十四、实际项目可以按这个顺序排查

当出现“通知显示正常,但点击只进首页”时,可以按下面顺序看:

  1. 先确认通知本身是否正常发布。
    如果通知都没展示,先排查通知授权、系统通知开关以及 publish(),不要直接查 Navigation。

  2. 检查 NotificationRequest 是否真的设置了 wantAgent。
    没有它,通知展示和业务启动并没有建立本文所讲的行为意图链路。

  3. 检查 Want 目标。
    bundleName 和 abilityName 是否真的是当前应用中允许启动的目标 Ability。第三方应用通过 WantAgent 只能设置自身 Ability。

  4. 检查业务 ID 是否真的放进 parameters。
    不要只传 targetPage,却忘了传订单 ID。

  5. 同时检查 onCreate() 和 onNewWant()。
    尤其是“应用关闭时正常,开着时失效”的情况。

  6. 确认 UIAbility 收到参数后确实交给了 ArkUI。
    如果 orderId 已经能在 Ability 层看到,问题基本已经从 Notification Kit 转移到应用内部路由。

  7. 检查 Navigation 路由表。
    OrderDetail 没有注册、页面名称不一致,同样会导致业务页进不去。

  8. 检查是否被去重逻辑误拦截。
    如果使用 lastHandledOrderId 一类状态,要明确它何时应该清空,以及不同业务事件能否产生相同 ID。

十五、把核心代码再收拢一下

完整链路其实只有三段。

通知侧:

const targetWant: Want = {
  bundleName: 'com.example.notificationdemo',
  abilityName: 'EntryAbility',
  parameters: {
    targetPage: 'OrderDetail',
    orderId: orderId
  }
};

const agent = await wantAgent.getWantAgent({
  wants: [targetWant],
  actionType: wantAgent.OperationType.START_ABILITY,
  requestCode: 100
});

await notificationManager.publish({
  id: 1001,
  content: {
    notificationContentType:
      notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
    normal: {
      title: '订单已发货',
      text: `订单 ${orderId} 已进入配送流程`
    }
  },
  notificationSlotType:
    notificationManager.SlotType.SOCIAL_COMMUNICATION,
  wantAgent: agent
});

UIAbility:

private handleWant(want: Want): void {
  const orderId =
    want.parameters?.['orderId'] as string | undefined;

  if (orderId) {
    AppStorage.setOrCreate(
      'notificationOrderId',
      orderId
    );
  }
}

onCreate(
  want: Want,
  launchParam: AbilityConstant.LaunchParam
): void {
  this.handleWant(want);
}

onNewWant(
  want: Want,
  launchParam: AbilityConstant.LaunchParam
): void {
  this.handleWant(want);
}

ArkUI:

@StorageLink('notificationOrderId')
@Watch('onNotificationOrderChanged')
notificationOrderId: string = '';

onNotificationOrderChanged(): void {
  if (!this.notificationOrderId) {
    return;
  }

  this.navPathStack.pushPath({
    name: 'OrderDetail',
    param: {
      orderId: this.notificationOrderId
    }
  });
}

真正需要记住的不是这三段代码,而是它们之间的边界:

WantAgent 负责把系统事件送进应用
Want.parameters 负责传业务定位参数
UIAbility 负责接参数
Navigation 负责真正进入业务页面

开发经验总结

通知点击只打开首页时,不要把排查重点全部放在通知 UI 上。

Notification Kit 已经提供了通知与 WantAgent 的连接点,Ability Kit 则负责把 Want 和目标 UIAbility 串起来;进入应用后,再由 ArkUI Navigation 完成业务页面路由。官方当前示例也仍然采用 @kit.NotificationKit + @kit.AbilityKit + WantAgent 这一组合。

这类功能真正需要处理好的有四件事:

  • Want 中携带稳定、最小化的业务 ID;
  • 通知必须设置正确的 WantAgent;
  • UIAbility 同时处理首次 Want 和后续新的 Want;
  • 应用内部建立统一的业务路由和页面去重规则。

做到这里,“点击通知只进首页”就不再是一个 Notification API 问题,而会变成一条很清晰、可以逐层观察的路由链路。

如果正在做聊天、订单、审批、内容更新这类通知,可以重点检查一下当前项目:通知里的业务上下文,究竟是在 WantAgent 触发后继续传到了业务页面,还是在 EntryAbility 那一层就已经丢掉了?

📝 写在最后

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

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

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

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

Logo

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

更多推荐