👋你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 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 | 本文用途 |
|---|---|---|
notificationManager | Notification Kit | 发布本地通知 |
NotificationRequest.wantAgent | Notification Kit | 指定通知点击后的行为 |
wantAgent.getWantAgent() | Ability Kit | 创建 WantAgent |
Want.parameters | Ability Kit | 携带orderId等业务参数 |
UIAbility.onCreate()/onNewWant() | Ability Kit | 接收启动 Want |
NavPathStack | ArkUI | 在应用内部跳到详情页 |
当前华为官方 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';constorderId:string='ORDER_20260923_001';constwant: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 页面名称。实际项目里更适合维护一份白名单映射,例如:
typeNotificationTarget='OrderDetail'|'ChatDetail';再由应用内部把它映射到真实路由。
五、第二步:把 Want 包装成 WantAgent
接下来创建 WantAgent。
根据官方当前接口定义,WantAgentInfo可以包含目标 Want、操作类型以及requestCode;启动 UIAbility 时使用:
wantAgent.OperationType.START_ABILITY官方文档将这个枚举值定义为“启动带 UI 的 Ability”。
最小代码可以写成:
import{Want,wantAgent}from'@kit.AbilityKit';asyncfunctioncreateOrderWantAgent(orderId:string):Promise<wantAgent.WantAgent>{consttargetWant:Want={bundleName:'com.example.notificationdemo',abilityName:'EntryAbility',parameters:{targetPage:'OrderDetail',orderId:orderId}};constinfo:wantAgent.WantAgentInfo={wants:[targetWant],actionType:wantAgent.OperationType.START_ABILITY,requestCode:100};returnawaitwantAgent.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';asyncfunctionpublishOrderNotification(orderId:string):Promise<void>{constorderWantAgent=awaitcreateOrderWantAgent(orderId);constrequest:notificationManager.NotificationRequest={id:1001,content:{notificationContentType:notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,normal:{title:'订单已发货',text:`订单${orderId}已进入配送流程`}},notificationSlotType:notificationManager.SlotType.SOCIAL_COMMUNICATION,wantAgent:orderWantAgent};awaitnotificationManager.publish(request);}这段代码的结构可以直接在华为官方现行示例中找到对应关系:
notificationManager.NotificationRequest notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXTnotificationManager.SlotType.SOCIAL_COMMUNICATIONwantAgent 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';functionhandleNotificationWant(want:Want):void{constparameters=want.parameters;if(!parameters){return;}consttargetPage=parameters['targetPage']asstring|undefined;constorderId=parameters['orderId']asstring|undefined;if(targetPage!=='OrderDetail'||!orderId){return;}AppStorage.setOrCreate<string>('notificationOrderId',orderId);}然后在 UIAbility 中同时处理:
import{AbilityConstant,UIAbility,Want}from'@kit.AbilityKit';exportdefaultclassEntryAbilityextendsUIAbility{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@Componentstruct Index{@Provide('navPathStack')navPathStack:NavPathStack=newNavPathStack();@StorageLink('notificationOrderId')@Watch('onNotificationOrderChanged')notificationOrderId:string='';privatelastHandledOrderId:string='';onNotificationOrderChanged():void{constorderId=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(),页面栈就可能不断增加相同详情页。
这里至少应该做两层防重。
第一层是“同一通知事件不要重复消费”。
前面的:
privatelastHandledOrderId:string='';就是一个最小示例。
实际项目里更合适的是组合:
业务类型 + 业务 ID例如:
constrouteKey=`order:${orderId}`;聊天则可以是:
constrouteKey=`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。
十四、实际项目可以按这个顺序排查
当出现“通知显示正常,但点击只进首页”时,可以按下面顺序看:
先确认通知本身是否正常发布。
如果通知都没展示,先排查通知授权、系统通知开关以及publish(),不要直接查 Navigation。检查 NotificationRequest 是否真的设置了
wantAgent。
没有它,通知展示和业务启动并没有建立本文所讲的行为意图链路。检查 Want 目标。
bundleName和abilityName是否真的是当前应用中允许启动的目标 Ability。第三方应用通过 WantAgent 只能设置自身 Ability。检查业务 ID 是否真的放进
parameters。
不要只传targetPage,却忘了传订单 ID。同时检查
onCreate()和onNewWant()。
尤其是“应用关闭时正常,开着时失效”的情况。确认 UIAbility 收到参数后确实交给了 ArkUI。
如果orderId已经能在 Ability 层看到,问题基本已经从 Notification Kit 转移到应用内部路由。检查 Navigation 路由表。
OrderDetail没有注册、页面名称不一致,同样会导致业务页进不去。检查是否被去重逻辑误拦截。
如果使用lastHandledOrderId一类状态,要明确它何时应该清空,以及不同业务事件能否产生相同 ID。
十五、把核心代码再收拢一下
完整链路其实只有三段。
通知侧:
consttargetWant:Want={bundleName:'com.example.notificationdemo',abilityName:'EntryAbility',parameters:{targetPage:'OrderDetail',orderId:orderId}};constagent=awaitwantAgent.getWantAgent({wants:[targetWant],actionType:wantAgent.OperationType.START_ABILITY,requestCode:100});awaitnotificationManager.publish({id:1001,content:{notificationContentType:notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,normal:{title:'订单已发货',text:`订单${orderId}已进入配送流程`}},notificationSlotType:notificationManager.SlotType.SOCIAL_COMMUNICATION,wantAgent:agent});UIAbility:
privatehandleWant(want:Want):void{constorderId=want.parameters?.['orderId']asstring|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 那一层就已经丢掉了?
📝 写在最后
如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!
我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!
感谢你的阅读,我们下篇文章再见~👋
✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。