HarmonyOS7 互动卡片postCardAction 三剑客:CALL / MESSAGE / ROUTER 精讲
postCardAction 三剑客:CALL / MESSAGE / ROUTER 精讲

postCardAction 概述
在 HarmonyOS 卡片开发中,postCardAction 是卡片与宿主应用之间通信的通用桥梁。无论是静态卡片还是互动卡片(Live Form),只要卡片需要向应用发送指令,都必须通过这个 API 完成。
postCardAction 接收两个参数:第一个是组件上下文(通常传入 this),第二个是一个包含 action、abilityName 和 params 的配置对象。其中 action 字段决定了指令的类型,支持三种取值:MESSAGE、CALL、ROUTER。
在 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);
}
}
这里有几个关键细节值得注意:
message参数是一个 JSON 字符串,由卡片侧postCardAction的params对象序列化而来EntryFormAbility通过解析message字段来区分不同的业务意图(requestOverflowvsupdateRequestOverflowState)- 卡片传递的额外参数(如
triggerAction、songId)可以通过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 中将 triggerAction 和 songId 写入文件,然后在 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());
这里的 callee 是 UIAbility 的内置能力,专门用于接收外部(包括卡片)的远程调用。卡片通过 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 项目选择了命名路由方案,原因有三:
- 解耦卡片与页面实现:卡片只需要知道页面的逻辑名称(如
MusicPage),无需关心页面文件的具体路径 - 支持 Navigation 容器:与 HarmonyOS 推荐的
Navigation+NavPathStack架构天然适配 - 集中管理路由映射:所有路由映射在
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 给互动卡片 |
选型原则:
-
和"展示"相关,选 ROUTER:凡是需要打开应用内的页面,都应该使用 ROUTER 类型,因为它会自动携带 Want 参数触发
onNewWant,配合命名路由实现页面跳转。 -
和"操作"相关,选 CALL:凡是需要在应用侧执行业务逻辑(播放、收藏、数据库写入)但不打开新页面的场景,都应该使用 CALL 类型。它通过 Callee 机制实现高效的进程间调用,且支持
method+actionType两层分发。 -
和"卡片激活"相关,选 MESSAGE:凡是需要触发互动卡片从普通卡片态展开为互动态的,必须使用 MESSAGE 类型。只有
FormExtensionAbility.onFormEvent才能调用formProvider.requestOverflow激活互动卡片。 -
避免混用:不要试图用 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; // 无效尺寸直接返回,避免异常崩溃
}
// ...
}
安全规范总结:
- 卡片的 abilityName 必须与 module.json5 声明一致,否则系统无法路由消息
- 所有参数在使用前必须做空值校验,特别是
formId、actionType等关键字段 - 使用 try-catch 捕获 JSON 解析和异步调用异常,避免单点崩溃影响整个 Ability
- 敏感数据(如用户信息)不要通过 postCardAction 的 params 透传,卡片 params 可能被序列化到持久化数据中
总结
postCardAction 的三种 action 类型构成了 HarmonyOS 卡片与宿主应用通信的完整体系。MESSAGE 负责激活互动卡片这条"视觉链路",CALL 负责驱动后台业务这条"操作链路",ROUTER 负责打通卡片到应用页面的"导航链路"。三者各司其职、配合默契,设计者只要遵循"展示用 ROUTER、操作用 CALL、激活用 MESSAGE"的选型原则,就能构建出清晰、可维护的卡片通信架构。
更多推荐



所有评论(0)