HarmonyOS 6.1 元服务实战:把电商卡片装进系统“负一屏”

HarmonyOS 6.1 元服务实战:把电商卡片装进系统“负一屏”

上篇做完安全加固,有读者问:“既然App都做完了,能不能像天气、日历那样,不用打开App就能看物流、加购物车?” 这正是HarmonyOS NEXT主打的元服务(Atomic Service)能力。今天我们把之前打磨好的电商Demo,“瘦身”成一个轻量级元服务卡片,实现“免安装、一键直达、跨设备流转”。实测卡片加载耗时从App启动的2s降至200ms,且能直接钉在系统负一屏。所有代码适配API23,含官方文档未提及的卡片数据刷新机制。


一、前言:为什么元服务是鸿蒙的“流量入口”?

之前的系列文章,我们构建了一个功能完备的电商App(环境搭建→UI开发→状态管理→分布式→端云→安全)。但App有个痛点:。用户为了下一个单,需要先下载几十MB的安装包,再等待几秒启动,流失率极高。

元服务(Atomic Service)是鸿蒙面向未来的轻量化程序形态,具备三大核心特征:

  1. 免安装:用户点击即达,像打开一个网页一样快,包体积限制在10MB以内。

  2. 一站式:以“万能卡片”的形式呈现在桌面、负一屏、搜索结果中,所见即所得。

  3. 跨设备:卡片可以在手机、平板、车机上无缝流转,逻辑与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个细节)

  1. 卡片环境限制:元服务卡片运行在独立的沙箱中,不能直接使用AppStoragePersistentStorage,也不能直接调用CloudUtil。所有数据必须通过FormExtensionAbility中转。

  2. UI组件限制:卡片中不能使用MarqueeVideoWeb等复杂组件,也不能使用if/else动态挂载组件(需用visibility控制)。这迫使你做极简设计。

  3. 点击事件穿透:卡片的onClick默认是跳转,按钮的onClick才是交互。如果按钮没反应,检查是否被上层组件遮挡。

  4. 数据刷新延迟updateForm是异步的,且系统会节流(短时间内多次调用只生效一次)。如果需要实时性,建议通过postCardAction触发callAbility调用App侧逻辑。

  5. 资源引用:卡片不能直接引用App模块的资源($r('app.media.xxx')),必须把图片等资源拷贝到卡片模块的resources目录下,或使用网络图片。

  6. 真机调试:远程模拟器不支持卡片“钉在桌面上”的手势操作,必须使用真机调试才能验证完整的用户交互路径。

  7. 包体积控制:元服务总包体积(含所有卡片)不能超过10MB。图片必须经过压缩,优先使用SVG矢量图。