系列:《Web 到 HarmonyOS》· 插播(不占正篇序号)
适合人群:会 Web 通知 / 后端发消息,准备理解鸿蒙怎么把一条业务消息送到系统通知栏
推送不是页面功能,是一条跨端通道:业务服务器决定推给谁,通道负责送到设备,应用只负责领身份、开权限、处理点击。
鸿蒙侧离线通道仍是华为 Push Kit 。业务层不要再直连华为、小米、苹果各写一套。在线走个推通道,离线由个推代发到厂商通道。业务服务器只认 CID / 别名,不认各家 Token。
下面用正在看的 entry 说明方案落在哪一层,再写推送业务方法。模块代码只作对照,不代表推送已经接进工程。
一、一个简单的项目示例
假如工程是单模块应用,包名 com.example.myapplication,入口模块类型是 entry,只声明了手机。系统从 module.json5 的 mainElement: EntryAbility 拉起应用,窗口创建后加载 pages/Index。 已注册页面:pages/Index、pages/login、pages/Main/home。
| 位置 | 模块里已有的职责 | 方案落点 |
|---|---|---|
EntryAbility |
设颜色模式、注册 iconfont、loadContent('pages/Index') |
领 CID、听 Token 变化、处理点击。属于应用壳,不放进某个页面 |
pages/login.ets |
账号密码 + 短信倒计时 | 登录成功后把设备身份和业务账号绑在一起;短信组件不参与推送 |
pages/Main/main.ets |
底栏双 Tab,首页 / 个人中心各一把 NavPathStack |
点通知后按业务类型推进对应栈,不另开一套路由 |
HomePage / ProfilePage |
首页、个人中心 | 消息落地用现有页面,方案里不单建消息中心 |
http/HttpClient.ets |
类似 axios 的统一请求 | 上报 CID、绑定账号走它,不在 Ability 里再写一套请求 |
api/dictionary |
按业务拆 API 的写法 | 推送上报同样拆成独立 API,不把地址写死在 Ability 里 |
对前端的映射:
| Web | 当前 entry |
|---|---|
main.ts 挂载应用 |
EntryAbility.onWindowStageCreate |
登录页 router.replace 进后台 |
登录成功后进入主框架 |
| 后台布局 + 子路由 | Main 的 Tabs + 两把导航栈 |
| axios 实例 | HttpClient |
方案只涉及三处:EntryAbility(领身份、收点击)、登录成功(绑定账号)、module.json5(厂商通道所需的 client_id)。页面层不承担推送。
二、业务上要回答的四件事
| 问题 | Push Kit 自己做 | 多端用个推时 |
|---|---|---|
| 怎么认设备 | pushService.getToken(),得到华为 Push Token |
PushManager.initialize,得到个推 CID |
| 怎么认用户 | 登录后把 Token 和 userId 存到自己的库;或 bindAppProfileId 绑匿名账号 |
用业务账号做 别名,一个别名最多绑 10 个 CID |
| 怎么发出去 | 自己拿 Client Secret 换 Access Token,调华为 v3 messages:send |
业务服务器调个推 REST,个推按在线 / 离线选通道 |
| 点了去哪 | onCreate + onNewWant 读 want.parameters |
同样读 Want;离线点击还要把个推任务号埋点补回报表 |
鸿蒙设备上,进程不在也能出通知,靠的是系统级长连接,不是页面在后台轮询。这和浏览器里的 Notification 不一样:Web 通知跟着页面或 Service Worker 走,关掉标签往往就断了。
三、Push Kit 业务方法
把客户端收成一条线:开通服务、配置 Client ID、获取 Token、接收消息、处理通知跳转。服务端只交代怎么发,重点在客户端。Android 要分别接华为、小米、OPPO、vivo、魅族;鸿蒙是一套 Push Kit。多端产品仍然建议业务层走个推,鸿蒙这一跳由个推代发到 Push Kit。
官方五步。个推接入之后,业务服务器不直接打华为,第 3 步改成打个推,由个推在离线时代发到推送云:
1. 写代码之前:开通、Client ID、测试设备
控制台步骤官方文档已有,编码前必须完成三件:
- 在 AppGallery Connect 创建应用并开通推送服务。
- 在应用信息页拿到 Client ID。
- 联调阶段把调试设备的 Push Token 填进 测试设备列表,否则测试消息下不去。
client_id 配在 entry 的 module.json5,它是 Push Kit 认应用的标识,配错就收不到:
json
{
"module": {
"name": "entry",
"metadata": [
{
"name": "client_id",
"value": "AGC 上的 Client ID"
}
]
}
}
工程级 build-profile.json5 的 products.runtimeOS 必须是 HarmonyOS。client_id 是厂商通道认应用用的,和「业务是否已经在用个推」是两件事:个推负责多端,这条配置只服务鸿蒙离线。
自分类权益要单独申请。不开通时,通知会被当成资讯营销,每天每设备大约只能到 2 条 。IM、订单、物流必须带对 category,否则同样被降级限流。
| 类别 | 典型业务 | 频控 | 权益 |
|---|---|---|---|
| IM | 聊天、通话 | 申请通过后不按营销限额 | 需申请 |
| SERVICE / 服务与通讯 | 订单、物流、账号安全 | 同上 | 需申请 |
| MARKETING / 资讯营销 | 活动、推荐 | 每天每设备约 2~5 条 | 默认有,但是紧的 |
从 API 12 起,服务端用 category 区分类型,客户端用 notificationManager 建通知渠道(例如 order_channel、重要性 HIGH),用户才能按类开关,而不是整应用一刀切。
测试消息 testMessage=true 每个项目每天大约 1000 条,且目标 Token 很少,只能联调。
2. 领 Token,并盯着它变
Push Token 是这台设备上这个应用的推送标识,服务端靠它定向。在 EntryAbility.onCreate 里取,不要等首页 aboutToAppear:
typescript
const pushToken = await pushService.getToken();
this.uploadToken(pushToken);
三个注意点:
getToken()是异步的。第一次调用会向华为推送服务器注册设备,必须有网。- Token 会变(应用升级、恢复出厂、重装)。必须在
onCreate里听tokenUpdate,变了就重新上报。不听的话,服务器一直拿旧 Token,用户就再收不到。 - 这次失败不代表推送坏了。做重试,或下次启动再取。
typescript
pushService.on('tokenUpdate', (token: string) => {
this.uploadToken(token);
});
上报时带上 AAID (AAID.getAAID())。它用来做到达率去重和统计,不是用户账号:
| 特性 | 含义 |
|---|---|
| 应用隔离 | 同一台设备,每个应用的 AAID 不同,也读不到别人的 |
| 匿名 | 不绑定 IMEI、SN 这类硬件号 |
| 会重置 | 卸载重装或清除应用数据后改变 |
和登录的关系(方案,不是本仓库已经接上的步骤):
- 取到 Token 和 AAID,先落本地。用户还没登录就先存着,不要用空账号去绑。
- 登录成功之后,把 Token、AAID 和业务账号一起上报。直连华为时用可映射的匿名
profileId(bindAppProfileId),不要上传明文用户 id。已有个推时,这一步是给 CID 绑别名,别名用业务账号。 - 退出登录时解除绑定,避免下一任用户收到上一任的消息。
通知开关默认是关的。在合适时机调 requestEnableNotification(进消息相关页,而不是冷启动第一秒)。拒绝时常见错误码 1600004。
3. 两种消息,前台拦得住的只有透传
| 类型 | 谁展示 | 应用要做什么 |
|---|---|---|
| 通知消息 | 系统通知栏、锁屏、横幅、角标,应用不用写展示代码 | 服务端配标题、正文、图片、category、点击动作 |
| 数据消息 / 透传 | 系统不展示 | EntryAbility 里自己处理,适合静默更新或自定义样式 |
透传在 Ability 的接收回调里拿 data。例如聊天类可以自己决定要不要弹一条本地通知:
typescript
onReceive(want: Want, data: Record<string, string>): void {
if (data['msg_type'] === 'chat') {
this.showChatNotification(data['sender'], data['content']);
}
}
应用在前台时,系统默认仍会弹通知栏。聊天页想「直接插一条气泡、不弹系统通知」,只能对透传 注册 receiveMessage,再用 AppStorage 通知 UI:
typescript
pushService.on('receiveMessage', (data: Record<string, string>) => {
if (data['type'] === 'new_message') {
AppStorage.setOrCreate('newMessage', data);
}
});
receiveMessage 只对透传生效。通知消息的前台展示由系统控制,客户端拦截不了。要「前台不打扰、自己画 UI」,这条业务必须发透传,不能发通知消息。
点通知后的动作:
| actionType | 行为 | 方案里怎么用 |
|---|---|---|
| 0 | 打开应用首页 | 进首页或登录后的主框架 |
| 1 | 打开应用内指定页 | 按业务类型推进对应导航栈,并在 Ability skills 里声明 action |
| 2 | 打开网页 | 没有 Web 容器就不选 |
| 3 | 富媒体 | 没有富媒体页就不选 |
冷启动走 onCreate,进程已在且 Ability 为单例时走 onNewWant。只写一处,另一种点击没反应。排查「发出去了但点了没反应」时,两处都打日志,把 want 打出来:
typescript
hilog.info(0x0000, 'PushDebug', 'onCreate, want: %{public}s', JSON.stringify(want));
hilog.info(0x0000, 'PushDebug', 'onNewWant, want: %{public}s', JSON.stringify(want));
4. 服务端下发:客户端要知道的字段
服务端三步:用 Client ID + Client Secret 换 Access Token,调 Push API,由推送云下到设备。正式地址是 POST https://push-api.cloud.huawei.com/v3/{projectId}/messages:send,Access Token 大约 1 小时,要缓存。成功码常见 80000000。V3 面向 HarmonyOS NEXT / 5 及以后。
客户端要对齐的字段:
| 字段 | 作用 |
|---|---|
target.token |
指定设备 |
target.topic |
按主题,例如订了 order 的设备 |
payload.notification |
通知的标题、正文、图片 |
payload.data |
透传,客户端在接收回调里拿 |
payload.harmony |
鸿蒙专用配置;payload.android 是安卓侧,不要混用 |
消息要带 notifyId 才能撤回。撤回只覆盖还没下发、或已展示但用户还没点的那条。失效 Token(例如 80000001)要从库里清掉。
鸿蒙厂商通道往往不直接回点击报表。用个推时,离线通知的 Want 里要带上任务号,在 onCreate / onNewWant 里补报点击。
四、为什么多端用个推,而不是业务层直连 Push Kit
Push Kit 解决的是「鸿蒙这一台设备」。产品如果同时有 Android、iOS,直连华为意味着:
- Android 还要再接华为、荣耀、小米、OPPO、vivo、魅族的厂商通道,应用被杀后个推自己的长连接不可靠,到达率靠厂商;
- iOS 还要单独维护 APNs 证书;
- 业务服务器要为每个平台写一套鉴权、消息体、频控和失败重试。
个推把这三端收成一次调用。客户端 SDK 覆盖 Android、iOS、HarmonyOS NEXT;服务端用同一套 REST:按 CID、别名、标签或全量选人。
和「多端已经有个推」这个前提对齐的几点:
- 身份和登录模型对齐。 CID 是这台设备上这个应用的推送身份,登录前就能拿到。别名是业务账号(邮箱、手机号、
loginName)。同一账号登录最多 10 台设备时,按别名推一次,这些设备都能收到。这比「一张表只存一个华为 Token、后登录覆盖先登录」更符合多端。 - 在线 / 离线不用业务层判断。 应用在前台,走个推通道(
push_message)。在后台、锁屏或进程已退出,走厂商通道(push_channel,鸿蒙段就是华为 Push Kit)。通道策略可以按端配置:在线个推、离线厂商;只走厂商;只走个推;或厂商失败再回个推。 - 鸿蒙离线仍然要开华为。 只集成个推主包,只有在线推送。要锁屏和杀进程后仍能通知,还得在 AGC 开通 Push Kit、上传服务账号密钥、在
module.json5配华为client_id。个推会自己取 Push Token 并和 CID 绑定,业务代码不必再维护一套华为 OAuth。 - 运营和补量在同一处。 标签圈人、分组对比、撤回、覆盖、未送达查询、短信补量,都在个推侧。自建华为 Token 表只适合「只有鸿蒙、且必须自建」。多端已经用个推时,再为鸿蒙单独养一套,会和 Android / iOS 分裂成两套人马。
个推不是替代 Push Kit。鸿蒙离线的最后一跳仍是华为。个推省的是业务服务器和客户端的多端分叉。
五、业务顺序
顺序按「个推已经是多端入口」来排。推送逻辑放在 Ability 和登录成功之后,不散进页面。
落点只说明职责,不表示这些调用已经写进工程:
- 初始化 :
EntryAbility.onCreate,和应用壳里的全局初始化同级,不属于页面。 - 绑定:登录成功之后绑别名、上报 CID。验证码倒计时不参与推送。
- 点击回跳:用主框架已有的导航栈。例如个人类消息进个人中心栈,不为推送再开一套页面栈。
- 网络:上报走统一 HTTP 客户端,推送接口单独成模块。
- 配置 :
module.json5需要华为client_id,供个推走鸿蒙厂商通道;通知权限文案单独做字符串资源。
消息分类按业务定,不要全标营销:
| 业务 | 建议类别 | 别名 |
|---|---|---|
| 登录地异常、验证码已使用 | 服务与通讯 | 当前登录账号 |
| 运营提醒、内容推荐 | 营销,且每天克制 | 同上,或按标签 |
| 多设备登录互踢 | 服务与通讯 | 同一别名下的其它 CID |
六、和 Web 对照,避免用错心智
| Web 习惯 | 鸿蒙推送 |
|---|---|
页面里 new Notification() |
通知由系统展示,页面可以已经销毁 |
登录态存在内存 / localStorage |
设备身份(CID / Token)和登录态分开存,登录成功才绑定 |
| 一个接口打到自己的后端就结束 | 后端还要再打个推;鸿蒙离线时个推再打华为 |
路由 query 带上来源 |
点击参数在 Want,冷启动和热启动各读一次 |
| 浏览器通知权限是站点级 | 应用通知权限默认关,要显式申请 |
前端经验仍然有用的部分:消息体用类型描述(type、title、body、业务 id),点击后的跳转就是现有导航。变的是这条消息的生命周期不在 Vue 组件里。
七、结论
业务方法收成五步:AGC 开通并申请自分类(给鸿蒙离线用)→ Ability 里拿 CID(底层仍是 Push Token)→ 登录成功后用别名绑定账号 → 服务器只调个推 → 点击在 onCreate 和 onNewWant 都处理,再推进已有导航栈。
只有鸿蒙、且必须自建时,才直连 Push Kit。多端已经是个推,就不要再为鸿蒙单独养一套华为 Token 表。