【无聊数学】从零开始开发一个鸿蒙元服务 原创
0.引言
元服务(原名为原子化服务)是HarmonyOS提供的一种面向未来的服务提供方式,是有独立入口、免安装、可为用户提供一个或多个便捷服务的新型应用程序形态。本文介绍了应用《无聊数学》元服务的开发初衷、设计理念和开发过程,并进行了开源。文章分为六节内容,欢迎阅读和交流。
1.为什么会有《无聊数学》
不管什么原因,人们的时间是越来越碎片化了,更甚至,时间的碎片化已经成了一部分人的生活常态。零星的时间碎片往往让人觉得无聊,除了无脑地刷短视频,能不能做些灵动点的事情呢?比如说,思考,甚至是深度点的思索!毕竟,人的脑子是越用越灵光,越不用越生锈。哲学家、思想家培根曾经说过“数学是思维的体操”。在碎片化的时间里,整两道数学题,动动脑子,应该是不辜负时光的一个好法子。那就开发一款手机应用,满足这个初衷。在无聊的时间里,玩玩数学,那这个应用就叫无聊数学了。
再升华一下,《无聊数学》的价值主张和初衷就非常的朴素了:
用数学打发无聊时间,越打发,越灵光
《无聊数学》是一个多端应用,有android版本,也有小程序版本,也有华为鸿蒙操作系统的元服务(改名字之前叫原子服务)。
微信小程序
如果您对这个《无聊数学》这个应用感兴趣,也可以访问刚应用的详情页面,这里是传送门:
2.《无聊数学》是一个怎样的应用
有了美好的愿景,还要进一步策划。为了实现初衷,这个应用要有以下几个特征才行:
- 信手拈来,随时随用,无需安装:这么轻量的应用,如果还要安装,对用户来说,真是多余的麻烦;
- 入口便捷,时刻显示简便和关键的信息:帮助用户克服大脑的懒惰,在打开短视频之前,先打开无聊数学;
- UI操作简单,耐看养眼:毕竟数学题需要思考,用户可以边看边想;
- 快速开发,快速上线,快速迭代;
- 数学题目侧重于逻辑推理,不堆砌数学知识;
鉴于以上几个特点,无聊数学非常适合开发成鸿蒙系统的元服务,技术架构上,很适合使用华为的云端一体化技术进行构建,具体原有,可以参考下表的描述。
《无聊数学》的特征 | 华为技术方案 |
1.信手拈来,随时随用,无需安装 | 元服务(原名为原子化服务)是HarmonyOS提供的一种面向未来的服务提供方式,是有独立入口、免安装、可为用户提供一个或多个便捷服务的新型应用程序形态。 详情可以参考官方文档。 |
2.入口便捷,时刻显示简便和关键的信息 | 元服务卡片,支持用户无需打开元服务便可获取服务内重要信息的展示和动态变化,可以动态展示用户的解题积分、解题进度、每日一题等信息。 |
3.UI操作简单,耐看养眼 | 元服务使用ArkUI开发,ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS基于TypeScript(简称TS)语言扩展而来,是TS的超集。可以快速构建UI界面和实现操作的逻辑。 详情可以阅读官方文档 |
4.快速开发,快速上线,快速迭代 | 1.开发工具DevEco,一个基于Serverless和ArkUI的端云一体化低代码开发平台,您可通过拖拽式开发,可视化配置构建元服务; 2.Serverless服务包含了云函数、云数据库、云存储和API网关等服务,可以作为应用的后端,无需自行搭建服务架构 |
3.开始动手
DevEco Studio是基于IntelliJ IDEA Community开源版本打造,面向华为终端全场景多设备的一站式集成开发环境(IDE),为开发者提供工程模板创建、开发、编译、调试、发布等E2E的HarmonyOS应用/服务开发。《无聊数学》元服务使用DevEco Studio 3.1 Release进行开发。
无聊数学元服务的前端使用ArkTS开发,后端采用Serverless服务,所以,在DevEco Studio中新建工程,选择“Empty Ability with CloudDev”模版,这个模版创建的工程,包括了元服务前端的开发和Serverless后端开发两部分。新建工程的具体选择路径“Create Project-->Atomic Service-->Empty Ability with CloudDev”。具体的操作,可以参考以下的截图。
(1)选择“Empty Ability with CloudDev”模版:
选择模版
(2)填写工程的属性,其中,Bundle name需要和AppGallery Connect中创建的元服务应用的包名一致。
配置Stage模型
(3)新建工程和AppGallery Connect中创建的元服务应用关联,这是由IDE自动完成的。
登录管理账号
工程创建之后,可以看到,主要由两部分组成,一部分是Application,是开发元服务前段的,一部分是CloudProgram,是开发serverless服务的。在后续的章节,会陆续进行介绍和设计开发。
工程目录结构
4.用户端的设计和开发
4.1应用的主题颜色
考虑到界面的养眼耐看,采用了深蓝色作为主颜色,为了避免过于单一枯燥,采用比较活泼的蓝绿作为辅助色,为了避免刺眼,文字标题采用了有一定灰度的颜色。如下图所示。
整个应用都需要使用到这些主题颜色,所以,可以将这些主题颜色定义成一个公共文件color.json和全局常量,重复使用。如下图所示。
color.json是一个json文件,具体如下:
{
"color": [
{
"name": "main_blue",
"value": "#143254"
},
{
"name" : "light_blue",
"value" : "#17426d"
},
{
"name" : "lighter_blue",
"value" : "#3e5d79"
},
{
"name" : "theme_green",
"value" : "#65a645"
},
{
"name" : "main_text_color",
"value" : "#c8c7c7"
},
{
"name" : "title_color",
"value" : "#5a5b5b"
}
]
}
4.2服务卡片的设计与开发
先看效果图,元服务卡片的设计需要符合华为官方的要求,在这前提之下,颜色采用本应用的主题颜色,视觉上,保持一致。
卡片采用堆叠布局,如图所示。
具体的布局代码如下:
@Builder buildStaticPictureCard(){
Stack() {
Image($r("app.media.card_bg"))
.width( this.FULL_WIDTH_PERCENT)
.height( this.FULL_HEIGHT_PERCENT)
.objectFit(ImageFit.Cover)
Column() {
Column(){
Text('无聊数学')
.fontSize('14fp')
.textOverflow({ overflow: TextOverflow.Ellipsis })
.fontColor('#C2C2C2')
.maxLines(1)
Text('用数学打发无聊时间')
.fontSize('10fp')
.textOverflow({ overflow: TextOverflow.Ellipsis })
.fontColor('#C2C2C2')
.maxLines(2)
.margin({top:'6fp'})
}
.width('100%')
.height('60fp')
.backgroundColor('#17426d')
.padding({top:'12fp',left : '10fp', right : '10fp'})
.alignItems(HorizontalAlign.Start)
.justifyContent(FlexAlign.Start)
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Start)
}
.width( this.FULL_WIDTH_PERCENT)
.height( this.FULL_HEIGHT_PERCENT)
.backgroundColor($r('app.color.main_background'))
.onClick(() => {
postCardAction( this, {
"action": this.ACTION_TYPE,
"abilityName": this.ABILITY_NAME,
"params": {
"message": this.MESSAGE
}
});
})
}
4.3主功能页面
元服务最大的一个特点,就是,卡片式的服务,卡片上可以呈现关键信息,用户点击卡片之后,应该立即呈现服务的内容。用户在点击无聊数学的卡片之后,直接打开主功能页面。数学的答案主要是数字,再者,计算器是日常用品,在用户心智中是非常熟悉的事物,一看就会,所以,主功能页面就依照计算器进行设计开发,这个页面采用SideBarContainer容器进行布局,如下图所示。
主页的结构
SideBarContainer是可以显示和隐藏的侧边栏容器,通过子组件定义侧边栏和内容区,第一个子组件表示侧边栏,第二个子组件表示内容区。
SideBarContainer布局的具体代码如下:
build() {
SideBarContainer(SideBarContainerType.Overlay) {
//第一个子组件表示侧边栏
this.PageSideBarContainer()
//第二个子组件表示内容区
this.PageContent()
}
.controlButton({
left: 10,
top: 10,
width: 28,
height: 28,
icons: {
shown: $r('app.media.ic_more'),
hidden: $r('app.media.ic_more'),
switching: $r('app.media.ic_more_back')
}
})
.showSideBar( this.isShowSideBar)
.sideBarPosition(SideBarPosition.Start)
.sideBarWidth('55%')
.minSideBarWidth('50%')
.maxSideBarWidth('60%')
.autoHide(true)
}
Grid网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局,包含子组件GridItem。
Grid() {
GridItem() {
this.DigitalButton('1', ButtonType.Circle)
}
GridItem() {
this.DigitalButton('2', ButtonType.Circle)
}
GridItem() {
this.DigitalButton('3', ButtonType.Circle)
}
GridItem() {
this.levelButton('关卡')
}
GridItem() {
this.clearInput('清除')
}
GridItem() {
this.DigitalButton('4', ButtonType.Circle)
}
GridItem() {
this.DigitalButton('5', ButtonType.Circle)
}
GridItem() {
this.DigitalButton('6', ButtonType.Circle)
}
GridItem() {
this.DigitalButton('.', ButtonType.Circle)
}
GridItem() {
this.ConfirmButton('确定')
}.rowStart(2)
.rowEnd(3)
////////////////////
GridItem() {
this.DigitalButton('7', ButtonType.Circle)
}
GridItem() {
this.DigitalButton('8', ButtonType.Circle)
}
GridItem() {
this.DigitalButton('9', ButtonType.Circle)
}
GridItem() {
this.DigitalButton('0', ButtonType.Circle)
}
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr')
.rowsGap(8)
.columnsGap(8)
.width('100%')
.height('100%')
.margin({ top: 10 })
.align(Alignment.Center)
.border({ width: 0 })
.layoutDirection(GridDirection.Row)
5.云端的设计和开发
元服务和AppGallery Connect的服务天然集成,所以,元服务应用的后端采用serverless进行构建,应该是最好的选择了。serverless提供的服务很丰富,包括了基本的云函数、云数据库、云存储和App网关等服务。同时,无聊数学的账号系统基于serverless的认证服务进行开发。Serverless的认证服务提供的账号认证方式也是非常的丰富,常见的认证方式都包括了,例如:手机号码、邮箱地址、华为帐号、微信、QQ、apple等等。
无聊数学支持多端的应用,服务端采用了AppGallery Connect的serverless服务,前端处了元服务之外,还支持小程序、app和快应用,如图所示。
整体结构图
使用DevEco Studio开发云服务非常便捷,可以很方便地开发云函数和创建数据模型,以下举例说明。
无聊数学会依据用户解题的时间和正确率,计算用户每道题的得分,并将得分保存到云端,防止丢失。在这个小场景里面,元服务前端记录用户解题消耗的时间和正确率,并调用云函数处理相关的逻辑,最后,得分记录以数据模型QuestionScore进行记录,保存到云数据库中。
访问路径
5.1添加数据QuestionScore对象
数据模型
新建数据模型之后,会得到一个QuestionScore.json文件,这个文件描述的信息包括四部分:数据模型名称objectTypeName、访问权限permissions、索引indexes和字段fields。在编辑好数据模型之后,可以选择“Deploy Cloud DB”,将该模型,发布到云数据库。
{
"objectTypeName": "QuestionScore",
"permissions": [
...
],
"indexes": [
{
"indexName": "questionNo",
"indexList": [
{
"fieldName": "questionNo",
"sortType": "ASC"
}
]
},
...
],
"fields": [
{
"isNeedEncrypt": false,
"fieldName": "scoreId",
"notNull": true,
"isSensitive": false,
"belongPrimaryKey": true,
"fieldType": "String"
},
...
]
}
数据类型成功发布之后,便可以在AppGallery Connect管理后台,看到QuestionScore类型,如下图所示。
云函数管理后台
5.2开发云函数
(1)新建云函数
使用DevEco Studio开发一个云函数“add-question-score”,用于计算用户得分并处理相关逻辑,最后将结果保存到云数据库,具体操作如下图所示。
云函数管理后台
(2)配置云函数
云函数要访问云数据库,需要先做些配置,到AppGallery Connect下载agconnect-services.json文件,并放置云函数更目录。
云函数配置
(3)在云函数中初始化agcClient:
const credentialPath = "agc-apiclient-xxxx.json";
const clientName = "clientCN";
const region = "CN";
const zoneName = 'appsDataZone';
var agcClient = null;
///
agconnect.AGCClient.initialize(agconnect.CredentialParser.toCredential(path.join(__dirname, credentialPath)), clientName, region);
agcClient = agconnect.AGCClient.getInstance(clientName);
(4)进行数据库访问:
const clouddb = require('@hw-agconnect/database-server/dist/index.js');
////
clouddb.AGConnectCloudDB.initialize(agcClient);
const cloudDBZoneConfig = new clouddb.CloudDBZoneConfig(zoneName);
const cloudDBZoneClient = clouddb.AGConnectCloudDB.getInstance(agcClient).openCloudDBZone(cloudDBZoneConfig);
/////
//2.插入到QuestionScore const scoreId = crypto.randomUUID().replace(/-/g, "");
const questionScore = clouddb.CloudDBZoneGenericObject.build('QuestionScore');
questionScore.addFieldValue('scoreId', scoreId, true);
questionScore.addFieldValue('questionNo', Number(curQuesNo));
questionScore.addFieldValue('userId', userId);
questionScore.addFieldValue('score', Number(score));
questionScore.addFieldValue('timeSeconds', Number(timeSeconds));
questionScore.addFieldValue('answerTimes', Number(answerTimes));
questionScore.addFieldValue('createdTime', new Date());
questionScore.addFieldValue('updatedTime', new Date());
// const respQuestionScoreNum = await cloudDBZoneClient.executeUpsert(questionScore);
logger.info("QuestionScore->currentQuestionNo-> respQuestionScoreNum: " + respQuestionScoreNum);
(5)发布云函数
编写完云函数之后,就可以发布云函数了,如前面所示,通过右键“add-question-score”函数,点击“Deploy Function”,如果顺利,就可以发布云函数。
成功发布之后,就可以在AppGallery Connect中看到云函数了,如下图所示。
发布云函数
6.开源
目前,我们将《无聊数学》的鸿蒙操作系统的元服务版本进行了开源,感兴趣的同学可以通过以下的地址访问。
7.总结
本文介绍了无聊数学的构思过程,介绍了采用华为云端一体化的技术方案的开发过程,包括元服务卡片和云端的开发过程。整个开发过程涉及到的细节比较多,篇幅有限,本文不可能一一介绍,希望本文对读者有所启发。
目前,《无聊数学》的华为、小米、VIVO、OPPO等应用商店、元服务和微信小程序都已经上线,欢迎读者使用无聊数学,进行思维体操。
未完,《无聊数学》功能还在继续开发之中,希望广大读者,可以提供一些有趣的数学题目,继续完善数学关卡,谢谢!