上篇做完安全加固,有读者问:“既然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) |
|---|---|---|
分发形态 |
|
|
入口形态 | 桌面图标 | 万能卡片(桌面/负一屏/锁屏) |
启动耗时 | 1-3秒(冷启动) | <300ms(卡片直出) |
开发重点 | 全量业务逻辑 | 核心信息展示+快捷交互 |
适用场景 | 深度购物、复杂管理 | 物流跟踪、价格查询、一键加购 |
💡核心认知:元服务不是App的替代品,而是App能力的“前置”和“轻量化”。它负责“种草”和“快捷操作”,复杂的比价、支付、售后依然交给App。两者通过
deeplink(深层链接)无缝衔接。
三、代码实现:从App到元服务的“瘦身”手术
3.1 新建元服务卡片模块
在DevEco Studio中,右键点击项目 -> New -> Module -> Atomic Service Card。
Module Name:
GoodsCardCard Name:
ProductQuickViewCardSupport 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矢量图。