本章围绕 HarmonyOS 开发中的元服务与服务卡片相关技术展开。在元服务方面,详细介绍了元服务从创建到上架的整个流程,包括元服务的技术架构和生态能力。同时阐述了应用云开发等能力。此外,还涉及如何使用云函数开发元服务等。在服务卡片方面,介绍了如何在项目中创建服务卡片以及服务卡片相关运行机制及模块。最后通过实现智算房贷元服务和计算器卡片两个案例,综合运用上述知识,展示了实际开发中的应用,帮助开发者更好地理解和掌握相关技术。
15.1 元服务介绍
在万物互联时代,人均设备持有量持续增长,设备种类与使用场景日益丰富,应用开发与应用入口变得愈发复杂。应用提供商和用户急需全新的服务模式,以简化应用开发,让听音乐、打车等服务的获取与使用更加便捷。基于此,HarmonyOS 不仅支持传统的安装式应用,还推出了更为便捷的免安装应用——元服务。
15.1.1 元服务是什么
元服务是HarmonyOS提供的轻量应用程序形态,基于 HarmonyOS SDK(仅可使用 “元服务 API 集”)开发,支持在 1+8+N 设备上运行,让用户能在合适场景与设备上便捷使用。它具有诸多显著特性:秒开直达,纯净清爽,无需漫长等待;服务相伴,恰合时宜,精准满足当下需求;即用即走,账号相随,使用体验无缝衔接;一体两面,嵌入运行,灵活适配不同场景;原生智能,全域搜索,快速定位所需服务;高效开发,生而可信,保障开发效率与服务质量。元服务可独立上架、分发、运行,独立实现业务闭环,大幅提升了信息与服务的获取效率,为用户带来更加高效、便捷的服务体验。
15.1.2 元服务与应用、卡片之间的基本关系
- 元服务
是系统提供的一种面向未来的服务提供方式,是有独立入口的(用户可通过点击方式直接触发)、免安装的(无需显式安装,由系统程序框架后台安装后即可使用)、可为用户提供一个或多个便捷服务的用户应用程序形态。
- 服务卡片
是一种界面展示形式,可以将应用的重要信息或操作前置到卡片,以达到服务直达、减少体验层级的目的。卡片常用于嵌入到其他应用(当前卡片使用方只支持系统应用,如桌面)中作为其界面显示的一部分,并支持拉起页面、发送消息等基础的交互功能。
15.1.3 从消费者、生态伙伴与开发者视角来看元服务
- 消费者视角
元服务为日常生活带来极大便利。秒开直达、纯净清爽的特性,告别繁琐安装与漫长等待,无弹窗干扰,使用体验流畅又纯粹。服务相伴恰合时宜,服务面板随时跟随,精准提醒,服务履约信息有保障。用完即走且账号相随,资产安全同步,毫无后顾之忧。
- 生态伙伴视角
元服务提供了新的业务拓展途径。一体两面、嵌入运行的特点,使其既能独立运营,又能与应用协同,助力私域运营。多个分发入口能更高效触达用户,服务通知和状态提醒有助于构建完整的服务闭环,提升用户粘性与忠诚度,增强在鸿蒙生态中的竞争力。
- 开发者视角
元服务提供了丰富的开发资源和规范的生态规则。标准 UX 组件集、场景化模板及 API 集,大大提高开发效率。遵循生态规则开发,实现生而可信,降低开发风险,让开发者更专注于创新和服务优化,在鸿蒙生态中实现自身价值。
15.1.4 元服务的能力特征
- 便捷高效
具备秒开直达特性,能即点即用,秒开启动,流畅运行,且无弹窗干扰,给予纯净体验。还有多个分发入口,触达用户更高效。在使用场景上,可将常用服务卡片添加到桌面或通过负一屏发现,无需安装就能使用,实现快捷服务。
- 智能贴心
服务相伴恰合时宜,服务面板常伴跟随,服务通知和状态提醒精准,提供便捷高效服务闭环。同时基于鸿蒙OS底座实现原生智能,能精准触达服务,在全域搜索中让用户便捷获取任何服务。
- 安全同步
用完即走无二次弹窗,用户资产跟随账号多设备安全同步,随时可找回元服务。
- 灵活轻量
和应用是鸿蒙生态下不同程序形态,免安装更轻量,两者可独立部署也能嵌入运行,助力商户私域运营。
- 开发规范
为开发者提供标准UX组件集、场景化模板及API集 ,并构建生态规则,保障开发者高效开发,实现生而可信。
15.2 元服务技术架构及生态能力
元服务基于HarmonyOS SDK开发,运行在HarmonyOS架构的应用层,依靠内核、驱动、框架等系统基础能力为用户服务。HarmonyOS架构自下而上分别是负责硬件资源管理的内核层、提供系统级服务能力的系统服务层、为应用开发提供API和框架(元服务依赖 “元服务 API 集” 开发)的框架层,以及包含元服务和传统应用的应用层。在元服务架构设计中,产品定制层针对不同设备形态提供个性化业务,依赖业务模块层和公共基础层;业务模块层可被产品定制层依赖,自身向下依赖公共基础层,封装通用业务逻辑提升代码复用性;公共能力层作为基础能力集,仅被产品层和业务模块层依赖,为上层提供统一的底层支持,如网络请求、数据存储等功能。
15.2.1 元服务在HarmonyOS架构中的位置
元服务基于HarmonyOS SDK开发,运行于 HarmonyOS的应用层。它依托系统的基础能力,如内核、驱动、框架等,为用户提供便捷的服务。在 HarmonyOS架构中,最底层是内核层,负责硬件资源管理等基础功能;往上是系统服务层,提供系统级的服务与能力;再往上是框架层,为应用开发提供各种 API 和开发框架,元服务依赖框架层中的 “元服务API集” 进行开发;最上层的应用层,包含了元服务和传统应用等各种应用程序。
15.2.2 元服务架构设计
- 产品定制层 Products
针对不同设备形态提供个性化业务,涵盖 UI、资源和相关配置。各产品间不能直接依赖,需向下依赖业务模块层和公共基础层,以此保证不同设备上的元服务能适配各自的硬件特性与用户交互习惯。
- 业务模块层 Features
可被产品定制层不同设备形态的 HAP 依赖,但不能反向依赖产品层,可向下依赖公共基础层。该层将通用业务逻辑进行封装,提升代码复用性,比如订单处理、用户信息管理等模块,在不同设备形态的元服务中都能调用。
- 公共能力层 Commons
作为基础能力集,各个业务模块层包含的公共业务都可下沉至此。此层仅能被产品层和业务模块层依赖,不可反向依赖,例如网络请求、数据存储等基础功能,为上层提供统一的底层支持
15.3 元服务开发
元服务开发主要环节如下:开发前,需注册华为开发者帐号并创建元服务,搭建开发环境,用DevEco Studio创建工程,且元服务包名有特定格式,要先获取AppID再创建工程,AppID可在AppGallery Connect 查询;开发中,元服务含页面、卡片、图标三部分,分别参考相应开发方式,DevEco Studio提供图片生成工具和真机调试能力。
15.3.1 创建元服务工程
首次或已打开DevEco Studio时按相应方式创建元服务新工程,选Atomic Service开发及模板。登录或访客模式开发,访客模式运行需补包名。授权访问账号,选APP ID,未注册可新注册。完成注册后刷新选ID,配置工程填 Project name。创建完成后介绍元服务工程目录结构,含各文件及目录的作用和用途。
- 创建元服务项目
若首次打开DevEco Studio,请选择Create Project开始创建一个新工程。如果已经打开了一个工程,请在菜单栏选择File > New > Create Project来创建一个新工程。选择Atomic Service元服务开发,选择模板,单击Next进行下一步配置。
- 点击Sign In登录后 点击Register APP ID注册新的APP ID
- 点击创建APP ID 按照步骤进行创建
- 点击下一步填写项目基本信息完成创建
- 结构说明
-
- AppScope > app.json5:元服务的全局配置信息。
- entry:HarmonyOS工程模块,编译构建生成一个HAP。
-
-
- src > main > ets:用于存放ArkTS源码。
- src > main > ets > entryability:元服务的入口。
- src > main > ets > pages:元服务包含的页面。
- src > main > resources:用于存放元服务所用到的资源文件,如图形、多媒体、字符串、布局文件等。关于资源文件。
- src > main > module.json5:模块配置文件。主要包含HAP的配置信息、元服务在具体设备上的配置信息以及元服务的全局配置信息。
- build-profile.json5:当前的模块信息 、编译信息配置项,包括buildOption、targets配置等。
- hvigorfile.ts:模块级编译构建任务脚本,开发者可以自定义相关任务和代码实现。
-
-
- oh_modules:用于存放三方库依赖信息。
- build-profile.json5:元服务级配置信息,包括签名signingConfigs、产品配置products等。
- hvigorfile.ts:元服务级编译构建任务脚本
15.3.2 生成元服务图标
DevEco Studio的Image Asset功能可生成统一元服务图标样式。选中模块或文件右键操作进入配置页面,选择本地符合格式、尺寸等要求的图片,配置颜色、名称等,点击 OK 生成图标并可配置在module.json5 中。
1.打开生成图标操作页
在工程中选中模块或文件,右键单击New > Image Asset,进入图标配置页面
- 图片要求
- 图标格式:.png、.jpeg、.jpg格式的静态图片资源。
- 图标尺寸:1024 x 1024 px(正方形)。
- 图标背景:不透明。
- 质量要求:图标内容需清晰可辨,避免存在模糊、锯齿、拉伸等问题。详见元服务图标设计规范。
15.3.3 构建元服务页面
元服务页面写法和ArkUI一致。我们创建一个简单的页面。
@Entry
@Component
struct APage {
@State message: string = '页面A';
build() {
Row() {
Column() {
Text("页面A")
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
15.3.4 使用真机运行元服务
元服务真机运行首先需要进行生成各种签名,生成签名的方式有两种:自动和手动,这里我们使用手动的方式进行签名,方便后续云服务的开发。
15.3.4.1 准备签名文件
HarmonyOS应用/元服务通过数字证书(.cer文件)和Profile文件(.p7b文件)来保证应用/元服务的完整性。在申请数字证书和Profile文件前,首先需要通过DevEco Studio来生成密钥(存储在格式为.p12的密钥库文件中)和证书请求文件(.csr文件)。
签名文件相关名字概念
- 密钥:包含非对称加密中使用的公钥和私钥,存储在密钥库文件中,格式为.p12,公钥和私钥对用于数字签名和验证。
- 证书请求文件:格式为.csr,全称为Certificate Signing Request,包含密钥对中的公钥和公共名称、组织名称、组织单位等信息,用于向AppGallery Connect申请数字证书。
- 数字证书:格式为.cer,由华为AppGallery Connect颁发。
- Profile文件:格式为.p7b,包含HarmonyOS应用/元服务的包名、数字证书信息、描述应用/元服务允许申请的证书权限列表,以及允许应用/元服务调试的设备列表(如果应用/元服务类型为Release类型,则设备列表为空)等内容,每个应用/元服务包中均必须包含一个Profile文件。
15.3.4.2 生成密钥和证书请求文件
在主菜单栏单击Build > Generate Key and CSR。点击New创建。
在Create Key Store窗口中,填写密钥库信息后,单击OK。
- Key Store File:设置密钥库文件存储路径,并填写p12文件名。
- Password:设置密钥库密码,必须由大写字母、小写字母、数字和特殊符号中的两种以上字符的组合,长度至少为8位。请记住该密码,后续签名配置需要使用。
- Confirm Password:再次输入密钥库密码。
15.3.4.3 设置相关秘钥
在Generate Key and CSR界面,设置CSR文件存储路径和CSR文件名
单击OK按钮,创建CSR文件成功,可以在存储路径下获取生成的密钥库文件(.p12)和证书请求文件(.csr)。
15.3.4.4 申请调试证书和Profile文件
通过生成的证书请求文件,向AppGallery Connect申请发布证书和Profile文件,操作如下。
- 创建HarmonyOS应用/元服务:在AppGallery Connect项目中,创建一个HarmonyOS应用/元服务,用于调试证书和Profile文件申请。
- 申请发布证书和Profile文件:在AppGallery Connect中申请、下载发布证书和Profile文件。
- 用于发布的证书和Profile文件申请完成后,请在DevEco Studio中进行签名。
- 点击ok完成调试
15.3.4.5 连接手机运行项目
在编辑窗口右上角的工具栏,单击按钮运行。我们的元服务就会自动在手机上运行。
15.4 开发基于ArkTS UI 的卡片(Form Kit)
Form Kit是在桌面、锁屏等系统入口嵌入显示应用信息的开发框架和API,能将应用重要信息或操作抽取到卡片实现信息展示与服务直达。使用场景上支持手机、平板等设备,应用和元服务均可开发,可在桌面、锁屏添加,不能在普通应用内嵌入,涉及使用方、提供方和管理服务等概念,常见步骤为长按图标选卡片再添加到桌面。开发模式支持Stage 和 FA,推荐 Stage,Stage可基于 ArkTS 或 JS 开发,FA 仅支持 JS,ArkTS 卡片在自定义动效等方面更优,其依赖 Ability Kit、可使用 ArkUI 部分能力,且卡片在 UI 和运动能力上有一定约束限制。
15.4.1 ArkTS 卡片运行机制和相关模块
- 卡片使用方
显示卡片内容的宿主应用,控制卡片在宿主中展示的位置,当前仅系统应用可以作为卡片使用方。
- 卡片提供方
提供卡片显示内容的应用,控制卡片的显示内容、控件布局以及控件点击事件。
- 卡片管理服务
用于管理系统中所添加卡片的常驻代理服务,提供formProvider的接口能力,同时提供卡片对象的管理与使用以及卡片周期性刷新等能力。
- 卡片渲染服务
用于管理卡片渲染实例,渲染实例与卡片使用方上的卡片组件一一绑定。卡片渲染服务运行卡片页面代码widgets.abc进行渲染,并将渲染后的数据发送至卡片使用方对应的卡片组件。
15.4.2 ArkTS 卡片开发
创建ArkTS卡片,首先需配置卡片的配置文件,对其生命周期进行管理,确保卡片正常运行。接着开发卡片页面,设计出符合需求的展示界面。然后开发卡片事件,实现交互功能。最后进行卡片数据交互,保障卡片与其他部分的数据流通和处理,通过这些步骤完成ArkTS卡片的创建,实现其展示、交互及数据处理等功能。
15.4.2.1 创建一个ArkTS卡片
创建ArkTS卡片有两种入口,创建工程选 Application 或 Atomic Service(元服务),创建后均可右键新建。已有的应用工程也能右键新建,API 10 及以上 Stage 模型工程在Service Widget菜单可直接选创建动态或静态卡片,也能通过配置文件修改类型。选卡片模板和ArkTS语言类型,单击“Finish”完成创建,创建后工程新增卡片相关文件。
步骤一:在对应目录下新建
在entry目录下右键选择新建 => Service Widget => Static Widget
步骤二:根据实际业务场景选择一个模版
步骤三: 在选择卡片的开发语言类型(Language)时,选择ArkTS选项,然后单击“Finish”,即可完成ArkTS卡片创建
建议根据实际使用场景命名卡片名称。ArkTS卡片创建完成后,工程中会新增如下卡片相关文件:卡片生命周期管理文件(EntryFormAbility.ets)、卡片页面文件(WidgetCard.ets)和卡片配置文件(form_config.json)。
15.4.2.2 配置卡片的配置文件
卡片需要在module.json5配置文件中的extensionAbilities标签下,配置FormExtensionAbility相关信息。FormExtensionAbility需要填写metadata元信息标签,其中键名称为固定字符串“ohos.extension.form”,资源为卡片的具体配置信息的索引。
{
"module": {
// ...
"extensionAbilities": [
{
"name": "EntryFormAbility",
"srcEntry": "./ets/entryformability/EntryFormAbility.ets",
"label": "$string:EntryFormAbility_label",
"description": "$string:EntryFormAbility_desc",
"type": "form",
"metadata": [
{
"name": "ohos.extension.form",
"resource": "$profile:form_config"
}
]
}
]
}
}
卡片的具体配置信息。在上述FormExtensionAbility的元信息(“metadata”配置项)中,可以指定卡片具体配置信息的资源索引。例如当resource指定为$profile:form_config时,会使用开发视图的resources/base/profile/目录下的form_config.json作为卡片profile配置文件。
- isDynamic标签
此标签标识卡片是否为动态卡片(仅针对ArkTS卡片生效)。
| 卡片类型 | 支持的能力 | 适用场景 | 优缺点 |
|---|---|---|---|
| 静态卡片 | 仅支持UI组件和布局能力。 | 主要用于展示静态信息(UI相对固定),仅可以通过FormLink组件跳转到指定的UIAbility。 | 功能简单但可以有效控制内存开销。 |
| 动态卡片 | 除了支持UI组件和布局能力,还支持通用事件能力和自定义动效能力。 | 用于有复杂业务逻辑和交互的场景。 | 功能丰富但内存开销较大。 |
- window标签
| 属性名称 | 含义 | 数据类型 | 是否可缺省 |
|---|---|---|---|
| designWidth | 标识页面设计基准宽度。以此为基准,根据实际设备宽度来缩放元素大小。 | 数值 | 可缺省,缺省值为720px。 |
| autoDesignWidth | 标识页面设计基准宽度是否自动计算。当配置为true时,designWidth将会被忽略,设计基准宽度由设备宽度与屏幕密度计算得出。 | 布尔值 | 可缺省,缺省值为false。 |
- 配置示例如下
{
"forms": [
{
"name": "widget",
"displayName": "$string:widget_display_name",
"description": "$string:widget_desc",
"src": "./ets/widget/pages/WidgetCard.ets",
"uiSyntax": "arkts",
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"colorMode": "auto",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "10:30",
"updateDuration": 1,
"defaultDimension": "2*2",
"supportDimensions": [
"2*2"
],
"formConfigAbility": "ability://EntryAbility",
"dataProxyEnabled": false,
"isDynamic": true,
"transparencyEnabled": false,
"metadata": []
}
]
}
15.4.2.3 卡片生命周期管理
创建ArkTS卡片,需实现FormExtensionAbility生命周期接口。
步骤一: 在EntryFormAbility.ets中,导入相关模块。
import { formBindingData, FormExtensionAbility,
formInfo, formProvider } from '@kit.FormKit';
import { Configuration, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
步骤二:生命周期使用示例
在EntryFormAbility.ets中,实现FormExtensionAbility生命周期接口,其中在onAddForm的入参want中可以通过FormParam取出卡片的相关信息。
const TAG: string = 'EntryFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;
export default class EntryFormAbility extends FormExtensionAbility {
onAddForm(want: Want): formBindingData.FormBindingData {
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onAddForm');
hilog.info(DOMAIN_NUMBER, TAG,
want.parameters?.[formInfo.FormParam.NAME_KEY] as string);
// ...
// 卡片使用方创建卡片时触发,提供方需要返回卡片数据绑定类
let obj: Record<string, string> = {
'title': 'titleOnAddForm',
'detail': 'detailOnAddForm'
};
let formData: formBindingData.FormBindingData =
formBindingData.createFormBindingData(obj);
return formData;
}
onCastToNormalForm(formId: string): void {
// 卡片使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理。
// 1、临时卡、常态卡是卡片使用方的概念。
// 2、临时卡是短期存在的,在特定事件或用户行为后显示,完成后自动消失。
// 3、常态卡是持久存在的,在用户未进行清除或更改的情况下会一直存在,
// 平时开发的功能卡片属于常态卡。
// 4、目前手机上没有地方会使用临时卡。
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onCastToNormalForm');
}
onUpdateForm(formId: string): void {
// 若卡片支持定时更新/定点更新/卡
// 片使用方主动请求更新功能,则提供方需要重写该方法以支持数据更新
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onUpdateForm');
let obj: Record<string, string> = {
'title': 'titleOnUpdateForm',
'detail': 'detailOnUpdateForm'
};
let formData: formBindingData.FormBindingData =
formBindingData.createFormBindingData(obj);
formProvider.updateForm(formId, formData).catch((error: BusinessError) => {
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] updateForm, error:'
+ JSON.stringify(error));
});
}
onChangeFormVisibility(newStatus: Record<string, number>): void {
// 卡片使用方发起可见或者不可见通知触发,提供方需要做相应的处理,仅系统应用生效
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onChangeFormVisibility');
}
onFormEvent(formId: string, message: string): void {
// 若卡片支持触发事件,则需要重写该方法并实现对事件的触发
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onFormEvent');
// ...
}
onRemoveForm(formId: string): void {
// 删除卡片实例数据
hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onRemoveForm');
// 删除之前持久化的卡片实例数据
// 此接口请根据实际情况实现,具体请参考:FormExtAbility Stage模型卡片实例
}
onConfigurationUpdate(config: Configuration) {
// 当前formExtensionAbility存活时更新系统配置信息时触发的回调。
// 需注意:formExtensionAbility创建后10秒内无操作将会被清理。
hilog.info(DOMAIN_NUMBER, TAG,
'[EntryFormAbility] onConfigurationUpdate:'
+ JSON.stringify(config));
}
onAcquireFormState(want: Want) {
// 卡片提供方接收查询卡片状态通知接口,默认返回卡片初始状态。
return formInfo.FormState.READY;
}
}
15.5 云开发(Cloud Foundation Kit)
Cloud Foundation Kit(云开发服务)能按需为应用提供云函数、云数据库、云存储等云端服务,应用运行所需的服务器和环境都由云端平台提供,开发者只需关注业务逻辑,无需操心服务器、操作系统、容器等基础设施。在 DevEco Studio中,其提供端云一体化开发体验,基于统一技术栈,可高效协同完成端、云代码的编写、调试、编译和部署,大幅提升构建HarmonyOS应用和元服务的效率。它具备诸多显著优势:低运维成本,无需构建和管理云端资源,计算、弹性收缩、存储等能力一应俱全;能弹性伸缩、按量计费,根据实际请求量灵活调整,避免为空闲资源付费,提升资源利用率并降低成本;安全可靠,支持数据全密态加密、APP、用户和服务三重认证,还有基于角色的权限管理机制,全方位保障数据安全;端云一体化开发,支持在一套IDE中基于统一技术栈进行端、云代码协同开发,助力前端开发人员轻松转型为全栈工程师。
15.5.1 开发准备
在我们APPGallery Connect平台上进行开通,我们可以直接使用之前元服务创建的项目进行开发演示。
15.5.1.1 开通云函数
首次使用云函数服务前,需要先开通此服务。如果您已经启用,可跳过本步骤。本实例基于元服务项目进行开通云函数,并并在元服务中进行云函数的调用。
步骤一:登录AppGallery Connect,点击“我的项目”。
步骤二:在项目列表中点击需要开通云函数的项目。
步骤三:在左侧导航栏选择“云开发(Serverless)> 云函数”,进入云函数页面,点击“立即开通”。
15.5.1.2 开通云数据库服务
步骤一:登录AppGallery Connect,点击“我的项目”。
步骤二:在项目列表中点击需要开通云数据库的项目。
步骤三:在左侧导航栏选择“云开发(Serverless)> 云数据库”,进入云数据库页面,点击“立即开通”。
15.5.2 云函数
开通云函数服务后,您首先需要在AGC中创建函数,并添加函数执行的代码。
15.5.2.1 创建函数
步骤一:登录AppGallery Connect,点击“我的项目”
- 在项目列表中点击需要创建云函数的项目。
- 在左侧导航栏选择“云开发(Serverless) > 云函数”,进入云函数主界面。
- 选择“函数”页签,点击“创建函数”
步骤二:配置触发器
页面右侧抽屉式滑出“创建函数”窗口,按照“函数配置 -> 触发器 -> 函数代码 -> 层配置”引导顺序配置函数。
步骤三:函数代码
步骤四:点击创建完成云函数创建
15.5.22 项目中调用云函数
步骤一:在“entry/src/main/module.json5”文件中添加网络权限
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
步骤二: 在函数的触发器页面点击“HTTP触发器”
查看“触发URL”的后缀,获取触发器的标识,格式为“函数名-版本号”。
步骤三:在代码中集成,实现最简单的云函数调用
import { cloudFunction } from '@kit.CloudFoundationKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct APage {
@State message: string = '页面A';
build() {
Row() {
Column() {
Text("页面A")
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
cloudFunction.call({
name: 'test4',
version: '$latest', // 如果不传入版本号,默认为“$latest”。
timeout: 10 * 1000, // 单位为毫秒,默认为70*1000毫秒。
data: {
param1: 'val1',
param2: 'val2'
}
}).then((value: cloudFunction.FunctionResult) => {
console.info(`Succeeded in calling the function, result:
${JSON.stringify(value.result)}`);
}).catch((err: BusinessError) => {
console.error(`Failed to call the function,
Code: ${err.code}, message: ${err.message}`);
})
})
}
.width('100%')
}
.height('100%')
}
}
15.6 发布元服务
上传元服务时需提供完善信息,包括名称、图标、介绍等,同时应确保信息符合实际功能,及时更新。并对元服务名称、图标、内容分级、分类及标签、语言、信息内容等方面提出具体规范和要求,如名称长度、禁用词汇、图标限制、不得误导等,还提及需提供可用账号信息以便审核 。
15.6.1 打包发布版本
元服务发布需遵循一定流程。首先要了解元服务审核指南的具体要求,并认真完成发布前的自检工作,确保元服务符合相关规范。接着准备好签名文件,进行编译构建元服务的操作。发布前还有一个可选环节,即发布邀请测试版本,邀请部分用户体验并收集反馈,提前改进问题,提升全网版本质量和用户体验。完成上述步骤后,进行元服务备案,履行必要的手续。最后,在各项准备工作就绪且符合要求的情况下,顺利发布元服务,使其面向用户提供服务。
15.6.1.2 申请发布证书和Profile文件
通过生成的证书请求文件,向AppGallery Connect申请发布证书和Profile文件,操作如下。
- 创建HarmonyOS应用/元服务:在AppGallery Connect项目中,创建一个HarmonyOS应用/元服务,用于发布证书和Profile文件申请。
- 申请发布证书和Profile文件:在AppGallery Connect中申请、下载发布证书和Profile文件。
- 用于发布的证书和Profile文件申请完成后,请在DevEco Studio中进行签名。
15.5.1.5 编译构建.app文件
打包APP时,DevEco Studio会将工程目录下的所有HAP/HSP模块打包到APP中,因此,如果工程目录中存在不需要打包到APP的HAP/HSP模块,请手动删除后再进行编译构建生成APP。
单击Build > Build Hap(s)/APP(s) > Build APP(s),等待编译构建完成已签名的应用包。
15.5.1.6 上架
将HarmonyOS应用/元服务打包成.app文件后上架到AppGallery Connect。
15.6.2 元服务备案
根据《工业和信息化部关于开展移动互联网应用程序备案工作的通知》,自2023年9月初起,在中国大陆地区提供互联网信息服务的APP开发者,需要依法履行APP备案手续,并通过APP分发平台的备案信息核验。点击一图读懂APP备案,可了解APP备案的流程与要求。
凡在AGC新上架或升级的元服务,且服务器也位于中国大陆,则必须先通过元服务的网络服务接入商完成APP备案,然后在元服务上架时提供备案信息用于核验,核验通过后才允许上架。您可先前往ICP/IP地址/域名信息备案管理系统查询当前元服务是否已备案;如尚未备案,请联系对应的网络服务接入商完成备案。关于如何选择接入商,需要根据您所选择的服务器提供商而定。目前常见的接入商有:华为云、阿里云、腾讯云。相关备案流程可参考第三方云平台备案流程。
15.6.3 发布元服务
根据表单提示填写完成后即可完成上架
15.7 案例实战
本节运用前面学过的知识,基于元服务和卡片分别实现两个案例
15.7.1 智算房贷元服务开发
本案例使用元服务创建了一个智能房贷计算器项目,项目功能包括商业贷款、公积金贷款、组合贷款三种计算方式。
15.7.1.1 案例效果截图
15.7.1.2 案例运用到的知识点
- 核心知识点
- 元服务开发流程
- 元服务调试
- 工具类元服务开发
- 其他知识点
- ArkTS 语言基础
- V2版状态管理:@ComponentV2/@Local/等
- Stage模型
- 自定义组件和组件生命周期
- @Builder装饰器:自定义构建函数
- if/else:条件渲染
- 日志管理类的编写
- MVVM模式
15.7.1.3 代码结构解读
├──entry/src/main/ets // 代码区
│ ├──components // 组件
│ │ └──DialogComponent.ets // 贷款年限的弹框组件
│ │ └──MonthlyPaymentComponent.ets // 贷款详情展示每月支付的列表组件
│ ├──entryability
│ │ └──EntryAbility.ets
│ ├──interface.ts // 接口类型文件
│ ├──pages
│ │ └──MortgageLoan.ets // 计算器首页
│ ├──utils.ts // 计算通用方法页
│ └──views
│ └──CalculationDetails.ets // 计算详情页
└──entry/src/main/resources // 应用资源目录
15.7.1.4 房贷计算器首页表搭建
- 基于Navigation进行页面路由搭建。
- 根据UI抽离出 formRadioItemBuilder 和 formInputItemBuilder 实现UI复用。
- 基于openCustomDialog实现更灵活的Dialog组件。
- 监听表单变化将页面表单数据存到mortgageLoanForm里。
- 点击计算跳转到详情页并通过NavPathStack 把表单数据传到详情页方便计算
15.7.1.5 房贷计算核心逻辑实现
- 等额本息计算函数
功能:根据贷款金额、月利率和总月数,计算等额本息还款方式下每月的还款信息。
步骤:
-
- 运用等额本息公式计算每月还款金额。
- 借助循环逐月计算利息、本金偿还额和剩余本金。
- 把每月的还款信息存储到 paymentInfoList 数组中并返回。
import { DialogBuilder } from '../components/DialogComponent'
import { InterestRateType, LoanType, loanTypes, MortgageLoanForm, RadioOption, rates } from '../interface'
import { ComponentContent } from '@kit.ArkUI'
import { CalculationDetails } from '../views/CalculationDetails'
@Entry
@ComponentV2
struct MortgageLoan {
@Local mortgageYear: number = 1
@Provider('pageInfo') pageInfo: NavPathStack = new NavPathStack();
@Local mortgageLoanForm: MortgageLoanForm = {
loanAmount: 0,
loanYears: 1,
interestRateType: 'LPR',
calculationMethod: 'EqualPrincipalAndInterest',
loanType: 'Commercial'
}
@Builder
private formRadioItemBuilder(title: string,
options: RadioOption[],
groupName: string,
cb?: (val: string) => void) {
Flex({
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.SpaceBetween
}) {
Text(title)
.fontSize(16)
Row({ space: 10 }) {
ForEach(options, (item: RadioOption) => {
Row({ space: 2 }) {
Text(item.label)
Radio({ value: item.value, group: groupName })
.radioStyle({
checkedBackgroundColor: '#d99756'
})
.height(30)
.width(30)
.onChange((val) => {
cb && cb(item.value)
})
}
})
}
.justifyContent(FlexAlign.End)
.layoutWeight(1)
}
}
@Builder
private formInputItemBuilder(title: string,
unit: string,
cb?: (val: string) => void) {
Flex({
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.SpaceBetween
}) {
Text(title)
.fontSize(16)
Flex({ alignItems: ItemAlign.Center }) {
TextInput()
.layoutWeight(1)
.backgroundColor(Color.Transparent)
.borderRadius(0)
.type(InputType.Number)
.textAlign(TextAlign.End)
.onChange((val) => {
cb && cb(val)
})
Text(unit)
}
.layoutWeight(1)
}
}
private ctx: UIContext = this.getUIContext();
private contentNode: ComponentContent<Object> =
new ComponentContent(this.ctx,
wrapBuilder(DialogBuilder),
this.selectMortgageYear.bind(this));
selectMortgageYear(index: number) {
this.mortgageLoanForm.loanYears = index + 1
this.ctx.getPromptAction().closeCustomDialog(this.contentNode)
this.mortgageLoanForm = JSON.parse(JSON.stringify(this.mortgageLoanForm))
}
@Builder
titleBuilder() {
Flex({
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.Center
}) {
Text('房贷计算')
.fontSize(20)
.fontWeight(700)
}
.height('100%')
.width('100%')
}
@Builder
PageMap() {
CalculationDetails()
}
build() {
Column() {
Navigation(this.pageInfo) {
Column({ space: 20 }) {
this.formRadioItemBuilder('贷款类型', loanTypes, 'loanType',
(val) => {
this.mortgageLoanForm.loanType = val as LoanType
this.mortgageLoanForm =
JSON.parse(JSON.stringify(this.mortgageLoanForm))
})
if (['ProvidentFund', 'Combination'].includes(
this.mortgageLoanForm.loanType)) {
this.formInputItemBuilder('公积金贷款数', '万元', (val) => {
this.mortgageLoanForm.providentFundLoanAmount =
Number(val) * 10000
})
this.formInputItemBuilder('公积金利率', '%', (val) => {
this.mortgageLoanForm.providentFundInterestRate =
Number(val) * 0.01
})
}
if (['Commercial', 'Combination'].includes(
this.mortgageLoanForm.loanType)) {
this.formInputItemBuilder('商业贷款', '万元', (val) => {
this.mortgageLoanForm.loanAmount = Number(val) * 10000
})
this.formRadioItemBuilder('利率方式', rates,
'interestRateType', (val) => {
this.mortgageLoanForm.interestRateType =
val as InterestRateType
})
this.formInputItemBuilder('LPR', '%', (val) => {
this.mortgageLoanForm.lprRate = Number(val) * 0.01
})
this.formInputItemBuilder('基点', '%', (val) => {
this.mortgageLoanForm.basisPoints = Number(val) * 0.01
})
}
Flex({
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.SpaceBetween
}) {
Text('按揭年数')
.fontSize(16)
Flex({
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.End
}) {
Text(`${this.mortgageLoanForm.loanYears}年
(${this.mortgageLoanForm.loanYears * 12})期`)
.width('100%')
.textAlign(TextAlign.End)
}
.layoutWeight(1)
.onClick(() => {
this.ctx.getPromptAction().openCustomDialog(this.contentNode)
})
}
}
.padding(10)
.margin({ top: 40 })
Button('开始计算')
.backgroundColor('#d99756')
.width('90%')
.margin({ top: 100 })
.onClick(() => {
this.pageInfo.pushPath(
{ name: 'default', param: this.mortgageLoanForm }
);
})
}
.height('100%')
.width('100%')
.navDestination(this.PageMap)
.mode(NavigationMode.Stack)
.title(this.titleBuilder)
.titleMode(NavigationTitleMode.Mini)
.hideBackButton(true)
}
.height('100%')
.width('100%')
}
}
2. 等额本金计算函数
功能:依据贷款金额、月利率和总月数,计算等额本金还款方式下每月的还款信息。
步骤:
-
- 算出每月固定的本金偿还额。
- 利用循环逐月计算利息、每月还款金额和剩余本金。
- 将每月的还款信息存入 paymentInfoList 数组并返回。
function calculateEqualPrincipal(loanAmount: number, monthlyInterestRate: numbe,
totalMonths: number) {
const principalRepayment = loanAmount / totalMonths;
const paymentInfoList = [];
let remainingPrincipal = loanAmount;
for (let i = 0; i < totalMonths; i++) {
const interestRepayment = remainingPrincipal * monthlyInterestRate;
const monthlyPayment = principalRepayment + interestRepayment;
remainingPrincipal -= principalRepayment;
paymentInfoList.push(
new MonthlyPaymentInfo(
monthlyPayment,
principalRepayment,
interestRepayment,
remainingPrincipal)
);
}
return paymentInfoList;
}
3. 商业房贷计算函数
功能:按照 MortgageLoanForm 中的信息,计算商业房贷的还款信息。
步骤:
-
- 从 mortgageLoanForm 中解构出所需的参数。
- 把贷款年限转换为总月数。
- 根据利率方式(LPR 或其他)计算月利率。
- 依据计算方式(等额本息或等额本金)调用相应的计算函数。
- 返回每月还款信息数组。
export function calculateMortgagePayment(mortgageLoanForm: MortgageLoanForm) {
const {
loanAmount,
loanYears,
interestRateType,
lprRate,
basisPoints,
calculationMethod
} = mortgageLoanForm;
const totalMonths = loanYears * 12;
let monthlyInterestRate;
if (interestRateType === 'LPR') {
monthlyInterestRate = (lprRate + basisPoints / 10000) / 12;
} else {
// 这里可以根据基准利率的逻辑进行处理,暂时简化为 0
monthlyInterestRate = 0;
}
if (calculationMethod === 'EqualPrincipalAndInterest') {
return calculateEqualPrincipalAndInterest(loanAmount,
monthlyInterestRate,
totalMonths);
} else if (calculationMethod === 'EqualPrincipal') {
return calculateEqualPrincipal(loanAmount,
monthlyInterestRate,
totalMonths);
}
return [];
}
4. 公积金贷款计算函数
功能:根据 MortgageLoanForm 中的信息,计算公积金贷款的还款信息。
步骤:
-
- 从 mortgageLoanForm 中解构出公积金贷款相关的参数。
- 将贷款年限转换为总月数。
- 计算月利率。
- 根据计算方式(等额本息或等额本金)调用相应的计算函数。
- 返回每月还款信息数组。
export function calculateProvidentFundLoan(mortgageLoanForm: MortgageLoanForm) {
const { providentFundLoanAmount,
providentFundInterestRate,
loanYears,
calculationMethod } = mortgageLoanForm;
const totalMonths = loanYears * 12;
const monthlyInterestRate = providentFundInterestRate / 12;
if (calculationMethod === 'EqualPrincipalAndInterest') {
return calculateEqualPrincipalAndInterest(providentFundLoanAmount,
monthlyInterestRate,
totalMonths);
} else if (calculationMethod === 'EqualPrincipal') {
return calculateEqualPrincipal(providentFundLoanAmount,
monthlyInterestRate,
totalMonths);
}
return [];
}
5. 组合贷款计算函数
功能:根据 MortgageLoanForm 中的信息,计算组合贷款(商业贷与公积金贷组合)的还款信息。
步骤:
-
- 分别调用 calculateMortgagePayment 和 calculateProvidentFundLoan 函数,计算商业贷款和公积金贷款的还款信息。
- 遍历两个还款信息数组,将每月的商业贷款和公积金贷款的还款金额、本金偿还额、利息偿还额和剩余本金相加。
- 将相加后的结果存储到 combinedPaymentInfo 数组中并返回。
export function calculateCombinedLoan(mortgageLoanForm: MortgageLoanForm) {
const commercialPaymentInfo =
calculateMortgagePayment(mortgageLoanForm);
const providentFundPaymentInfo =
calculateProvidentFundLoan(mortgageLoanForm);
const combinedPaymentInfo: MonthlyPaymentInfo[] = [];
for (let i = 0; i < commercialPaymentInfo.length; i++) {
const commercial = commercialPaymentInfo[i];
const providentFund = providentFundPaymentInfo[i];
const totalMonthlyPayment = commercial.monthlyPayment
+ providentFund.monthlyPayment;
const totalPrincipalRepayment = commercial.principalRepayment
+ providentFund.principalRepayment;
const totalInterestRepayment = commercial.interestRepayment
+ providentFund.interestRepayment;
const totalRemainingPrincipal = commercial.remainingPrincipal
+ providentFund.remainingPrincipal;
combinedPaymentInfo.push(
new MonthlyPaymentInfo(totalMonthlyPayment,
totalPrincipalRepayment,
totalInterestRepayment,
totalRemainingPrincipal)
);
}
return combinedPaymentInfo;
}
15.7.1.5 房贷计算页
- 通过getParamByName方法拿到表单页传入的参数
- 通过calculate方法实现计算贷款方式
- 基于Tabs实现等额本息和等额本金的切换
- 通过MonthlyPaymentComponent展示贷款内容
import { MonthlyPaymentComponent } from "../components/MonthlyPaymentComponent";
import { MonthlyPaymentInfo, MortgageLoanForm } from "../interface";
import { calculateCombinedLoan,
calculateMortgagePayment,
calculateProvidentFundLoan } from "../utils";
@ComponentV2
export struct CalculationDetails {
@Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack();
@Local mortgageLoanForm: MortgageLoanForm = {
loanAmount: 0,
loanYears: 1,
interestRateType: 'LPR',
calculationMethod: 'EqualPrincipalAndInterest',
loanType: "Commercial"
}
@Local calculatorResult: MonthlyPaymentInfo[] = []
@Builder
listItemGroupTitleBuilder(title: string) {
Text(title)
.width('100%')
.fontSize(20)
.fontWeight(500)
}
aboutToAppear() {
const mortgageLoanForms =
this.pageInfo.getParamByName('default') as MortgageLoanForm[]
this.mortgageLoanForm = mortgageLoanForms[0]
this.calculate()
}
calculate () {
if (this.mortgageLoanForm.loanType === 'Commercial') {
this.calculatorResult =
calculateMortgagePayment((this.mortgageLoanForm))
return
}
if (this.mortgageLoanForm.loanType === 'ProvidentFund') {
this.calculatorResult =
calculateProvidentFundLoan((this.mortgageLoanForm))
return
}
if (this.mortgageLoanForm.loanType === 'Combination') {
this.calculatorResult =
calculateCombinedLoan(this.mortgageLoanForm)
return
}
}
build() {
NavDestination() {
Tabs() {
TabContent() {
MonthlyPaymentComponent({ calculatorResult: this.calculatorResult })
}
.width('100%')
.height('100%')
.tabBar('等额本息')
TabContent() {
MonthlyPaymentComponent({ calculatorResult: this.calculatorResult })
}
.width('100%')
.height('100%')
.tabBar('等额本息')
}
.width('100%')
.height('100%')
.onChange((index) => {
this.mortgageLoanForm.calculationMethod =
index === 0 ? 'EqualPrincipalAndInterest' : 'EqualPrincipal'
this.calculate()
})
}.width('100%')
.height('100%')
.title('计算结果')
}
}
15.7.1.6 代码视频教程
完整案例代码与视频教程请参见:
代码:Code-15-01.zip。
视频:《元服务实现智算房贷》。
15.7.2 卡片案例实战实现计算器功能
本示例基于ArkTS卡片实现了一个卡片计算器,利用ArkTS的优势,卡片的设计注重简洁直观的用户界面,确保操作简便流畅,并且能够快速响应用户的输入。
15.7.2.1 案例效果截图
15.7.2.2 案例运用到的知识点
- 核心知识点
- 通过卡片的形式在未打开应用时体验应用部分功能
- 卡片开发流程
- 其他知识点
- ArkTS 语言基础
- V2版状态管理:@ComponentV2/@Local/
- Stage模型
- 自定义组件和组件生命周期
- @Builder装饰器:自定义构建函数
- @Extend装饰器:定义扩展组件样式
- if/else:条件渲染
- 日志管理类的编写
- MVVM模式
15.7.2.3 代码结构解读
├──entry/src/main/ets // 代码区
│ ├──calc
│ │ └──pages
│ │ └──CardCalc.ets // 计算器卡片页面
│ ├──entryability
│ │ └──EntryAbility.ets
│ ├──entryability
│ │ └──EntryFormAbility.ets // 卡片声明周期处理文件
│ ├──model
│ │ └──Logger.ts // 日志文件
│ └──pages
│ └──index.ets // 首页
└──entry/src/main/resources // 应用资源目录
15.7.2.4 计算器总体逻辑实现
- onInputValue函数
onInputValue函数接收代表用户在计算器界面点击按钮值的字符串参数value,依据不同输入值执行不同操作以更新计算器表达式与计算结果。若点击值为 C,函数会清空当前表达式this.expression和计算结果this.result 并结束执行;若为空字符串(即删除按钮),则删除表达式最后一个字符,用 calc 函数重新计算更新后的表达式结果并赋值给this.result,若表达式为空则清空结果;若为运算符,会检查表达式是否为空,若不为空且最后一个字符是运算符则替换,若为空且运算符为 * 或 / 则不处理,否则添加到表达式末尾;若点击值为 =,调用calc函数计算当前表达式结果并赋值给this.result,若结果不为空且不为undefined,将结果赋值给表达式this.expression 并清空结果;若为数字,则将数字添加到表达式末尾,调用calc函数重新计算结果并赋值给this.result。
// 处理用户输入值的函数
onInputValue = (value: string) => {
// 当用户点击 C 按钮时,清空表达式和计算结果
if (value === 'C') {
this.expression = '';
this.result = '';
return;
} else if (value === '') {
// 当用户点击删除按钮时,删除表达式的最后一个字符,并重新计算结果
this.expression =
this.expression.substring(0, this.expression.length - 1);
this.result = calc(this.expression);
if (!this.expression.length) {
this.result = '';
}
} else if (isOperator(value)) {
// 当用户点击运算符按钮时
let size = this.expression.length;
if (size) {
const last = this.expression.charAt(size - 1);
// 如果表达式最后一个字符也是运算符,则替换最后一个运算符
if (isOperator(last)) {
this.expression =
this.expression.substring(0, this.expression.length - 1);
}
}
// 如果表达式为空,且用户点击的是 * 或 / 运算符,则不做处理
if (!this.expression && (value === '*' || value === '/')) {
return;
}
this.expression += value;
} else if (value === '=') {
// 当用户点击等号按钮时,计算表达式的结果
this.result = calc(this.expression);
if (this.result!== '' && this.result!== undefined) {
this.expression = this.result;
this.result = '';
}
} else {
// 当用户点击数字按钮时,将数字添加到表达式中,并重新计算结果
this.expression += value;
this.result = calc(this.expression);
}
}
3. calc函数
calc函数通过将输入的中缀表达式解析、转换为后缀表达式,再计算后缀表达式的值,实现了对数学表达式的计算功能。
// 计算表达式的函数
export function calc(inputContent: string): string {
// 将中缀表达式解析为数组
const infixExpression: string[] = parseInfixExpression(inputContent);
// 将中缀表达式转换为后缀表达式
const suffixExpression: string[] = toSuffixExpression(infixExpression);
// 计算后缀表达式的值
return calcSuffixExpression(suffixExpression);
}
15.7.2.5 代码视频教程
完整案例代码与视频教程请参见:
代码:Code-15-02.zip。
视频:《服务卡片实现计算器》。
15.8 本章小结
本章围绕HarmonyOS开发中的元服务与服务卡片展开,深入介绍了相关技术。在元服务方面,阐述其概念、与应用和卡片的关系,从消费者、生态伙伴、开发者视角分析优势,讲解技术架构、开发流程(创建工程、生成图标、构建页面、真机运行)、云开发(开通服务、创建及调用云函数)以及发布(打包、申请证书、备案、上架)等内容。在服务卡片上,介绍了Form Kit,讲解ArkTS卡片运行机制(使用方、提供方、管理服务、渲染服务)、开发过程(创建、配置文件、生命周期管理)。最后通过智算房贷元服务和计算器卡片两个案例,展示了实际开发应用。前者涵盖商业、公积金、组合贷款计算,涉及元服务开发多方面知识点;后者基于ArkTS卡片实现计算器功能,体现了卡片形式的优势。通过本章学习,开发者能全面掌握HarmonyOS元服务和服务卡片的相关技术,为实际开发应用打下坚实基础。