postCardAction 三剑客:CALL / MESSAGE / ROUTER 精讲

在这里插入图片描述

postCardAction 概述

在 HarmonyOS 卡片开发中,postCardAction 是卡片与宿主应用之间通信的通用桥梁。无论是静态卡片还是互动卡片(Live Form),只要卡片需要向应用发送指令,都必须通过这个 API 完成。

postCardAction 接收两个参数:第一个是组件上下文(通常传入 this),第二个是一个包含 actionabilityNameparams 的配置对象。其中 action 字段决定了指令的类型,支持三种取值:MESSAGECALLROUTER

在 LiveCard 项目中,这三种 action 被封装为枚举 FormCarAction,定义在常量文件中:

[d:\HarmonyOS\WorkSpace\LiveCard\entry\src\main\ets\model\common\FormCardConstant.ets]

export enum FormCarAction {
  CALL = 'call',
  ROUTER = 'router',
  MESSAGE = 'message'
}

三种 action 各有明确的职责边界:

  • MESSAGE:向 FormExtensionAbility 发送消息事件,主要用于请求激活互动卡片
  • CALL:向应用 UIAbility 发送远程调用指令,主要用于业务操作(播放控制、收藏等)
  • ROUTER:基于命名路由跳转到应用内落地页

下面逐一深入剖析。


MESSAGE 类型:请求激活互动卡片的完整链路

从卡片点击到 onFormEvent 的事件链路

MESSAGE 类型走的是卡片 → FormExtensionAbility 的通信路径。当用户在卡片上点击某个按钮,需要触发互动卡片(Live Form)展开时,卡片调用 postCardAction,将消息发送到与之关联的 FormExtensionAbility 实例。

这条链路的终点是 FormExtensionAbility.onFormEvent 生命周期回调。来看 LiveCard 项目中的实现:

[d:\HarmonyOS\WorkSpace\LiveCard\entry\src\main\ets\entryformability\EntryFormAbility.ets]

async onFormEvent(formId: string, message: string): Promise<void> {
  const params: Record<string, Object> = JSON.parse(message);
  let shortMessage: string = params.message as string;

  if (shortMessage === 'requestOverflow') {
    let widthRatio: number = params.widthRatio as number;
    let heightRatio: number = params.heightRatio as number;
    let duration: number = params.duration as number;

    let triggerAction: string = params.triggerAction as string || '';
    let songId: string = params.songId as string || '';

    if (triggerAction) {
      MusicFileStore.storeTriggerAction(this.context, triggerAction, songId);
    }

    this.requestOverflow(formId, widthRatio, heightRatio, duration);
    return;
  } else if (shortMessage === 'updateRequestOverflowState') {
    updateRequestOverFlowState(formId, true);
  }
}

