上篇做完安全加固,有读者问:"既然App都做完了,能不能像天气、日历那样,不用打开App就能看物流、加购物车?" 这正是HarmonyOS NEXT主打的元服务(Atomic Service) 能力。今天我们把之前打磨好的电商Demo,"瘦身"成一个轻量级元服务卡片,实现**"免安装、一键直达、跨设备流转"**。实测卡片加载耗时从App启动的2s降至200ms,且能直接钉在系统负一屏。所有代码适配API23,含官方文档未提及的卡片数据刷新机制。
一、前言:为什么元服务是鸿蒙的"流量入口"?
之前的系列文章,我们构建了一个功能完备的电商App(环境搭建→UI开发→状态管理→分布式→端云→安全)。但App有个痛点:重。用户为了下一个单,需要先下载几十MB的安装包,再等待几秒启动,流失率极高。
**元服务(Atomic Service)**是鸿蒙面向未来的轻量化程序形态,具备三大核心特征:
-
免安装:用户点击即达,像打开一个网页一样快,包体积限制在10MB以内。
-
一站式:以"万能卡片"的形式呈现在桌面、负一屏、搜索结果中,所见即所得。
-
跨设备:卡片可以在手机、平板、车机上无缝流转,逻辑与UI自动适配。
本文将基于之前的电商Demo,剥离出"商品详情卡片"和"购物车快捷入口"两个核心场景,改造成元服务,实现**"用户还没打开App,就已经完成了加购"**的极致体验。
二、核心概念辨析:App vs 元服务
很多新手会把元服务和Widget(小组件)搞混,先明确两者的本质区别:
| 维度 | 传统电商App | 元服务(Atomic Service) |
|---|---|---|
| 分发形态 | .hap安装包,需下载安装 |
.hsp/.har卡片包,免安装 |
| 入口形态 | 桌面图标 | 万能卡片(桌面/负一屏/锁屏) |
| 启动耗时 | 1-3秒(冷启动) | <300ms(卡片直出) |
| 开发重点 | 全量业务逻辑 | 核心信息展示+快捷交互 |
| 适用场景 | 深度购物、复杂管理 | 物流跟踪、价格查询、一键加购 |
💡 核心认知 :元服务不是App的替代品,而是App能力的"前置"和"轻量化" 。它负责"种草"和"快捷操作",复杂的比价、支付、售后依然交给App。两者通过
deeplink(深层链接)无缝衔接。
三、代码实现:从App到元服务的"瘦身"手术
3.1 新建元服务卡片模块
在DevEco Studio中,右键点击项目 -> New -> Module -> Atomic Service Card。
-
Module Name :
GoodsCard -
Card Name :
ProductQuickViewCard -
Support Dimensions : 选择
2 * 2(小卡片)和2 * 4(大卡片)。
3.2 改造卡片UI(复用现有组件)
元服务的卡片UI也是ArkTS编写,我们可以复用之前写好的GoodsItem的部分逻辑,但要做极简处理。
路径 :GoodsCard/src/main/ets/widget/pages/ProductQuickViewCard.ets
// 元服务卡片页面
@Entry
@Component
struct ProductQuickViewCard {
// 卡片数据(由FormExtensionAbility提供)
@State goodsTitle: string = 'HarmonyOS 6.1 定制款...';
@State goodsPrice: number = 5999;
@State stockCount: number = 99; // 新增库存信息
private formId: string = ''; // 卡片ID,用于更新数据
aboutToAppear() {
// 获取卡片ID,用于后续主动刷新
this.formId = this.getFormId();
// 初始加载云端数据(通过FormExtensionAbility)
this.loadCardData();
}
/**
* 加载卡片数据(调用之前封装的云请求工具)
* 注意:卡片环境不能直接用CloudUtil,需通过FormExtensionAbility桥接
*/
async loadCardData(): Promise<void> {
try {
// 1. 通过callAbility调用FormExtensionAbility获取数据
const action = 'action.system.event.DATA_UPDATE';
const params = {
formId: this.formId,
message: 'request_data'
};
await FeatureAbility.callAbility('FormExtensionAbility', action, params);
} catch (err) {
console.error('卡片加载数据失败:', err);
}
}
build() {
Column() {
// 商品标题(限制1行,超出省略)
Text(this.goodsTitle)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ bottom: 8 })
// 价格
Text(`¥${this.goodsPrice}`)
.fontSize(18)
.fontColor('#FF0000')
.fontWeight(FontWeight.Bold)
.margin({ bottom: 12 })
// 库存状态
Row() {
Text(this.stockCount > 0 ? `库存充足(${this.stockCount})` : '缺货')
.fontSize(12)
.fontColor(this.stockCount > 0 ? '#00AA00' : '#FF0000')
Blank()
// 一键加购按钮
Button('加购')
.fontSize(12)
.height(28)
.backgroundColor(this.stockCount > 0 ? '#0A59F7' : '#CCCCCC')
.enabled(this.stockCount > 0)
.onClick(() => this.addToCart())
}
.width('100%')
}
.padding(16)
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
.borderRadius(16)
.onClick(() => this.jumpToApp()) // 点击卡片跳转App
}
/**
* 一键加购逻辑(核心交互)
*/
async addToCart(): Promise<void> {
try {
// 调用之前写好的云函数,无需打开App
const result = await FeatureAbility.callAbility(
'com.example.shop.EntryAbility',
'action.business.ADD_TO_CART',
{ goodsId: 1, count: 1 }
);
if (result.code === 0) {
promptAction.showToast({ message: '已加入购物车' });
// 触发卡片数据刷新(库存-1)
this.stockCount--;
}
} catch (err) {
console.error('加购失败:', err);
}
}
/**
* 跳转回完整App(Deep Link)
*/
jumpToApp(): void {
const context = getContext(this) as AbilityContext;
context.startAbility({
bundleName: 'com.example.shop',
abilityName: 'EntryAbility',
parameters: { goodsId: 1 } // 携带参数,实现精准跳转
});
}
}
3.3 实现卡片数据刷新(FormExtensionAbility)
卡片是静态的,数据需要动态更新。这需要借助FormExtensionAbility。
路径 :GoodsCard/src/main/ets/entryformability/EntryFormAbility.ets
import { FormExtensionAbility, formBindingData } from '@kit.FormKit';
import { CloudUtil } from '../../../entry/src/main/ets/common/CloudUtil'; // 复用App的云工具
export default class EntryFormAbility extends FormExtensionAbility {
/**
* 卡片创建时调用
*/
onAddForm(want: Want): formBindingData.FormBindingData {
console.log('卡片创建:', want.parameters?.formId);
// 返回初始数据(可以是缓存数据)
const data = {
goodsTitle: '加载中...',
goodsPrice: 0,
stockCount: 0
};
return formBindingData.createFormBindingData(data);
}
/**
* 接收卡片更新请求(核心方法)
*/
onUpdateForm(formId: string, want: Want): void {
console.log('卡片更新请求:', formId);
// 1. 从云端获取最新数据(复用之前的CloudUtil)
CloudUtil.queryDatabase('goods', { id: 1 }).then((result) => {
if (result.length > 0) {
const goods = result[0];
const formData = {
goodsTitle: goods.title,
goodsPrice: goods.price,
stockCount: goods.stock
};
// 2. 推送数据到卡片
const formProvider = formBindingData.createFormBindingData(formData);
formProvider.updateForm(formId, formProvider).then(() => {
console.log('卡片数据更新成功');
});
}
}).catch(err => {
console.error('云端数据获取失败:', err);
});
}
/**
* 处理卡片事件(如按钮点击)
*/
onFormEvent(formId: string, message: string): void {
console.log('卡片事件:', formId, message);
if (message === 'request_data') {
this.onUpdateForm(formId, want);
}
}
}

四、踩坑记录(官方文档没写的7个细节)
-
卡片环境限制 :元服务卡片运行在独立的沙箱中,不能直接使用
AppStorage、PersistentStorage,也不能直接调用CloudUtil。所有数据必须通过FormExtensionAbility中转。 -
UI组件限制 :卡片中不能使用
Marquee、Video、Web等复杂组件,也不能使用if/else动态挂载组件(需用visibility控制)。这迫使你做极简设计。 -
点击事件穿透 :卡片的
onClick默认是跳转 ,按钮的onClick才是交互。如果按钮没反应,检查是否被上层组件遮挡。 -
数据刷新延迟 :
updateForm是异步的,且系统会节流(短时间内多次调用只生效一次)。如果需要实时性,建议通过postCardAction触发callAbility调用App侧逻辑。 -
资源引用 :卡片不能直接引用App模块的资源(
$r('app.media.xxx')),必须把图片等资源拷贝到卡片模块的resources目录下,或使用网络图片。 -
真机调试:远程模拟器不支持卡片"钉在桌面上"的手势操作,必须使用真机调试才能验证完整的用户交互路径。
-
包体积控制:元服务总包体积(含所有卡片)不能超过10MB。图片必须经过压缩,优先使用SVG矢量图。