HarmonyOS开发:消息推送Push消息

举报
Jack20 发表于 2026/06/25 20:59:25 2026/06/25
【摘要】 HarmonyOS开发:消息推送Push消息📌 核心要点:Push消息不是简单的"发个通知",通知消息系统展示、透传消息静默处理、推送目标精准触达、送达率统计闭环,每个环节都有讲究。 背景与动机你的App需要给用户发消息。最简单的做法?轮询。客户端每隔5秒问一次服务器"有新消息吗?"——然后你的服务器被10万个客户端每5秒打一次,直接打爆。更离谱的是,轮询在后台跑,手机电量哗哗往下掉。用...

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做了以下更新:

  1. 实时活动推送:新增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);
  1. 推送分组与摘要:通知消息支持分组展示,同一组的通知折叠显示,避免通知栏被刷屏。可以在推送时指定group字段。

  2. 推送静默时段:新增静默时段配置,用户可以设置"晚上10点到早上8点不接收营销推送"。服务端推送时会自动遵守这个设置。

  3. 推送回执增强:新增消息展示回执和点击回执。之前只有送达回执,现在可以知道消息是否被展示了、用户是否点击了,推送效果统计更精确。

  4. 多Token支持:同一用户的多设备场景优化。用户有手机和平板,推送可以指定"只推手机"或"手机平板都推"。

总结

Push消息是App和用户保持连接的核心通道。通知消息给用户看,透传消息给App用,主题推送做精准触达——三种方式配合使用,才能既不打扰用户又不丢失关键信息。

核心记住三点:

  • 通知权限要引导,用户关了通知你的推送就废了
  • 透传消息在后台不可靠,关键消息必须用通知消息
  • 推送频率要克制,用户关通知只需要一秒钟
评估维度 说明
学习难度 ⭐⭐⭐ 基础推送简单,透传消息处理和送达优化需要经验
使用频率 ⭐⭐⭐⭐⭐ 几乎所有应用都需要推送能力
重要程度 ⭐⭐⭐⭐⭐ 没有推送,App就是一座孤岛

Push消息不是"发个通知"这么简单。推送策略决定了用户是"每天打开你的App"还是"关掉通知再也不看"。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。