HarmonyOS开发:消息推送Push消息
HarmonyOS开发:消息推送Push消息
📌 核心要点:Push消息不是简单的"发个通知",通知消息系统展示、透传消息静默处理、推送目标精准触达、送达率统计闭环,每个环节都有讲究。
背景与动机
你的App需要给用户发消息。最简单的做法?轮询。客户端每隔5秒问一次服务器"有新消息吗?"——然后你的服务器被10万个客户端每5秒打一次,直接打爆。
更离谱的是,轮询在后台跑,手机电量哗哗往下掉。用户一看电池统计,你的App占了30%的耗电,直接卸载。
Push消息就是来解决这个问题的。服务端主动推,客户端被动收,不轮询、不费电、实时到达。
但Push消息不是"调个API就完事了"。通知消息和透传消息的区别你搞清楚了吗?推送目标怎么选?消息被系统拦截了怎么办?送达率怎么统计?这些问题不搞清楚,你的推送要么用户看不到,要么看到了不想看直接关通知。
核心原理
Push消息的核心架构:应用服务端 → Push服务端 → 设备端。 你的服务器不直接连用户设备,而是通过华为Push服务中转。
flowchart TD
A[应用服务端] --> B[调用Push API]
B --> C[华为Push服务]
C --> D{消息类型}
D -->|通知消息| E[系统通知栏展示]
D -->|透传消息| F[应用静默接收]
E --> G[用户点击通知]
G --> H[打开App对应页面]
F --> I[应用处理数据]
I --> J{处理方式}
J -->|更新UI| K[刷新页面内容]
J -->|本地通知| L[生成通知栏消息]
J -->|静默同步| M[后台同步数据]
C --> N[推送目标筛选]
N --> O{目标类型}
O -->|全部设备| P[广播推送]
O -->|指定Token| Q[单设备推送]
O -->|主题订阅| R[按兴趣分组推送]
O -->|条件筛选| S[按用户标签推送]
classDef server fill:#1565C0,color:#fff,stroke:#0D47A1
classDef push fill:#E65100,color:#fff,stroke:#BF360C
classDef notify fill:#2E7D32,color:#fff,stroke:#1B5E20
classDef target fill:#6A1B9A,color:#fff,stroke:#4A148C
class A,B,server
class C,D,push
class E,F,G,H,I,J,K,L,M,notify
class N,O,P,Q,R,S,target
通知消息 vs 透传消息
这是Push消息最基础也最重要的分类:
| 对比项 | 通知消息 | 透传消息 |
|---|---|---|
| 展示方式 | 系统通知栏展示 | 应用自己处理 |
| 用户感知 | 明显,能看到通知 | 无感,后台静默处理 |
| App在前台 | 可配置是否展示 | 直接收到的回调 |
| App在后台 | 系统展示通知 | 收到回调(受系统限制) |
| App已杀进程 | 系统展示通知 | 可能收不到 |
| 适用场景 | 营销推送、消息提醒 | 数据同步、静默更新 |
简单说:通知消息是"给用户看的",透传消息是"给App用的"。
推送Token
每个设备安装App后,Push服务会分配一个唯一的Push Token。你的服务端需要保存这个Token,推送时指定Token才能送达。
Token不是永久的。用户卸载重装、清除数据、系统升级,Token都可能变化。你需要在Token变化时更新服务端的记录。
代码实战
基础用法:获取Push Token并接收通知
第一步:获取Push Token,上传到你的服务端。第二步:接收推送消息。
// PushManager.ets
import { pushService } from '@kit.PushKit';
import { notification } from '@kit.NotificationKit';
import { AbilityConstant, Want } from '@kit.AbilityKit';
export class PushManager {
private static instance: PushManager;
private pushToken: string = '';
private constructor() {}
static getInstance(): PushManager {
if (!PushManager.instance) {
PushManager.instance = new PushManager();
}
return PushManager.instance;
}
/**
* 初始化Push服务
* 在EntryAbility的onCreate中调用
*/
async init(): Promise<void> {
try {
// 1. 获取Push Token
const token = await pushService.getToken();
this.pushToken = token;
console.info(`[Push] 获取Token成功: ${token}`);
// 2. 上报Token到应用服务端
await this.reportTokenToServer(token);
// 3. 监听Token变化
pushService.on('token', (newToken: string) => {
console.info(`[Push] Token更新: ${newToken}`);
this.pushToken = newToken;
this.reportTokenToServer(newToken);
});
// 4. 请求通知权限
await this.requestNotificationPermission();
console.info('[Push] 初始化完成');
} catch (error) {
console.error(`[Push] 初始化失败: ${JSON.stringify(error)}`);
}
}
/**
* 上报Push Token到服务端
*/
private async reportTokenToServer(token: string): Promise<void> {
try {
// 调用你的服务端接口,保存Token
// await apiClient.post('/push/register', { token: token });
console.info(`[Push] Token已上报: ${token}`);
} catch (error) {
console.error(`[Push] Token上报失败: ${JSON.stringify(error)}`);
}
}
/**
* 请求通知权限
* HarmonyOS必须用户授权才能展示通知
*/
private async requestNotificationPermission(): Promise<boolean> {
try {
const isEnabled = await notification.isNotificationEnabled();
if (!isEnabled) {
// 弹出系统权限请求对话框
await notification.requestNotificationPermission();
}
return true;
} catch (error) {
console.error(`[Push] 请求通知权限失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 获取当前Push Token
*/
getPushToken(): string {
return this.pushToken;
}
/**
* 订阅主题
* 订阅后,推送到该主题的消息会自动收到
*/
async subscribeTopic(topic: string): Promise<boolean> {
try {
await pushService.subscribe(topic);
console.info(`[Push] 订阅主题成功: ${topic}`);
return true;
} catch (error) {
console.error(`[Push] 订阅主题失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 取消订阅主题
*/
async unsubscribeTopic(topic: string): Promise<boolean> {
try {
await pushService.unsubscribe(topic);
console.info(`[Push] 取消订阅成功: ${topic}`);
return true;
} catch (error) {
console.error(`[Push] 取消订阅失败: ${JSON.stringify(error)}`);
return false;
}
}
}
进阶用法:透传消息处理与本地通知
透传消息不会自动展示通知,需要你在回调中自己处理。
// PushMessageHandler.ets
import { pushService, PushMessage } from '@kit.PushKit';
import { notification } from '@kit.NotificationKit';
import { router } from '@kit.ArkUI';
// 透传消息类型
enum MessageType {
NEW_ARTICLE = 'new_article', // 新文章
COMMENT_REPLY = 'comment_reply', // 评论回复
SYSTEM_NOTICE = 'system_notice', // 系统通知
DATA_SYNC = 'data_sync' // 数据同步
}
export class PushMessageHandler {
private static instance: PushMessageHandler;
private constructor() {}
static getInstance(): PushMessageHandler {
if (!PushMessageHandler.instance) {
PushMessageHandler.instance = new PushMessageHandler();
}
return PushMessageHandler.instance;
}
/**
* 注册消息监听
* 在Ability的onCreate中调用
*/
registerMessageListener(): void {
// 监听通知消息点击事件
pushService.on('notificationClick', (want: Want) => {
this.handleNotificationClick(want);
});
// 监听透传消息
pushService.on('pushMessage', (message: PushMessage) => {
this.handlePushMessage(message);
});
console.info('[PushHandler] 消息监听已注册');
}
/**
* 处理通知消息点击
*/
private handleNotificationClick(want: Want): void {
const extras = want.parameters;
if (!extras) return;
const messageType = extras.messageType as string;
const targetId = extras.targetId as string;
console.info(`[PushHandler] 通知点击: type=${messageType}, id=${targetId}`);
// 根据消息类型跳转到对应页面
switch (messageType) {
case MessageType.NEW_ARTICLE:
router.pushUrl({
url: 'pages/ArticleDetailPage',
params: { articleId: targetId }
});
break;
case MessageType.COMMENT_REPLY:
router.pushUrl({
url: 'pages/CommentDetailPage',
params: { commentId: targetId }
});
break;
case MessageType.SYSTEM_NOTICE:
router.pushUrl({
url: 'pages/NotificationPage',
params: {}
});
break;
}
}
/**
* 处理透传消息
*/
private handlePushMessage(message: PushMessage): void {
try {
// 解析消息数据
const data = message.getData();
if (!data) return;
const messageType = data.messageType as string;
console.info(`[PushHandler] 收到透传消息: type=${messageType}`);
switch (messageType) {
case MessageType.NEW_ARTICLE:
// 新文章通知:展示本地通知
this.showLocalNotification(
'新文章发布',
data.title as string || '您关注的作者发布了新文章',
{ messageType: messageType, targetId: data.articleId as string }
);
break;
case MessageType.COMMENT_REPLY:
// 评论回复:展示本地通知 + 更新未读计数
this.showLocalNotification(
'收到回复',
data.content as string || '有人回复了你的评论',
{ messageType: messageType, targetId: data.commentId as string }
);
this.updateUnreadCount();
break;
case MessageType.DATA_SYNC:
// 数据同步:静默处理,不展示通知
this.handleDataSync(data);
break;
case MessageType.SYSTEM_NOTICE:
// 系统通知:展示本地通知
this.showLocalNotification(
'系统通知',
data.content as string || '您有一条新的系统通知',
{ messageType: messageType, targetId: data.noticeId as string }
);
break;
}
} catch (error) {
console.error(`[PushHandler] 处理透传消息失败: ${JSON.stringify(error)}`);
}
}
/**
* 展示本地通知
* 透传消息不会自动展示,需要手动创建
*/
private showLocalNotification(
title: string,
content: string,
extras: Record<string, string>
): void {
try {
const request: notification.NotificationRequest = {
id: Date.now(), // 通知ID,唯一
content: {
notificationContentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: title,
text: content
}
},
// 点击通知时传递的参数
wantAgent: extras,
// 通知渠道ID(Android 8.0+必须)
slotType: notification.SlotType.SOCIAL_COMMUNICATION
};
notification.publish(request);
console.info(`[PushHandler] 本地通知已发布: ${title}`);
} catch (error) {
console.error(`[PushHandler] 发布本地通知失败: ${JSON.stringify(error)}`);
}
}
/**
* 更新未读消息计数
*/
private updateUnreadCount(): void {
// 更新AppStorage中的未读计数
const currentCount = AppStorage.get<number>('unreadCount') || 0;
AppStorage.setOrCreate('unreadCount', currentCount + 1);
}
/**
* 处理数据同步消息
*/
private handleDataSync(data: Record<string, Object>): void {
// 触发Cloud DB同步
// CloudDBManager.getInstance().syncFromCloud();
console.info('[PushHandler] 数据同步消息已处理');
}
}
完整示例:服务端推送与送达统计
客户端搞定了,服务端怎么推?推送效果怎么统计?
// === 服务端推送代码(Node.js) ===
// push-server.js
const axios = require('axios');
// 华为Push服务端API配置
const PUSH_CONFIG = {
appId: 'your_app_id',
appSecret: 'your_app_secret',
pushUrl: 'https://push-api.cloud.huawei.cn/v2/{appId}/messages:send'
};
/**
* 获取Push服务AccessToken
*/
async function getAccessToken(): Promise<string> {
const response = await axios.post(
'https://oauth-login.cloud.huawei.cn/oauth2/v3/token',
new URLSearchParams({
grant_type: 'client_credentials',
client_id: PUSH_CONFIG.appId,
client_secret: PUSH_CONFIG.appSecret
})
);
return response.data.access_token;
}
/**
* 发送通知消息(广播)
*/
async function sendBroadcastNotification(title: string, body: string, data?: Record<string, string>) {
const token = await getAccessToken();
const message = {
validate_only: false,
message: {
notification: {
title: title,
body: body,
image: data?.image || ''
},
android: {
notification: {
click_action: {
type: 1, // 打开应用
intent: `myapp://notification?type=${data?.type || 'default'}&id=${data?.id || ''}`
},
sound: '/raw/notification',
default_sound: true
}
},
data: data || {} // 透传数据
}
};
try {
const response = await axios.post(
PUSH_CONFIG.pushUrl.replace('{appId}', PUSH_CONFIG.appId),
message,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
console.info(`推送成功: ${JSON.stringify(response.data)}`);
return response.data;
} catch (error) {
console.error(`推送失败: ${JSON.stringify(error.response?.data)}`);
throw error;
}
}
/**
* 发送透传消息(指定Token)
*/
async function sendDataMessage(pushToken: string, data: Record<string, string>) {
const token = await getAccessToken();
const message = {
validate_only: false,
message: {
data: JSON.stringify(data), // 透传数据必须是JSON字符串
token: [pushToken] // 推送目标Token数组
}
};
try {
const response = await axios.post(
PUSH_CONFIG.pushUrl.replace('{appId}', PUSH_CONFIG.appId),
message,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
return response.data;
} catch (error) {
console.error(`透传推送失败: ${JSON.stringify(error.response?.data)}`);
throw error;
}
}
/**
* 按主题推送
*/
async function sendTopicMessage(topic: string, title: string, body: string) {
const token = await getAccessToken();
const message = {
validate_only: false,
message: {
notification: { title, body },
topic: topic // 推送到订阅了该主题的所有设备
}
};
const response = await axios.post(
PUSH_CONFIG.pushUrl.replace('{appId}', PUSH_CONFIG.appId),
message,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
return response.data;
}
// 导出
module.exports = {
sendBroadcastNotification,
sendDataMessage,
sendTopicMessage
};
// === 客户端推送效果统计 ===
// PushAnalytics.ets
import { analytics } from '@kit.AnalyticsKit';
export class PushAnalytics {
/**
* 记录推送消息接收
*/
static logMessageReceived(messageType: string, messageId: string): void {
analytics.onEvent({
eventId: 'push_received',
params: {
'message_type': messageType,
'message_id': messageId,
'receive_time': Date.now().toString()
}
});
}
/**
* 记录通知点击
*/
static logNotificationClicked(messageType: string, messageId: string): void {
analytics.onEvent({
eventId: 'push_clicked',
params: {
'message_type': messageType,
'message_id': messageId,
'click_time': Date.now().toString()
}
});
}
/**
* 记录推送导致的转化行为
* 比如用户点击推送后购买了商品
*/
static logPushConversion(messageType: string, messageId: string, action: string): void {
analytics.onEvent({
eventId: 'push_conversion',
params: {
'message_type': messageType,
'message_id': messageId,
'conversion_action': action
}
});
}
}
踩坑与注意事项
坑1:通知权限被拒绝
HarmonyOS的通知权限是用户手动授权的。用户拒绝了通知权限,你的通知消息一个都展示不了。
解决方案:在合适的时机引导用户开启通知权限,而不是一启动就弹。比如用户第一次收到消息时,提示"开启通知不错过重要消息"。
坑2:透传消息在后台可能收不到
App在后台时,透传消息的送达率受系统限制。HarmonyOS为了省电,会限制后台App的网络活动。如果你的App没有后台保活能力,透传消息可能被系统丢弃。
解决方案:关键消息用通知消息,不要用透传消息。透传消息只用于"丢了也没关系"的数据同步场景。
坑3:Push Token不是永久的
用户卸载重装App、清除App数据、系统升级,Token都会变。如果你服务端存的是旧Token,推送就发不到设备上。
建议:每次App启动时都检查Token是否变化,变化了就更新服务端。不要只在首次启动时上报。
坑4:推送频率别太高
用户一天收到20条推送,烦不烦?烦了就关通知。通知一关,你的推送渠道就废了。
建议:
- 同一用户每天不超过3条营销推送
- 重要消息(订单、安全提醒)不受限制
- 提供推送偏好设置,让用户选择接收哪些类型的推送
坑5:推送消息的图片有大小限制
通知消息的图片不超过1MB,建议512×256px。图片太大加载慢,影响通知展示。图片必须是HTTPS链接。
坑6:服务端AccessToken会过期
服务端调用Push API需要AccessToken,有效期约1小时。过期后需要重新获取。别把AccessToken缓存太久,否则推送失败还不知道原因。
HarmonyOS 6适配说明
HarmonyOS 6对Push Kit做了以下更新:
- 实时活动推送:新增Live Activity推送能力,可以在锁屏和通知中心展示实时更新的内容(如外卖进度、比赛比分)。类似iOS的Live Activity。
// HarmonyOS 6实时活动推送
const liveActivity = new pushService.LiveActivity({
id: 'order_12345',
title: '外卖配送中',
content: '骑手距离您还有2km',
progress: 0.7, // 进度70%
icon: 'https://myapp.com/delivery.png'
});
await pushService.startLiveActivity(liveActivity);
-
推送分组与摘要:通知消息支持分组展示,同一组的通知折叠显示,避免通知栏被刷屏。可以在推送时指定
group字段。 -
推送静默时段:新增静默时段配置,用户可以设置"晚上10点到早上8点不接收营销推送"。服务端推送时会自动遵守这个设置。
-
推送回执增强:新增消息展示回执和点击回执。之前只有送达回执,现在可以知道消息是否被展示了、用户是否点击了,推送效果统计更精确。
-
多Token支持:同一用户的多设备场景优化。用户有手机和平板,推送可以指定"只推手机"或"手机平板都推"。
总结
Push消息是App和用户保持连接的核心通道。通知消息给用户看,透传消息给App用,主题推送做精准触达——三种方式配合使用,才能既不打扰用户又不丢失关键信息。
核心记住三点:
- 通知权限要引导,用户关了通知你的推送就废了
- 透传消息在后台不可靠,关键消息必须用通知消息
- 推送频率要克制,用户关通知只需要一秒钟
| 评估维度 | 说明 |
|---|---|
| 学习难度 | ⭐⭐⭐ 基础推送简单,透传消息处理和送达优化需要经验 |
| 使用频率 | ⭐⭐⭐⭐⭐ 几乎所有应用都需要推送能力 |
| 重要程度 | ⭐⭐⭐⭐⭐ 没有推送,App就是一座孤岛 |
Push消息不是"发个通知"这么简单。推送策略决定了用户是"每天打开你的App"还是"关掉通知再也不看"。
- 点赞
- 收藏
- 关注作者
评论(0)