【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注册的包名,确保与平台一致
- 选择设备类型(建议先选“手机”),点击“完成”
项目结构说明:
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
中配置页面路由:
{ "src": [ "pages/HomePage", "pages/GamePage", "pages/UserAgreement" ] }
2. 首页实现(HomePage.ets)
首页包含开始游戏按钮、近期分数和操作指南:
@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)
核心游戏逻辑包含网格生成、计时和点击判断:
@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)
{ "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)
@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
实现卡片数据刷新:
export default class ShulteFormAbility extends FormExtensionAbility { onUpdate(formId: string) { // 获取最新成绩 const bestScore = AppStorage.Get('bestScore') || '00:00'; // 更新卡片数据 this.updateForm(formId, { 'bestScore': bestScore }); } }
五、源码示例
// 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; }
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) } }