前言
如果你做过移动端开发,一定经历过这样的场景:用户只是想查个快递、看个天气,却要先去应用市场下载一个几十MB的App,等安装完,那股急切的心情早已烟消云散。应用体积膨胀、用户获取成本攀升,已经成为移动生态的顽疾。
HarmonyOS给出的解法叫做元服务(Atomic Service)。它不是"小程序"的简单翻版,而是从系统底层重新设计的一种轻量级服务形态——免安装、即点即用、支持卡片化展示,还能在设备间无缝流转。
本系列前文已经帮你搭好了ArkTS和ArkUI的基础(第1-3篇),也走过了路由、网络、数据持久化等核心能力(第4-7篇)。从这篇开始,我们将进入HarmonyOS生态独有的能力域。第11篇先带你理解元服务的本质,并手把手写出第一个可运行的元服务。
一、元服务到底是什么?
1.1 官方定义与技术特征
元服务(Atomic Service)是HarmonyOS提供的一种轻量级、免安装的应用形态。它具备四个核心特征:
1.2 元服务的两种形态
元服务在HarmonyOS中以两种形态存在:
形态一:独立元服务
有独立的包名和签名
可以独立发布到应用市场
拥有自己的生命周期管理
适合功能完整、可独立运营的服务场景
形态二:内嵌元服务
作为传统HarmonyOS应用的一部分存在
通过应用内的特定入口触发
与宿主应用共享部分资源
适合为已有App提供轻量级入口
1.3 元服务 vs 小程序 vs 快应用
国内开发者对"免安装轻应用"并不陌生,但元服务与小程序、快应用有本质区别:
关键结论:元服务不是"鸿蒙版小程序",而是HarmonyOS原生能力的一部分,它可以调用最完整的系统API,也能享受最底层的分布式能力。
二、元服务的工程结构与配置
2.1 创建元服务工程
在DevEco Studio中创建元服务与创建普通应用几乎一致:
File → New → Create Project
选择 Atomic Service 模板(而非Application)
填写Bundle Name、Save Location等基本信息
选择API版本(建议API 12+,即HarmonyOS NEXT 5.0)
创建完成后,工程结构与普通应用的主要区别在于 module.json5 的配置。
2.2 module.json5 核心配置解析
元服务的 entry/src/main/module.json5 中有几个关键字段:
{
"module": {
"name": "entry",
"type": "atomicService",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
],
"metadata": [
{
"name": "atomicService",
"value": {
"installationFree": true,
"minimumAtomicServiceVersion": 1
}
}
]
}
}
重点看三个配置:
2.3 包体约束与Ability限制
元服务不是"想怎么写就怎么写",HarmonyOS对它有明确的约束:
包体大小:单个HAP不超过5MB,整个元服务不超过10MB
Ability数量:最多4个UIAbility + 4个ServiceExtensionAbility
后台限制:后台存活时间严格受限,不支持常驻后台
权限范围:部分高危权限(如读取短信、通话记录)对元服务不可申请
这些约束倒逼开发者做功能聚焦。一个元服务应该只解决一个具体场景,而不是试图替代完整App。
三、开发第一个元服务:天气速查
3.1 需求与架构
我们来做一个最实用的示例——天气速查元服务。功能极简:
打开即显示当前定位城市的天气
支持桌面卡片展示温度与天气图标
点击卡片进入详情页,查看未来三天预报
这个示例同时覆盖元服务的两个核心能力:UIAbility页面 + Service Widget卡片。
3.2 主页面实现
// entry/src/main/ets/pages/Index.ets
import { http } from '@kit.NetworkKit';
import { promptAction } from '@kit.ArkUI';
interface WeatherData {
city: string;
temp: number;
condition: string;
icon: Resource;
forecast: Array<{
day: string;
tempHigh: number;
tempLow: number;
condition: string;
}>;
}
@Entry
@Component
struct WeatherPage {
@State weather: WeatherData | null = null;
@State isLoading: boolean = true;
private weatherIcons: Map<string, Resource> = new Map([
['晴', $r('app.media.ic_sunny')],
['多云', $r('app.media.ic_cloudy')],
['雨', $r('app.media.ic_rainy')],
['阴', $r('app.media.ic_overcast')]
]);
aboutToAppear() {
this.fetchWeather();
}
async fetchWeather() {
try {
// 实际项目中应调用真实天气API,此处模拟数据
await new Promise(resolve => setTimeout(resolve, 800));
this.weather = {
city: '北京',
temp: 26,
condition: '多云',
icon: this.weatherIcons.get('多云') ?? $r('app.media.ic_sunny'),
forecast: [
{ day: '明天', tempHigh: 28, tempLow: 18, condition: '晴' },
{ day: '后天', tempHigh: 25, tempLow: 16, condition: '雨' },
{ day: '大后天', tempHigh: 24, tempLow: 15, condition: '多云' }
]
};
} catch (err) {
promptAction.showToast({ message: '获取天气失败' });
} finally {
this.isLoading = false;
}
}
build() {
Column({ space: 16 }) {
if (this.isLoading) {
LoadingProgress()
.width(48)
.height(48)
.color('#2196F3')
} else if (this.weather) {
// 当前天气
Column({ space: 8 }) {
Text(this.weather.city)
.fontSize(20)
.fontColor('#666')
Image(this.weather.icon)
.width(80)
.height(80)
Text(`${this.weather.temp}°`)
.fontSize(64)
.fontWeight(FontWeight.Bold)
.fontColor('#333')
Text(this.weather.condition)
.fontSize(18)
.fontColor('#888')
}
.width('100%')
.padding(24)
// 分割线
Divider()
.strokeWidth(1)
.color('#EEE')
.margin({ left: 24, right: 24 })
// 未来三天预报
Row() {
ForEach(this.weather.forecast, (item, index) => {
Column({ space: 6 }) {
Text(item.day)
.fontSize(14)
.fontColor('#666')
Text(`${item.tempHigh}° / ${item.tempLow}°`)
.fontSize(16)
.fontColor('#333')
Text(item.condition)
.fontSize(12)
.fontColor('#888')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
})
}
.width('100%')
.padding(24)
.justifyContent(FlexAlign.SpaceAround)
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F7FA')
}
}
3.3 Service Widget 卡片实现
元服务的灵魂在于卡片(Service Widget)。用户不需要打开完整页面,在桌面或负一屏就能看到核心信息。
// entry/src/main/ets/entryformability/EntryFormAbility.ets
import { FormExtensionAbility, formBindingData, formInfo } from '@kit.FormKit';
import { want } from '@kit.AbilityKit';
export default class EntryFormAbility extends FormExtensionAbility {
// 卡片创建时触发
onAddForm(want: want): formBindingData.FormBindingData {
const formId = want.parameters?.[formInfo.FormParam.FORM_IDENTITY_KEY] as string;
const formName = want.parameters?.[formInfo.FormParam.FORM_NAME_KEY] as string;
// 模拟天气数据,实际应从网络获取或缓存读取
const formData = {
city: '北京',
temp: 26,
condition: '多云',
updateTime: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
};
return formBindingData.createFormBindingData(formData);
}
// 卡片更新时触发(如定时刷新、系统触发)
onUpdateForm(formId: string): void {
const formData = {
city: '北京',
temp: 25, // 模拟温度变化
condition: '晴',
updateTime: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
};
formBindingData.createFormBindingData(formData);
}
// 卡片销毁时触发
onRemoveForm(formId: string): void {
console.info(`Form removed: ${formId}`);
}
}
卡片UI在 entry/src/main/ets/widget/pages/CardWidget.ets 中定义:
// entry/src/main/ets/widget/pages/CardWidget.ets
let storage = new LocalStorage();
@Entry(storage)
@Component
struct CardWidget {
@LocalStorageProp('city') city: string = '--';
@LocalStorageProp('temp') temp: number = 0;
@LocalStorageProp('condition') condition: string = '--';
@LocalStorageProp('updateTime') updateTime: string = '--:--';
build() {
Column({ space: 8 }) {
Row() {
Text(this.city)
.fontSize(14)
.fontColor('#666')
Text(this.updateTime)
.fontSize(10)
.fontColor('#999')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row({ space: 12 }) {
Text(`${this.temp}°`)
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor('#2196F3')
Column({ space: 4 }) {
Text(this.condition)
.fontSize(14)
.fontColor('#666')
Text('天气速查')
.fontSize(10)
.fontColor('#999')
}
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.shadow({ radius: 8, color: 'rgba(0,0,0,0.08)', offsetX: 0, offsetY: 2 })
}
}
3.4 卡片配置:form_config.json
卡片需要在 entry/src/main/resources/base/profile/form_config.json 中注册:
{
"forms": [
{
"name": "WeatherCard",
"displayName": "$string:weather_card_name",
"description": "$string:weather_card_desc",
"src": "./ets/widget/pages/CardWidget.ets",
"uiSyntax": "arkts",
"window": {
"designWidth": 360,
"autoDesignWidth": true
},
"colorMode": "auto",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "10:30",
"updateDuration": 1,
"defaultDimension": "2*2",
"supportDimensions": [
"1*2",
"2*2",
"2*4"
]
}
]
}
关键配置说明:
updateEnabled+updateDuration:支持定时刷新,单位是30分钟(1=30分钟)supportDimensions:声明卡片支持的尺寸,系统会根据桌面空间自动适配scheduledUpdateTime:每天固定时间点刷新
四、元服务的分发与触达
开发完成只是第一步,元服务的价值在于系统级分发能力。
4.1 分发入口矩阵
4.2 意图框架(Intent Framework)接入
HarmonyOS NEXT引入了意图框架,系统可以根据用户当前场景主动推荐元服务。例如用户收到一条带地址的短信,系统可能直接在底部推荐"打开地图导航"的元服务入口。
接入方式是在 module.json5 中声明意图:
{
"skills": [
{
"actions": [
"action.intent.WEATHER_QUERY"
],
"entities": [
"entity.weather.city"
]
}
]
}
这意味着当系统识别到用户有"查天气"意图时,你的元服务可能出现在推荐位。
五、元服务的调试与测试
5.1 真机调试要点
元服务的调试与普通应用基本一致,但有几个特殊点:
卡片预览:DevEco Studio提供Form Preview功能,可在不运行完整应用的情况下预览卡片UI
卡片刷新:手动触发卡片更新的命令:
hdc shell am service -a ohos.form.action.REFRESH -e formId <form_id>免安装验证:确保
installationFree为true,且应用市场签名正确,否则无法以元服务形态运行
5.2 常见问题排查
六、小结与下篇预告
元服务不是"小一号的App",而是HarmonyOS生态中一种全新的服务交付范式。它用极致的轻量化换取极致的触达效率,用系统级分发取代应用市场的人海战术。
本文带你完成了三件事:
理解元服务的本质:免安装、卡片化、跨设备、系统级分发
掌握工程配置:module.json5的关键字段、包体约束、Ability限制
实战开发:一个带Service Widget的天气速查元服务,覆盖UIAbility + 卡片双形态
但元服务跑在系统层,安全问题不容忽视。免安装意味着更严格的权限管控,系统级分发意味着更高的安全合规要求。第12篇将深入鸿蒙的安全机制与权限管理,从应用签名、ACL访问控制到隐私合规,帮你筑牢元服务的安全防线。
参考阅读
本系列第1篇:《鸿蒙开发初体验:从零搭建DevEco Studio到运行第一个ArkUI应用》
本系列第3篇:《ArkUI声明式UI实战:组件、布局与状态管理入门》