【HarmonyOS】元服务入门详解 (一)

一、前言
首先元服务并非是小程序小游戏,也不是之前的快应用。元服务是使用ArkTS语言开发。共同点是与他们相同都是免安装的小安装包应用。
它与其他轻应用的共同点是免安装特性,但在技术实现和生态定位上有本质区别:
1、开发语言 :采用ArkTS(鸿蒙生态主推的声明式语言) 2、包体特性 :严格限制安装包大小(通常≤10MB),实现秒开体验 3、API体系 :使用独立的元服务API集,而非传统应用的系统API 4、分发方式 :通过"服务找人"模式,依托系统级卡片、小艺搜索等入口触达用户 5、体验优势:无需安装即可使用核心功能,支持跨设备流转
元服务因为限制安装包大小,并且使用单独的元服务API集,非应用开发直接使用系统API。正因为以上两点,做到了免安装的效果。元服务作为免安装的轻量服务形态,在用户触达和体验上有显著优势。
并且在开发模式上与应用也不同,需要先在AGC平台创建后,才能在IDE进行项目创建进行包名映射绑定。
元服务通过"服务找人"这个概念,使用系统级别的卡片入门,小艺搜索入门,提供系统级别的"小程序"体验。让用户可以在手机上,不用安装App,就能使用功能。
以本文将开发的"舒尔特方格"元服务为例,用户可以通过桌面卡片直接启动游戏,或通过小艺搜索"舒尔特方格"快速调用,无需经历传统应用的下载安装流程。
二、元服务开发前置步骤
1、AGC平台创建元服务项目
元服务开发需先在华为应用市场开发者平台(AGC)完成项目注册,步骤如下:
- 登录AGC平台,进入"我的项目"
- 点击"新增项目",选择"元服务"类型
- 填写基本信息(应用名称、包名等),包名需牢记(如
"com.atomicservice.691757860316xxxxx",
现在元服务的包名都是固定格式,最后都是appid。所以一定要先在AGC上创建项目)
2、 IDE创建元服务项目
使用DevEco Studio创建元服务项目:
- 新建项目时选择"元服务应用"模板
- 输入项目名称(如
ShulteTable
),选择保存路径 - 填写AGC注册的包名,确保与平台一致
- 选择设备类型(建议先选"手机"),点击"完成"
项目结构说明:
less
ShulteTable/
├─ entry/ // 主模块
│ ├─ src/main/ets/ // ArkTS代码
│ │ ├─ entryability/ // 入口能力
│ │ ├─ pages/ // 页面
│ │ └─ widget/ // 卡片
└─ agconnect-services.json // AGC配置文件
3. 元服务图标设计
元服务需准备两类图标: (1)应用图标 :1024x1024px,用于服务入口。可使用工具进行裁剪: 在entry文件夹,右键New,选择Image Asset:
(2)卡片图标:根据卡片尺寸设计(如2x2卡片为128x128px)
图标需符合鸿蒙设计规范,建议使用圆角设计,放置在entry/src/main/resources/base/media
目录下。
三、元服务UI开发
以舒尔特方格游戏为例,讲解元服务UI开发核心要点。 基于ArkTS的UI开发(页面路由、组件布局、状态管理)
1. 页面路由配置
在main_pages.json
中配置页面路由:
json
{
"src": [
"pages/HomePage",
"pages/GamePage",
"pages/UserAgreement"
]
}
2. 首页实现(HomePage.ets)
首页包含开始游戏按钮、近期分数和操作指南:
typescript
@Entry
@Component
struct HomePage {
@State recentScores: ScoreItem[] = [];
aboutToAppear() {
this.loadScores(); // 加载历史记录
}
loadScores() {
// 从本地存储获取数据
const scores = AppStorage.Get('scores') || [];
this.recentScores = scores.slice(0, 5);
}
build() {
Column() {
// 标题
Text('舒尔特方格')
.fontSize(32)
.fontWeight(FontWeight.Bold)
.margin(20)
// 开始按钮
Button('开始游戏')
.width('80%')
.height(50)
.backgroundColor('#007DFF')
.fontColor('#FFFFFF')
.onClick(() => router.pushUrl({ url: 'pages/GamePage' }))
.margin(10)
// 近期分数
Column() {
Text('近期成绩')
.fontSize(18)
.margin(10)
List() {
ForEach(this.recentScores, (item) => {
ListItem() {
Row() {
Text(`${item.size}x${item.size}`)
.width(60)
Text(item.time)
.flexGrow(1)
Text(item.date)
.width(80)
}
.padding(10)
}
})
}
}
.backgroundColor('#FFFFFF')
.borderRadius(10)
.padding(5)
.margin(10)
.width('90%')
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
interface ScoreItem {
size: number;
time: string;
date: string;
}
3. 游戏页面实现(GamePage.ets)
核心游戏逻辑包含网格生成、计时和点击判断:
typescript
@Entry
@Component
struct GamePage {
@State gridSize: number = 5;
@State numbers: number[] = [];
@State current: number = 1;
@State timer: string = '00:00';
@State timerId: number = 0;
aboutToAppear() {
this.initGrid();
}
initGrid() {
// 生成1~n²的随机序列
const total = this.gridSize * this.gridSize;
this.numbers = Array.from({ length: total }, (_, i) => i + 1)
.sort(() => Math.random() - 0.5);
this.current = 1;
}
startTimer() {
const startTime = Date.now();
this.timerId = setInterval(() => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
this.timer = `${Math.floor(elapsed / 60).toString().padStart(2, '0')}:${
(elapsed % 60).toString().padStart(2, '0')
}`;
}, 1000);
}
build() {
Column() {
// 计时器
Text(`用时: ${this.timer}`)
.fontSize(20)
.margin(10)
// 网格选择
Scroll({ direction: Axis.Horizontal }) {
Row() {
ForEach([3, 4, 5, 6, 7], (size) => {
Button(`${size}x${size}`)
.onClick(() => this.gridSize = size)
.margin(5)
.backgroundColor(this.gridSize === size ? '#007DFF' : '#F5F5F5')
})
}
.padding(5)
}
// 游戏网格
Grid() {
ForEach(this.numbers, (num) => {
GridItem() {
Button(num.toString())
.width('100%')
.height('100%')
.backgroundColor(num === this.current ? '#007DFF' :
num < this.current ? '#90EE90' : '#EEEEEE')
.onClick(() => {
if (num === this.current) {
if (this.current === this.gridSize * this.gridSize) {
// 游戏结束
clearInterval(this.timerId);
this.saveScore(); // 保存成绩
} else {
this.current++;
}
}
})
}
})
}
.columnsTemplate(Array(this.gridSize).fill('1fr').join(' '))
.rowsTemplate(Array(this.gridSize).fill('1fr').join(' '))
.width('90%')
.height('60%')
}
.width('100%')
.padding(10)
}
}
四、元服务卡片开发
元服务卡片可直接在桌面显示关键信息,支持快速交互。 桌面卡片的创建与数据交互。
1. 卡片配置(widget.json)
json
{
"forms": [
{
"name": "ShulteWidget",
"description": "舒尔特方格快捷卡片",
"src": "./ShulteWidget.ets",
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"colorMode": "auto",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "00:00",
"updateDuration": 1
}
]
}
2. 卡片实现(ShulteWidget.ets)

typescript
@Entry
@Component
struct ShulteWidget {
private formProvider: FormProvider = new FormProvider();
build() {
Column() {
Text('舒尔特方格')
.fontSize(16)
.margin(5)
Button('快速开始')
.width('80%')
.height(30)
.fontSize(14)
.backgroundColor('#007DFF')
.onClick(() => {
// 打开元服务
this.formProvider.startAbility({
bundleName: 'com.example.shultetable',
abilityName: 'EntryAbility'
});
})
Text('最佳记录: 01:25')
.fontSize(12)
.margin(5)
.fontColor('#666666')
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
.padding(10)
}
}
3. 卡片数据更新
通过FormExtensionAbility
实现卡片数据刷新:
typescript
export default class ShulteFormAbility extends FormExtensionAbility {
onUpdate(formId: string) {
// 获取最新成绩
const bestScore = AppStorage.Get('bestScore') || '00:00';
// 更新卡片数据
this.updateForm(formId, {
'bestScore': bestScore
});
}
}
五、源码示例

typescript
// pages/HomePage.ets
import router from '@ohos.router';
import promptAction from '@ohos.promptAction';
@Entry
@Component
struct HomePage {
@State recentScores: ScoreItem[] = [];
@State isLoading: boolean = true;
aboutToAppear() {
this.loadRecentScores();
}
// 加载最近5次游戏记录
loadRecentScores() {
// 模拟从本地存储加载数据
setTimeout(() => {
// 示例数据,实际应从AppStorage获取
this.recentScores = [
{ id: 1, time: '01:25', date: '2025-07-14', gridSize: 5 },
{ id: 2, time: '01:40', date: '2025-07-13', gridSize: 5 },
{ id: 3, time: '01:55', date: '2025-07-12', gridSize: 5 },
{ id: 4, time: '02:10', date: '2025-07-10', gridSize: 5 },
{ id: 5, time: '02:30', date: '2025-07-09', gridSize: 5 },
];
this.isLoading = false;
}, 500);
}
// 格式化日期显示
formatDate(dateStr: string): string {
const date = new Date(dateStr);
return `${date.getMonth() + 1}/${date.getDate()}`;
}
// 导航到游戏页面
navigateToGame() {
router.pushUrl({
url: 'pages/GamePage'
});
}
@Builder TitleView(){
// 顶部导航栏
Row() {
Button() {
// Image($r('app.media.back_arrow')) // 需要准备返回箭头图标
// .width(24)
// .height(24)
}
.onClick(() => router.back())
.margin({ left: 15 })
Text('专注力训练')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#007DFF')
.margin({ left: 20 })
}
.width('100%')
.height(50)
.backgroundColor('#FFFFFF')
}
/**
* 用户头像view
*/
@Builder UserInfoView(){
Column(){
// 用户头像
Image($r("app.media.icon"))
.width(px2vp(200))
.height(px2vp(200))
// 昵称
// 设置按钮
Button("设置")
.onClick(()=>{
router.pushUrl({
url: 'pages/AuthPage'
})
})
}
.margin({ bottom: 30, top: 20 })
}
build() {
Column(){
this.TitleView()
Column() {
this.UserInfoView()
// 开始游戏按钮
Button('开始游戏')
.width('80%')
.height(50)
.backgroundColor('#007DFF')
.fontColor('#FFFFFF')
.fontSize(18)
.onClick(() => this.navigateToGame())
.margin({ bottom: 30, top: 20 })
.borderRadius(25)
.hoverEffect(HoverEffect.Auto)
// 近期分数卡片
Column() {
Text('近期分数')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ left: 15, top: 10, bottom: 10 })
.width('100%')
if (this.isLoading) {
LoadingProgress()
.color('#007DFF')
.width(50)
.height(50)
.margin({ top: 20, bottom: 20 })
} else if (this.recentScores.length === 0) {
Text('暂无游戏记录')
.fontSize(16)
.fontColor('#999999')
.margin({ top: 20, bottom: 20 })
} else {
List() {
ForEach(this.recentScores, (score: ScoreItem) => {
ListItem() {
Row() {
Text(`${score.gridSize}x${score.gridSize}`)
.fontSize(16)
.width(70)
.textAlign(TextAlign.Center)
.fontColor('#007DFF')
.backgroundColor('#E6F4FF')
.margin({ right: 10 })
.padding(5)
.borderRadius(8)
Column() {
Text(score.time)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Start)
Text(this.formatDate(score.date))
.fontSize(12)
.fontColor('#999999')
.width('100%')
.textAlign(TextAlign.Start)
}
.width('100%')
}
.width('100%')
.height(60)
.padding({ left: 10, right: 10 })
}
})
}
.width('100%')
.height(220)
.margin({ bottom: 20 })
}
}
.width('90%')
.backgroundColor('#FFFFFF')
.borderRadius(15)
.margin({ bottom: 20 })
// 游戏操作指南卡片
Column() {
Text('游戏操作指南')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ left: 15, top: 10, bottom: 10 })
.width('100%')
Column() {
GuideItem({
index: 1,
content: '选择网格大小'
})
GuideItem({
index: 2,
content: '点击"开始游戏"按钮'
})
GuideItem({
index: 3,
content: '按照数字顺序点击网格中的数字'
})
GuideItem({
index: 4,
content: '完成所有数字点击后,游戏结束并显示用时'
})
GuideItem({
index: 5,
content: '点击"重置游戏"可重新开始'
})
}
.width('100%')
.padding(15)
}
.width('90%')
.backgroundColor('#FFFFFF')
.borderRadius(15)
.margin({ bottom: 30 })
}
.width('100%')
.height('100%')
.padding({ left: 15, right: 15 })
.backgroundColor('#F8F9FA')
}
.height(px2vp(2000))
.width("100%")
// Scroll(){
//
// }
// .width("100%")
// .height("100%")
// .scrollable(ScrollDirection.Vertical)
}
}
// 指南项组件
@Component
struct GuideItem {
index: number = 0;
content: string = '';
build() {
Row() {
Text(`${this.index}`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width(28)
.height(28)
.textAlign(TextAlign.Center)
.backgroundColor('#007DFF')
.fontColor('#FFFFFF')
.borderRadius(14)
.margin({ right: 10 })
Text(this.content)
.fontSize(16)
.width('90%')
}
.width('100%')
.margin({ bottom: 15 })
}
}
// 分数项接口
interface ScoreItem {
id: number;
time: string;
date: string;
gridSize: number;
}

typescript
import promptAction from '@ohos.promptAction';
@Entry
@Component
struct GamePage {
// 网格数
@State gridSize: number = 2; // 默认5x5网格
@State gridData: number[] = [];
@State currentNumber: number = 1;
@State isGameStarted: boolean = false;
@State isGameFinished: boolean = false;
@State timer: string = '00:00';
@State startTime: number = 0;
@State timerId: number = 0;
@State bestTime: string = '00:00';
aboutToAppear() {
this.initGrid();
const savedBestTime: string = AppStorage.get('bestTime') ?? "00:00";
if (savedBestTime) {
this.bestTime = savedBestTime;
}
}
/**
* 初始化网格数据
*/
initGrid() {
const totalCells = this.gridSize * this.gridSize;
this.gridData = [];
for (let i = 1; i <= totalCells; i++) {
this.gridData.push(i);
}
// 打乱顺序
this.gridData.sort(() => Math.random() - 0.5);
this.currentNumber = 1;
this.isGameFinished = false;
console.log("wppDebug", " list: " + JSON.stringify(this.gridData));
}
// 开始游戏
startGame() {
this.isGameStarted = true;
this.startTime = Date.now();
this.timerId = setInterval(() => {
const elapsedTime = Date.now() - this.startTime;
const seconds = Math.floor(elapsedTime / 1000) % 60;
const minutes = Math.floor(elapsedTime / 60000);
this.timer = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}, 1000);
}
// 处理格子点击
handleCellClick(number: number) {
if (!this.isGameStarted || this.isGameFinished) {
return;
}
if (number === this.currentNumber) {
if (number === this.gridSize * this.gridSize) {
// 游戏完成
this.isGameFinished = true;
clearInterval(this.timerId);
this.checkBestTime();
promptAction.showToast({ message: '恭喜,你完成了游戏!' });
} else {
this.currentNumber++;
}
}
}
// 检查是否是最佳时间
checkBestTime() {
const currentTime = this.timer;
if (this.bestTime === '00:00' || this.compareTime(currentTime, this.bestTime)) {
this.bestTime = currentTime;
AppStorage.set('bestTime', currentTime);
}
}
compareTime(time1: string, time2: string): number {
// 假设日期为同一天(如2000-01-01)
const date1 = new Date(`2000-01-01T${time1}:00`);
const date2 = new Date(`2000-01-01T${time2}:00`);
// 比较时间戳
return date1.getTime() - date2.getTime();
}
// 重置游戏
resetGame() {
clearInterval(this.timerId);
this.initGrid();
this.isGameStarted = false;
this.timer = '00:00';
}
// 改变网格大小
changeGridSize(size: number) {
if (this.isGameStarted && !this.isGameFinished) {
promptAction.showToast({ message: '请先完成当前游戏' });
return;
}
this.gridSize = size;
this.initGrid();
}
// 按钮格子数
@State buttonList: Array<string> = ['2x2', '3x3', '4x4', '5x5', '6x6', '7x7']; //, '8x8', '9x9'
@Builder ItemButtonView(){
Button('5x5')
.onClick(() => this.changeGridSize(5))
.margin(5)
.enabled(!this.isGameStarted || this.isGameFinished)
.backgroundColor(this.gridSize === 5 ? '#007DFF' : '#F5F5F5')
.fontColor(this.gridSize === 5 ? '#FFFFFF' : '#000000')
}
/**
* 格子列表
*/
@Builder ButtonListView(){
Scroll(){
Row() {
ForEach(this.buttonList, (item: string, index: number) => {
Button(item)
.onClick(() => this.changeGridSize(index + 2))
.margin(5)
.enabled(!this.isGameStarted || this.isGameFinished)
.backgroundColor(this.gridSize === (index + 2) ? '#007DFF' : '#F5F5F5')
.fontColor(this.gridSize === (index + 2) ? '#FFFFFF' : '#000000')
.height(px2vp(200))
}, (item: string) => item)
}
.width(px2vp(1800))
}
.margin(5)
.margin({ bottom: 20 })
.width("100%")
.scrollable(ScrollDirection.Horizontal)
.height(px2vp(210))
}
build() {
Column() {
// 标题
Text('专注力训练')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 10 })
// 计时器和最佳时间
Row() {
Text(`当前时间: ${this.timer}`)
.fontSize(18)
.margin({ right: 20 })
Text(`最佳时间: ${this.bestTime}`)
.fontSize(18)
}
.margin({ bottom: 20 })
// 网格大小选择
this.ButtonListView()
// 开始/重置按钮
Button(this.isGameStarted ? '重置游戏' : '开始游戏')
.onClick(() => {
if (this.isGameStarted) {
this.resetGame();
} else {
this.startGame();
}
})
.width('50%')
.margin({ bottom: 20 })
// 游戏网格
Grid() {
ForEach(this.gridData, (number: number) => {
GridItem() {
Button(`${number}`)
.width('100%')
.height('100%')
.backgroundColor(
this.isGameFinished
? '#90EE90'
: number < this.currentNumber
? '#90EE90'
: number === this.currentNumber
? '#007DFF'
: '#F5F5F5'
)
.fontColor(
number === this.currentNumber || number < this.currentNumber
? '#FFFFFF'
: '#000000'
)
.onClick(() => this.handleCellClick(number))
.enabled(this.isGameStarted && !this.isGameFinished)
}
})
}
.columnsTemplate(new Array(this.gridSize).fill('1fr').join(' '))
.rowsTemplate(new Array(this.gridSize).fill('1fr').join(' '))
.columnsGap(1)
.rowsGap(1)
.width('95%')
.height('60%')
.margin({ bottom: 20 })
}
.width('100%')
.height('100%')
.padding(15)
}
}