这里有几个关键细节值得注意:

  1. message 参数是一个 JSON 字符串,由卡片侧 postCardActionparams 对象序列化而来
  2. EntryFormAbility 通过解析 message 字段来区分不同的业务意图(requestOverflow vs updateRequestOverflowState
  3. 卡片传递的额外参数(如 triggerActionsongId)可以通过 params 透传进来

formProvider.requestOverflow 的触发时机与参数传递

requestOverflow 是激活互动卡片的关键 API。当 onFormEvent 收到 requestOverflow 消息后,会计算卡片展开的区域和动画时长,然后调用 formProvider.requestOverflow

private async requestOverflow(formId: string, widthRatio: number, heightRatio: number,
  duration: number): Promise<void> {
  try {
    let formRect: formInfo.Rect = await formProvider.getFormRect(formId);
    if (formRect.width <= 0 || formRect.height <= 0) {
      return;
    }
    let cardWidth = formRect.width * widthRatio;
    let cardHeight = formRect.height * heightRatio;
    let leftOffset = (formRect.width - cardWidth) / 2;
    let topOffset = (formRect.height - cardHeight) / 2;

    formProvider.requestOverflow(formId, {
      area: {
        left: leftOffset,
        top: topOffset,
        width: cardWidth,
        height: cardHeight
      },
      duration: duration
    }).catch((e: BusinessError) => {
      Logger.error(TAG, `requestOverflow error, code: ${e.code} message: ${e.message}`);
    });
  } catch (error) {
    Logger.error(TAG, `requestOverflow error: ${error}`);
  }
}

requestOverflow 的第二个参数是一个 OverflowArea 对象,包含:

  • area:定义互动卡片展开后的实际显示区域(位置和尺寸),通过 widthRatio / heightRatio 缩放系数计算出精确的偏移和尺寸
  • duration:展开动画的持续时间(单位毫秒),项目中统一使用 3500ms

各类型卡片使用不同的缩放系数:

export class LiveCardScale {
  public static readonly DELIVERY_WIDTH = 1.45;
  public static readonly DELIVERY_HEIGHT = 1.45;
  public static readonly EXERCISE_WIDTH = 1.4;
  public static readonly EXERCISE_HEIGHT = 1.4;
  public static readonly MUSIC_WIDTH = 1.1;
  public static readonly MUSIC_HEIGHT = 1.5;
  public static readonly SLEEP_WIDTH = 1.25;
  public static readonly SLEEP_HEIGHT = 1.5;
}

值得一提的是,对于音乐卡片这类需要传递额外上下文(如播放动作和歌曲 ID)的场景,项目使用了文件持久化作为中转:在 onFormEvent 中将 triggerActionsongId 写入文件,然后在 LiveFormExtensionAbility.onLiveFormCreate 中读取并传递给互动卡片 UI。这是因为卡片进程和互动卡片进程是独立运行的不同进程,无法直接共享内存数据。

以下是卡片侧发起 MESSAGE 请求的典型用法,以音乐卡片的播放按钮为例:

[d:\HarmonyOS\WorkSpace\LiveCard\entry\src\main\ets\widget\pages\MusicCard.ets]

ActionUtils.requestOverFlowWithAction(this, LiveCardScale.MUSIC_WIDTH,
  LiveCardScale.MUSIC_HEIGHT, LIVE_CARD_DURATION, 'PLAY', this.songId);

对应的 ActionUtils 封装方法:

[d:\HarmonyOS\WorkSpace\LiveCard\entry\src\main\ets\utils\ActionUtils.ets]

public requestOverFlowWithAction(component: object, widthRatio: number, heightRatio: number,
  duration: number, triggerAction: string, songId?: string): void {
  postCardAction(component, {
    action: FormCarAction.MESSAGE,
    abilityName: ENTRY_FORM_ABILITY,
    params: {
      message: 'requestOverflow',
      widthRatio: widthRatio,
      heightRatio: heightRatio,
      duration: duration,
      triggerAction: triggerAction,
      songId: songId || ''
    },
  });
}

CALL 类型:播放控制等操作指令

CardActionHandler 如何统一分发 CALL 请求

CALL 类型的 postCardAction 走的是卡片 → UIAbility.callee 的通信路径。卡片发送 CALL 请求到 UIAbility 后,系统通过 AbilityKit 的 Callee 机制将请求分发到注册的处理函数。

EntryAbility.onCreate 中注册 Callee 监听:

[d:\HarmonyOS\WorkSpace\LiveCard\entry\src\main\ets\entryability\EntryAbility.ets]

CardActionHandler.setContext(this.context);
this.callee.on('cardAction', CardActionHandler.getHandler());

这里的 calleeUIAbility 的内置能力,专门用于接收外部(包括卡片)的远程调用。卡片通过 postCardAction 发出的 CALL 请求,最终会被路由到 callee.on 注册的回调函数中。

CardActionHandler 是整个 CALL 请求的分发中枢,它解析参数中的 actionType 字段,将不同类型的请求分发到对应的处理方法:

[d:\HarmonyOS\WorkSpace\LiveCard\entry\src\main\ets\utils\CardActionHandler.ets]

private cardActionCall = (data: rpc.MessageSequence): null => {
  try {
    let params: Record<string, string> = JSON.parse(data.readString());
    Logger.info(TAG, `cardActionCall actionType: ${params.actionType}`);

    switch (params.actionType) {
      case CardActionType.PLAY_ACTION:
        this.handlePlayAction(params);
        break;
      case CardActionType.COLLECT_ACTION:
        this.handleCollectAction(params);
        break;
      case CardActionType.REQUEST_UPDATE:
        this.handleRequestUpdate(params);
        break;
      case CardActionType.EXERCISE_ACTION:
        this.handleExerciseAction(params);
        break;
      default:
        Logger.warn(TAG, `Unknown actionType: ${params.actionType}`);
    }
  } catch (err) {
    let error = err as BusinessError;
    Logger.error(TAG, `cardActionCall err, code: ${error.code}, message: ${error.message}`);
  }
  return null;
};

这种设计模式的核心优势在于统一收口、按类型分发——所有卡片发送的 CALL 请求都由同一个处理入口接收,通过 actionType 字段解耦,新增一种操作类型只需要在 CardActionHandler 中增加一个 case 分支,无需修改调用链路。

method 参数设计与可扩展性

在 CALL 请求的参数中,method 字段起到二级路由的作用。当前项目的 CALL 请求统一使用 method: 'cardAction',即所有卡片操作都走 callee.on('cardAction', ...) 这一条通道。

postCardAction(component, {
  action: FormCarAction.CALL,
  abilityName: ENTRY_ABILITY,
  params: {
    method: 'cardAction',
    actionType: CardActionType.PLAY_ACTION,
    playActionType: type,
    formId: formId,
  },
});

method 的设计具备良好的可扩展性:当未来需要增加完全不同类型的远程调用时(例如卡片请求应用执行数据同步、触发通知等),可以通过定义新的 method 值在 callee 上注册不同的处理路径,而 actionType 则在同一 method 下做精细化的业务分发。

来看各处理分支的具体逻辑:

播放控制(PLAY_ACTION):

private handlePlayAction(params: Record<string, string>): void {
  if (params.playActionType) {
    let playActionType: PlayActionType = params.playActionType as PlayActionType;
    MediaService.getInstance().initAndPlayByAction(playActionType);
  }
}

这里 playActionType 定义了四种播放操作:播放、暂停、上一首、下一首,每种操作都通过 MediaService 统一调度。

收藏操作(COLLECT_ACTION):

private handleCollectAction(params: Record<string, string>): void {
  if (params.collectActionType && this.context) {
    let songRdbHelper = SongRdbHelper.getInstance(this.context);
    if (params.collectActionType === CollectAction.COLLECTED) {
      songRdbHelper.updateCollected(params.songId, CollectAction.COLLECTED);
      FormUtils.updateCardCollectStatus(this.context, true);
      this.context.eventHub.emit('collected', params.songId, CollectAction.COLLECTED);
    } else {
      songRdbHelper.updateCollected(params.songId, CollectAction.UNCOLLECTED);
      FormUtils.updateCardCollectStatus(this.context, false);
      this.context.eventHub.emit('collected', params.songId, CollectAction.UNCOLLECTED);
    }
  }
}

收藏操作展示了 CALL 请求的典型模式:卡片发送一个操作指令——参数中携带操作类型和目标数据 ID——应用侧完成业务逻辑(数据库更新、卡片 UI 刷新、事件广播)——通过 formProvider.updateForm 回写卡片状态。

请求更新(REQUEST_UPDATE):

private handleRequestUpdate(params: Record<string, string>): void {
  if (params.formId && this.context) {
    FormUtils.updateMusicControlSingle(this.context, params.formId);
  }
}

卡片在 isNeedRequestUpdate 数据变更时,通过 CALL 请求让应用端主动推送最新数据。

运动操作(EXERCISE_ACTION):

private handleExerciseAction(params: Record<string, string>): void {
  if (params.exerciseAction && this.context) {
    switch (params.exerciseAction) {
      case ExerciseAction.START_EXERCISE:
        FormUtils.updateExerciseCardState(this.context, ExerciseState.IN_PROGRESS);
        break;
      case ExerciseAction.END_EXERCISE:
        FormUtils.updateExerciseCardState(this.context, ExerciseState.COMPLETED);
        break;
      case ExerciseAction.RESET_EXERCISE:
        FormUtils.resetExerciseCard(this.context);
        break;
    }
  }
}

ROUTER 类型:卡片跳转到应用落地页

命名路由 vs 页面路由的选择

ROUTER 类型的 postCardAction 用于从卡片直接跳转到应用内的某个页面(落地页)。在 HarmonyOS 中,页面路由主要有两种方式:

  • 页面路由(router.pushUrl):基于 pages 列表中的页面路径跳转。这种方式需要硬编码页面路径,耦合度高。
  • 命名路由(NavPathStack):基于 route_map.json 中定义的名称跳转。业务方只需要知道页面的逻辑名称,不需要关心具体的文件路径。

LiveCard 项目选择了命名路由方案,原因有三:

  1. 解耦卡片与页面实现:卡片只需要知道页面的逻辑名称(如 MusicPage),无需关心页面文件的具体路径
  2. 支持 Navigation 容器:与 HarmonyOS 推荐的 Navigation + NavPathStack 架构天然适配
  3. 集中管理路由映射:所有路由映射在 route_map.json 中统一维护,新增页面只需添加一条记录

route_map.json 的配置方式

首先在 module.json5 中声明路由映射配置文件的引用:

[d:\HarmonyOS\WorkSpace\LiveCard\entry\src\main\module.json5]

"routerMap": "$profile:route_map",

然后在 resources/base/profile/route_map.json 中定义具体的映射关系:

[d:\HarmonyOS\WorkSpace\LiveCard\entry\src\main\resources\base\profile\route_map.json]

{
  "routerMap": [
    {
      "name": "SleepReport",
      "pageSourceFile": "src/main/ets/view/sleep/SleepReportPageView.ets",
      "buildFunction": "SleepReportPageViewBuilder"
    },
    {
      "name": "CloverPage",
      "pageSourceFile": "src/main/ets/view/sleep/CloverPageView.ets",
      "buildFunction": "CloverPageViewBuilder"
    },
    {
      "name": "DeliveryPage",
      "pageSourceFile": "src/main/ets/view/delivery/DeliveryPageView.ets",
      "buildFunction": "DeliveryPageViewBuilder"
    },
    {
      "name": "ExercisePage",
      "pageSourceFile": "src/main/ets/view/exercise/ExercisePageView.ets",
      "buildFunction": "ExercisePageViewBuilder"
    },
    {
      "name": "MusicPage",
      "pageSourceFile": "src/main/ets/view/music/MusicPageView.ets",
      "buildFunction": "MusicPageViewBuilder"
    }
  ]
}

每条路由记录包含三个关键字段:

  • name:路由的逻辑名称,卡片侧使用此名称跳转
  • pageSourceFile:页面文件的源码路径
  • buildFunction:页面的 @Builder 函数名,用于 Navigation 容器渲染

卡片端发送 ROUTER 请求的封装:

public jumpAppPage(component: object, pageName: string): void {
  postCardAction(component, {
    action: FormCarAction.ROUTER,
    abilityName: ENTRY_ABILITY,
    params: {
      routeName: pageName,
    },
  });
}

EntryAbility 中通过 onNewWant 接收路由请求,由 RouterUtil 处理页面跳转:

[d:\HarmonyOS\WorkSpace\LiveCard\entry\src\main\ets\utils\RouterUtil.ets]

openPageByWant(want: Want): void {
  if (want.parameters && want.parameters.routeName) {
    let routeName = want.parameters.routeName as string;
    this.pushPage(routeName);
  }
}

pushPage(name: string): void {
  this.pathStack.replacePath({
    name: name,
    param: []
  });
}

使用 replacePath 而非 pushPath 的原因是:从卡片跳转到应用落地页通常不需要返回卡片主页,直接替换路径栈更符合用户体验。

实际使用示例——音乐卡片点击跳转到音乐详情页:

// 在 MusicCard.ets 中
RelativeContainer() {
  // ... 卡片 UI
}
.width('100%')
.height('100%')
.onClick(() => {
  ActionUtils.jumpAppPage(this, 'MusicPage');
});

运动卡片完成运动后跳转到运动报告页:

// 在 ExerciseCard.ets 中
case ExerciseState.COMPLETED: {
  ActionUtils.jumpAppPage(this, 'ExercisePage');
  postCardAction(this, {
    action: FormCarAction.CALL,
    abilityName: ENTRY_ABILITY,
    params: {
      method: 'cardAction',
      actionType: CardActionType.EXERCISE_ACTION,
      exerciseAction: ExerciseAction.RESET_EXERCISE
    }
  });
  return;
}

这里展示了三种类型协同工作的典型场景:同一个点击事件中,先通过 ROUTER 跳转到落地页,再通过 CALL 发送数据重置指令。


三种类型的选型指南

场景 推荐类型 目标组件 典型用途
激活互动卡片 MESSAGE FormExtensionAbility 请求卡片展开动画,传递动画参数
触发后台业务逻辑 CALL UIAbility (callee) 播放控制、收藏、数据库操作、状态更新
跳转到应用页面 ROUTER UIAbility (want) 点击卡片跳转到详情页或功能页
更新卡片 UI 状态 CALL + updateForm UIAbility → 卡片 通过 formProvider.updateForm 回写数据
传递动画上下文 MESSAGE + 文件持久化 FormExtensionAbility → LiveForm 传递 triggerAction 给互动卡片

选型原则:

  1. 和"展示"相关,选 ROUTER:凡是需要打开应用内的页面,都应该使用 ROUTER 类型,因为它会自动携带 Want 参数触发 onNewWant,配合命名路由实现页面跳转。

  2. 和"操作"相关,选 CALL:凡是需要在应用侧执行业务逻辑(播放、收藏、数据库写入)但不打开新页面的场景,都应该使用 CALL 类型。它通过 Callee 机制实现高效的进程间调用,且支持 method + actionType 两层分发。

  3. 和"卡片激活"相关,选 MESSAGE:凡是需要触发互动卡片从普通卡片态展开为互动态的,必须使用 MESSAGE 类型。只有 FormExtensionAbility.onFormEvent 才能调用 formProvider.requestOverflow 激活互动卡片。

  4. 避免混用:不要试图用 CALL 来实现页面跳转,或用 ROUTER 来传递业务操作指令。每种类型的通信路径不同,混用会导致代码难以维护。


通信安全与规范

abilityName 的显式声明

postCardAction 的每个调用中,都必须显式指定 abilityName 字段,指明消息要发送到哪个 Ability。项目中通过常量统一管理:

export const ENTRY_ABILITY = 'LiveCardAbility';
export const ENTRY_FORM_ABILITY = 'EntryFormAbility';

这两个常量对应 module.json5 中声明的 Ability 名称:

  • LiveCardAbility:主应用的 UIAbility,处理 CALL 和 ROUTER 请求
  • EntryFormAbility:卡片的 FormExtensionAbility,处理 MESSAGE 请求

显式声明 abilityName 的好处在于:

  • 避免歧义:当应用存在多个 Ability 时,确保消息到达正确的目标
  • 提升可读性:通过常量名就能看出通信方向
  • 便于维护:如果 Ability 名称变更,只需修改一处常量定义

参数校验与防御性编程

CardActionHandler 中采用了多层校验机制:

private cardActionCall = (data: rpc.MessageSequence): null => {
  try {
    let params: Record<string, string> = JSON.parse(data.readString());
    // ... 分发逻辑
  } catch (err) {
    let error = err as BusinessError;
    Logger.error(TAG, `cardActionCall err, code: ${error.code}, message: ${error.message}`);
  }
  return null;
};

每一层处理器也都做了空值检查:

private handlePlayAction(params: Record<string, string>): void {
  if (params.playActionType) {
    // ...
  }
}

private handleCollectAction(params: Record<string, string>): void {
  if (params.collectActionType && this.context) {
    // ...
  }
}

EntryFormAbility.onFormEvent 中同样对参数进行了合法性校验:

async onFormEvent(formId: string, message: string): Promise<void> {
  const params: Record<string, Object> = JSON.parse(message);
  // ... 处理逻辑
}

private async requestOverflow(...): Promise<void> {
  let formRect: formInfo.Rect = await formProvider.getFormRect(formId);
  if (formRect.width <= 0 || formRect.height <= 0) {
    return;  // 无效尺寸直接返回,避免异常崩溃
  }
  // ...
}

安全规范总结:

  1. 卡片的 abilityName 必须与 module.json5 声明一致,否则系统无法路由消息
  2. 所有参数在使用前必须做空值校验,特别是 formIdactionType 等关键字段
  3. 使用 try-catch 捕获 JSON 解析和异步调用异常,避免单点崩溃影响整个 Ability
  4. 敏感数据(如用户信息)不要通过 postCardAction 的 params 透传,卡片 params 可能被序列化到持久化数据中

总结

postCardAction 的三种 action 类型构成了 HarmonyOS 卡片与宿主应用通信的完整体系。MESSAGE 负责激活互动卡片这条"视觉链路",CALL 负责驱动后台业务这条"操作链路",ROUTER 负责打通卡片到应用页面的"导航链路"。三者各司其职、配合默契,设计者只要遵循"展示用 ROUTER、操作用 CALL、激活用 MESSAGE"的选型原则,就能构建出清晰、可维护的卡片通信架构。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