1. 一分一段表元服务开发原因
做为高三就读孩子的家长,这两年对高考的志愿填报非常关注,希望在不浪费一分的情况下尽可能考上更合适的专业,要做到这一点很不容易,因为这不仅仅和孩子自己的分数有关,更和这个分数在全省的排名有关,需要了解到有多少考生可能构成竞争关系,比你分数低的不会和你竞争,比你分数高很多的也不会和你竞争,只有那些比你的分数高个十几二十分的人才会和你竞争,了解这个区间每个分数段的考生数量分布,对于合理填报志愿非常重要。当然,各个地区的招生办公室都会公布高考成绩的一分一段表,大部分是图片或者pdf格式的,可以人工去查,实际操作起来比较麻烦,为简化查找,决定自己开发一个这样的应用,恰好鸿蒙也在这个时间点发布了5.0版本,其中的元服务简直就是为这种类型的应用量身定制的,不需要安装,点开即用,简直太完美了,最终决定使用元服务的形式进行开发。
2. 为什么选择端云一体化开发
2.1. 数据量估算
确定开发形式后,就开始技术调研,首先估算数据量,以山东省为例,科目选择是3+3的形式,考试成绩除了有总分排名外,另外还有选考化学、物理、生物、思政、历史、地理的排名,也就是有7种分数排名,按照2022到2024年三年的夏季高考一分一段表数据量计算,总条数达到了11382条,这个数据量还会以每年7千条左右的速度增加。这只是山东省的,全国有31个高考省份,总数量可能会有数十万条,使用文件存储不太现实了,不但更新不方便,最后打包的大小也会超出元服务包2M的限制,所以,只能选择线上数据库的形式进行数据管理。
2.2. 数据库选型
决定使用数据库后,就要对数据库进行选型,无论是购买服务器安装数据库,还是直接租用数据库,都会产生较高的费用,维护工作量也会增加。幸好,华为应用市场应用一站式服务平台AppGallery Connect(AGC)提供了云数据库,可以方便的进行数据管理,而且鸿蒙的云开发服务Cloud Foundation Kit为此提供了全方位的支持,可以在鸿蒙应用中通过API直接操作数据库,开发效率直接飞起。最吸引人的还不止这些,云数据库可以免费试用 ,提供了充足的免费额度,对于一般的应用,免费额度用不完,根本用不完,额度明细如下图所示:
既然AGC这样慷慨,那我也就不客气了,直接选它作为数据库使用,在当前版本中设计了两个数据库表,一个存储分数类别,一个存储具体的分数信息,如图所示:
2.3. 首开速度的优化
在本应用的功能设计中,用户进入页面后会选择高考的地区、科目和年度,而且是联动选择的,也就是说,需要预先加载所有这些类别信息。问题在于,这些信息可能会有几百条数据,需要一定的下载时间,如果让用户等待时间过长,显然不太友好,特别是在用户第一次使用时,还要叠加元服务包本身的下载和安装时间,这个矛盾就更突出了。解决起来也不复杂,使用AGC提供的预加载服务即可,把需要在首页使用的主要信息通过预加载的形式加载到缓存中,用户首开页面时,直接通过缓存读取数据,从而极大地提高响应速度,优化用户体验,增加用户留存的概率。在具体的实现中,类别信息是存储在云数据库对象AreaScoreType中的,通过云函数get-all-score-type-list对该云数据库对象进行操作,读取所有的类别,转化为json字符串,然后预加载那里选择该函数作为实现预加载的形式。云函数和预加载的截图如下所示: 云函数:
预加载:
预加载需要使用云开发服务中的云函数模块cloudFunction,通过call接口实现预加载,本示例封装的预加载函数如下:
javascript
export function functionPreload() {
let promise = cloudFunction.call({
name: "get-all-score-type-list", // 预加载缓存数据的云函数名称
timeout: 3 * 1000, // 获取缓存数据的超时时间
loadMode: cloudFunction.LoadMode.PRELOAD // 获取缓存数据必须设置为PRELOAD
}
);
promise.then((data: cloudFunction.FunctionResult) => { // 接口调用成功处理缓存的应用数据
hilog.info(0x0000, 'testTag', 'get preload cache successfully');
AppStorage.setOrCreate('scoreTypeList', JSON.stringify(data.result));
}).catch((err: Error) => {
hilog.error(0x0000, 'testTag', 'fail to get preload cache: %{public}s', err.message);
functionNormal(); // 使用普通方式获取应用数据
})
}
复制
在EntryAbility的onCreate生命周期函数里调用此函数即可:
scss
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
functionPreload()
}
复制
这样,通过预加载就把分数类型信息序列化后的字符串存储到了全局变量scoreTypeList中。然后,在要使用该变量的页面中使用@StorageLink装饰该变量:
less
@StorageLink('scoreTypeList') scoreTypeList: string = "";
复制
通过如下的方式进行使用:
javascript
//存储所有省区、科目、年度的列表
let allAreaScoreTypeList: AreaScoreType[] = []
//如果没有通过预加载获取到所有省区、科目、年度列表的字符串,直接从数据库读取
if (this.scoreTypeList == undefined || this.scoreTypeList == "") {
allAreaScoreTypeList = await findAllAreaScoreType()
} else { //否则就从预加载的字符串解析
allAreaScoreTypeList = JSON.parse(this.scoreTypeList) as AreaScoreType[]
}
复制
2.4. 端云一体化的选择
上文解释了启用预加载的原因和大体流程,其实还是没有回答为什么要选择端云一体化的形式进行开发,我们接着讨论。上文提到了云函数get-all-score-type-list,该函数实现了对云数据库的访问,那么,具体的函数代码如何编写?如何上传到云端,如何进行调试?如果传统的方式,也是可以的,但是非常复杂,AGC里也有相关的文档,我也就不赘述了,但是,如果使用端云一体化的话,一切都变的简单高效了。按照AGC中关于端云一体化开发的文档进行操作,可以得到这样的HarmonyOS工程,包括两个项目,如下图所示:
工程上部为端侧的项目,下部为云侧的项目,端侧的和普通的HarmonyOS应用项目没什么区别,需要注意的是云侧的,它的结构和端侧是有区别的,运行环境为nodejs,代码扩展名为ts,本应用云函数的代码如下所示:
javascript
import { CloudDbZoneWrapper } from './CloudDBZoneWrapper';
let myHandler = async function (event, context, callback, logger) {
let cloudDBZoneWrapper = new CloudDbZoneWrapper();
let result = await cloudDBZoneWrapper.queryScoreTypes();
return callback(JSON.stringify(result));
};
export { myHandler };
复制
要部署函数到云端也非常简单,直接右键菜单单击Deploy"get-all-score-type-list"部署即可:
运行或者调试也一样简单,还是上面的右键菜单,然后单击"Run"或者"Debug"启动函数,再单击"视图"菜单"工具窗口"子菜单下的"Cloud Functions Requestor",弹出云函数请求窗口,如图所示:
选择要请求的函数,选择本地还是云端环境,然后输入触发事件需要的参数,再单击"Trigger"按钮即可获取调用结果,如图所示:
3. 一多特性的实现
现在的端侧机型越来越多,手机、平板、折叠屏都需要适配,而且还有横屏和竖屏的区别,如果每种情况都通过独立的页面实现,那开发工作量也太大了,维护也很不方便。鸿蒙针对这种情况提供了一多的支持,可以一次开发适配多种机型,极大地提高了应用开发的效率,本应用也利用了一多能力,实现了对手机、平板、折叠屏的适配,界面如下所示:
竖屏:
横屏:
平板:
折叠屏:
毕竟作者是个后端开发者,审美能力有限,也许界面有点简陋,大家重点关注下一多能力的技术实现方面。
本应用借鉴了官方示例中一多能力的代码,封装了OneMoreHelper工具类,可以计算屏幕基于宽度和高度的类别,代码如下:
ini
import { display, window } from "@kit.ArkUI";
export function updateWidthBp(windowObj?: window.Window): void {
if (windowObj === undefined) {
return;
}
let mainWindow: window.WindowProperties = windowObj.getWindowProperties();
let windowWidth: number = mainWindow.windowRect.width;
let windowWidthVp = windowWidth / display.getDefaultDisplaySync().densityPixels;
let widthBp: string = '';
if (windowWidthVp < 320) {
widthBp = 'xs';
} else if (windowWidthVp >= 320 && windowWidthVp < 600) {
widthBp = 'sm';
} else if (windowWidthVp >= 600 && windowWidthVp < 840) {
widthBp = 'md';
} else if (windowWidthVp >= 840 && windowWidthVp < 1440) {
widthBp = 'lg';
} else {
widthBp = 'xl';
}
AppStorage.setOrCreate('currentWidthBreakpoint', widthBp);
}
export function updateHeightBp(windowObj?: window.Window): void {
if (windowObj === undefined) {
return;
}
let mainWindow: window.WindowProperties = windowObj.getWindowProperties();
let windowHeight: number = mainWindow.windowRect.height;
let windowWidth: number = mainWindow.windowRect.width;
let windowWidthVp = windowWidth / display.getDefaultDisplaySync().densityPixels;
let windowHeightVp = windowHeight / display.getDefaultDisplaySync().densityPixels;
let heightBp: string = '';
let aspectRatio: number = windowHeightVp / windowWidthVp;
if (aspectRatio < 0.8) {
heightBp = 'sm';
} else if (aspectRatio >= 0.8 && aspectRatio < 1.2) {
heightBp = 'md';
} else {
heightBp = 'lg';
}
AppStorage.setOrCreate('currentHeightBreakpoint', heightBp);
}
复制
在EntryAbility的onWindowStageCreate生命周期函数进行调用:
kotlin
windowStage.getMainWindow().then((data: window.Window) => {
this.windowObj = data;
updateWidthBp(this.windowObj);
updateHeightBp(this.windowObj);
this.windowObj.on('windowSizeChange', (windowSize: window.Size) => {
updateWidthBp(this.windowObj);
updateHeightBp(this.windowObj);
})
})
复制
代码比较容易理解,就是在创建窗口和窗口大小变化时,重新计算宽度和高度分类,并记录到AppStorage的currentWidthBreakpoint和currentHeightBreakpoint中。在页面index.ets定义变量currentWidthBreakpoint和currentHeightBreakpoint,实时获取窗口宽高类型的变化:
less
@StorageLink('currentWidthBreakpoint') currentWidthBreakpoint: string = 'md';
@StorageLink('currentHeightBreakpoint') currentHeightBreakpoint: string = 'sm';
复制
在定义组件的时候,通过这些变量实现一多界面的适配,以本应用两个柱状图和折线图的适配为例,代码如下所示:
kotlin
GridRow({
columns: 12,
gutter: { x: 5, y: 5 },
direction: GridRowDirection.Row
}) {
//柱状图
GridCol({
span: {
xs: 12,
md: (this.currentHeightBreakpoint == "lg") ? 12 : 6
}
}) {
McBarChart({
options: this.barSeriesOption
})
.width("100%")
.height("100%")
.padding({
left: ((this.currentHeightBreakpoint == "sm") && this.currentWidthBreakpoint != "xl") ? 15 : 0
})
}
.height((this.currentHeightBreakpoint == "sm" || this.currentHeightBreakpoint == "md") ? "100%" : "50%")
//折线图
GridCol({
span: {
xs: 12,
md: (this.currentHeightBreakpoint == "lg") ? 12 : 6
}
}) {
McLineChart({
options: this.lineSeriesOption
})
.width("100%")
.height("100%")
}
.height((this.currentHeightBreakpoint == "sm" || this.currentHeightBreakpoint == "md") ? "100%" : "50%")
}
.width('100%')
.height(250)
.flexGrow(1)
复制
这样,只要明确了目标屏幕的分类,就可以很方便的进行适配。
4. 借助云测试保证应用质量
应用开发是一个牵涉到各个层面的复杂工程,在目前支持HarmonyOS 5.0的设备比较少的情况下,这一复杂性就更突出了,为了更好的服务于广大开发者,AGC提供了云测试能力,可以使用真机对兼容性、稳定性、性能、功耗、UX进行全方位测试,同样每天提供300分钟的免费额度,真是量大管饱。本应用的云测试效果如下所示:
云测试详情:
通过了云测试,这下提交应用上架的底气更足了!
5. 结语
HarmonyOS 5.0版本提供的开发能力非常强大,AGC在此基础上扩展了更多的功能性、易用性能力,特别是针对开发者开发过程中的痛点、难点,AGC提供的解决方案简直称得上完美,这里呼吁广大开发者,积极了解、合理利用AGC能力,为应用的开发、上架插上腾飞的翅膀。