游戏万能卡片- Serverless(端云一体化开发) 原创 精华
@toc
1. 前言
舒尔特方格游戏-关系型数据库版本,请移步到上一篇帖子查看,本篇帖子内容还是舒尔特方格游戏,只是换了后宫^_^云数据库,此项目是用最新版DevEco Studio 3.1 Release并创建端云一体开发,由于目前此版本不支持直接调用云数据库,不过可以通过云函数调用云数据库,也就是在服务卡片业务逻辑里通过调用云函数来完成游戏数据保存到云数据库,开发工具支持本地函数调用测试,大大方便了开发,此贴重点讲解云函数和云数据库开发,本地和远端调用,从而进一步学习Serverless知识,舒尔特方格游戏效果图如下:
2.知识点
为丰富HarmonyOS对云端开发的支持、实现HarmonyOS生态端云联动,DevEco Studio推出了云开发功能,开发者在创建工程时选择云开发模板,即可在DevEco Studio内同时完成HarmonyOS应用/服务的端侧与云侧开发,体验端云一体化协同开发。
相比于传统开发模式,云开发模式具备成本低、效率高、门槛低等优势,具体区别见下表。
区别点 | 传统开发模式 | 云开发模式 |
---|---|---|
开发工具 | 端侧与云侧各需一套开发工具,云侧需自建服务器,工具成本高。 | DevEco Studio一套开发工具即可支撑端侧与云侧同时开发,无需搭建服务器,工具成本低。 |
开发人员 | 端侧与云侧要求不同的开发语言,技能要求高。需多人投入,且开发人员之间需持续、准确沟通,人力与沟通成本高、效率低。 | 依托AppGallery Connect(以下简称AGC)Serverless云服务开放的接口,端侧开发人员也能轻松开发云侧代码,大大降低开发门槛。开发人员数量少,降低人力成本,提高沟通效率。 |
运维 | 需自行构建运营与运维能力,成本高、负担重。 | 直接接入AGC Serverless云服务,实现免运维,无运维成本或资源浪费。 |
2.1 开发流程
HarmonyOS应用端云一体化开发流程如下图所示。
2.2 创建端云一体化开发工程
2.2.1 新建原子化服务工程
2.2.2 工程初始化配置
2.2.3 端云一体化开发工程介绍
2.3 开发云工程
2.3.1 开发云函数
2.3.2 开发云数据库
2.4 部署云工程
2.4.1 部署云工程
2.5 小结
了解这些端云一体化开发知识点后,下面围绕舒尔特方格游戏,在云数据库里设计卡片表结构和成绩表结构,然后再编写相关云函数,并在本地测试通过后,再测试远程,最后在元服务业务逻辑调用云函数。
3. 云数据库开发讲解
3.1 objecttype创建
3.1.1 展开CloudProgram -> clouddb -> objecttype 右击objecttype目录,创建 -> Cloud DB Object Type 输入Object Type Name为t_form,点击确认,修改内容如下:
{
"fields": [
{
"isNeedEncrypt": false,
"fieldName": "formId",
"notNull": true,
"belongPrimaryKey": true,
"fieldType": "String"
},
{
"isNeedEncrypt": false,
"fieldName": "formName",
"notNull": true,
"defaultValue": "",
"belongPrimaryKey": false,
"fieldType": "String"
},
{
"isNeedEncrypt": false,
"fieldName": "dimension",
"notNull": true,
"defaultValue": "0",
"belongPrimaryKey": false,
"fieldType": "Integer"
}
],
"indexes": [
{
"indexName": "formId",
"indexList": [{ "fieldName": "formId", "sortType": "ASC" }]
}
],
"objectTypeName": "t_form",
"permissions": [...]
}
3.1.2 展开CloudProgram -> clouddb -> objecttype 右击objecttype目录,创建 -> Cloud DB Object Type 输入Object Type Name为t_score,点击确认,修改内容如下:
{
"fields": [
{
"isNeedEncrypt": false,
"fieldName": "formId",
"notNull": true,
"belongPrimaryKey": true,
"fieldType": "String"
},
{
"isNeedEncrypt": false,
"fieldName": "matrixNum",
"notNull": true,
"defaultValue": "",
"belongPrimaryKey": false,
"fieldType": "String"
},
{
"isNeedEncrypt": false,
"fieldName": "bestScore",
"notNull": true,
"defaultValue": "0",
"belongPrimaryKey": false,
"fieldType": "Double"
}
],
"indexes": [
{
"indexName": "formId",
"indexList": [{ "fieldName": "formId", "sortType": "ASC" }]
}
],
"objectTypeName": "t_score",
"permissions": [...]
}
3.2 dataentry创建
3.2.1 展开CloudProgram -> clouddb -> dataentry 右击dataentry目录,创建 -> Cloud DB Data Entry 这里先选择上面创建的Object Type为t_form,再输入Data Entry Name为form_data,点击确认,修改内容如下:
{
"cloudDBZoneName": "widgetCard",
"objectTypeName": "t_form",
"objects": [
{
"formId": "x000001",
"formName": "卡片1",
"dimension": 2
}
]
}
3.2.2 展开CloudProgram -> clouddb -> dataentry 右击dataentry目录,创建 -> Cloud DB Data Entry 这里先选择上面创建的Object Type为t_score,再输入Data Entry Name为score_data,点击确认,修改内容如下:
{
"cloudDBZoneName": "widgetCard",
"objectTypeName": "t_score",
"objects": [
{
"formId": "x000001",
"matrixNum": "3x3",
"bestScore": 2.234
}
]
}
3.3 小结
其实dataentry文件可以不创建,这里对两个表都初始化了一条数据,是方便下面的调用使用,云数据库就是定义好表结构、权限配置就可以,数据的添加、修改、删除、查询都可以通过云函数来完成。
4. 云函数开发讲解
4.1 卡片云函数创建
4.1.1 展开CloudProgram -> cloudfunctions 右击cloudfunctions目录,创建 -> Cloud Function 输入Cloud Function Name为form-func,点击确认, 卡片云函数里包含了增删改查操作,所以在form-func下,创建不同的文件夹来区分,目录结构如下:
![image-20230612144738145]
4.1.2 首先说一下与云数据库交互文件,t_form.js对应的是云数据库实体类,如各属性的get和set方法,之前FA模式下的DevEco Studio端云一体化开发,支持直接调用云数据库,现在Stage模式下的DevEco Studio端云一体化开发,还不支持直接调用云数据库,通过云函数来调用,所以这里的云数据库实体类,除了属性的get和set方法外,还要手工添加一些方法,如卡片实例体类:
class t_form {
getFieldTypeMap() {
let fieldTypeMap = new Map();
fieldTypeMap.set('formId', 'String');
fieldTypeMap.set('formName', 'String');
fieldTypeMap.set('dimension', 'Integer');
return fieldTypeMap;
}
getClassName() {
return 't_form';
}
getPrimaryKeyList() {
let primaryKeyList = [];
primaryKeyList.push('formId');
return primaryKeyList;
}
getIndexList() {
let indexList = [];
return indexList;
}
getEncryptedFieldList() {
let encryptedFieldList = [];
return encryptedFieldList;
}
// set and get
setFormId(formId) {this.formId = formId;}
getFormId() {return this.formId;}
setFormName(formName) {this.formName = formName;}
getFormName() {return this.formName;}
setDimension(dimension) {this.dimension = dimension;}
getDimension() {return this.dimension;}
}
module.exports = {t_form}
4.1.3 CloudDBZoneWrapper操作云数据库,这里主要列举构造函数和增加方法内容:
import * as clouddb from '@agconnect/database-server';
import { t_form as FormBean } from './models/t_form';
import * as agconnect from '@agconnect/common-server';
const ZONE_NAME = "widgetCard";
export class CloudDBZoneWrapper {
logger;
cloudDbZone;
constructor(credential, logger) {
this.logger = logger;
try {
// 初始化AGCClient
let agcClient;
try {
agcClient = agconnect.AGCClient.getInstance();
} catch {
agconnect.AGCClient.initialize(credential);
agcClient = agconnect.AGCClient.getInstance();
}
// 初始化AGConnectCloudDB实例
let cloudDbInstance;
try {
cloudDbInstance = clouddb.AGConnectCloudDB.getInstance(agcClient);
} catch {
clouddb.AGConnectCloudDB.initialize(agcClient);
cloudDbInstance = clouddb.AGConnectCloudDB.getInstance(agcClient);
}
// 创建CloudDBZoneConfig配置对象,并设置云侧CloudDB zone名称,打开Cloud DB zone实例
const cloudDBZoneConfig = new clouddb.CloudDBZoneConfig(ZONE_NAME);
this.cloudDbZone = cloudDbInstance.openCloudDBZone(cloudDBZoneConfig);
} catch (err) {
logger.error("xx [form-func]CloudDBZoneWrapper init CloudDBZoneWrapper error: " + err);
}
}
async insert(addForm) {
if (!this.cloudDbZone) {
this.logger.error("xx [form-func]CloudDBZoneWrapper->insert CloudDBClient is null, try re-initialize it");
}
try {
let res = await this.cloudDbZone.executeUpsert(addForm);
this.logger.info("xx [form-func]CloudDBZoneWrapper->insert Insert " + res + " records success");
} catch (error) {
this.logger.error("xx [form-func]CloudDBZoneWrapper->insert executeInsert addressRecords failed " + error);
}
}
}
4.1.4 新增卡片函数form-insert,关键代码如下:
import { CloudDBZoneWrapper } from '../clouddb/CloudDBZoneWrapper.js';
import * as Utils from '../utils/Utils.js';
export const myHandler = async function (event, context, callback, logger) {
const credential = Utils.getCredential(context, logger);
try {
const cloudDBZoneWrapper = new CloudDBZoneWrapper(credential, logger);
let formObj = cloudDBZoneWrapper.getForm(event);
await cloudDBZoneWrapper.insert(formObj);
callback({
ret: { code: 0, desc: "SUCCESS" },
});
} catch (err) {
logger.error("xx [form-func]insert func error:" + err.message + " stack:" + err.stack);
callback({
ret: { code: -1, desc: "ERROR" },
});
}
};
4.1.5 卡片云函数主入口,关键代码如下:
let myHandler = async function (event, context, callback, logger) {
let operation;
let params;
logger.info("xx enter form func with operation " + event.operation);
operation = event.body ? JSON.parse(event.body).operation : event.operation;
params = event.body ? JSON.parse(event.body).params : event.params;
switch (operation) {
case "query":
query.myHandler(params, context, callback, logger);
break;
case "queryById":
queryById.myHandler(params, context, callback, logger);
break;
case "insert":
insert.myHandler(params, context, callback, logger);
break;
case "update":
update.myHandler(params, context, callback, logger);
break;
case "delete":
deleteByObj.myHandler(params, context, callback, logger);
break;
default:
callback({
ret: { code: -1, desc: "no such function" },
});
}
};
module.exports.myHandler = myHandler;
4.2 成绩云函数创建
4.2.1 展开CloudProgram -> cloudfunctions 右击cloudfunctions目录,创建 -> Cloud Function 输入Cloud Function Name为score-func,点击确认, 成绩云函数里包含了增删改查操作,所以在score-func下,创建不同的文件夹来区分,目录结构如下:
4.2.2 成绩表云数据库操作与卡片操作一样,这里就不在重复了,可以参考一下上面卡片操作方法就可以。
5. 云函数本地与远程调试
5.1 Run模式启动调试
5.1.1 右击“cloudfunctions”目录,选择“Run Cloud Functions”。
5.1.2 查看“Run”面板。如果出现“Cloud Functions loaded successfully”,表示所有函数已成功加载到本地运行的HTTP Server中,并生成对应的POST URL。
5.1.3 在菜单栏选择“Tools > CloudDev > Cloud Functions Requestor”,使用Cloud Functions Requestor触发函数调用。
5.1.4 在弹出的“Cloud Functions Requestor”面板,填写触发事件参数。
5.1.5 点击“Save”,可保存当前触发事件。
5.2 Debug模式启动调试
5.2.1 右击“cloudfunctions”目录,选择“Run Cloud Functions”。
5.2.2 查看Console面板。如果出现“Cloud Functions loaded successfully”,表示函数成功加载到本地运行的HTTP Server中,并生成对应的POST URL。
5.2.3 如需设置断点调试,在函数代码中选定要设置断点的有效代码行,在行号后单击鼠标左键设置断点,设置断点后,调试能够在断点处中断,并高亮显示该行。
5.2.4 在菜单栏选择“Tools > CloudDev > Cloud Functions Requestor”,使用Cloud Functions Requestor触发函数调用。
5.2.5 在弹出的“Cloud Functions Requestor”面板,填写触发事件参数。
5.2.6 点击“Save”,可保存当前触发事件。
5.3 自定义Run/Debug配置
5.3.1 在菜单栏选择“Run > Edit Configurations”。
5.3.2 在“Run/Debug Configurations”窗口,点击+,选择“Cloud Functions”,新增一个Run/Debug配置。
5.3.3 自定义Run/Debug配置,完成后点击“OK”。
· Name:Run/Debug配置的名称,如“functions-custom1”。
· Server Port:HTTP服务端监听端口。默认为“18090”,自定义端口号建议大于1024。勾选“Auto increment”表示如当前端口被占用则端口号自动加“1”。
· Environment variables:函数运行的环境变量,为key-value形式。
点击“Edit environment variables”按钮,在“Environment Variables”弹窗中点击“+”添加一个环境变量,然后点击“OK”。添加成功后,您便可以将变量配置信息传入到函数执行环境中,用于函数运行时读取。
5.3.4 选择刚刚自定义的Run/Debug配置,分别点击Run或Debug。后续调试步骤与默认配置下的调试步骤一致,请分别参见Run模式启动调试或Debug模式启动调试。
5.4 测试
5.4.1 实现云函数调用云数据库,需要您先部署云工程,云端才会有相关数据及环境变量。同时,云函数为访问云数据库使用了“PROJECT_CREDENTIAL”环境变量,部署函数到AGC云端时,云端会自动配置好“PROJECT_CREDENTIAL”以运行环境变量。但在本地调试函数时,需要您手动将“PROJECT_CREDENTIAL”环境变量添加到Run/Debug配置中。否则,函数调试代码执行会因获取不到“PROJECT_CREDENTIAL”环境变量而中断。
5.4.2 从AGC获取的“PROJECT_CREDENTIAL”环境变量添加到调试配置中。您也可以添加您需要的其他环境变量。
5.4.3 添加完环境变量后,启动函数,再点击Trigger, 就可以看到成功返回数据了。
6. 代码讲解
6.1 云函数调用公共类
DatabaseUtils.ets云函数操作类部分代码如下:
export class DatabaseUtils {
async callWithParams(context, trigger, operation, params) {
await getAGConnect(context);
let body = {
"operation": operation,
"params": params
}
try {
let functionCallable = agconnect.function().wrap(trigger);
let functionResult = await functionCallable.call(body);
return functionResult.getValue();
// return functionResult.getValue().result;
}
catch (err) {
return {
"ret": {"code": -1, "desc": "ERROR"}
}
}
}
async invoke(context: any, trigger?: string, operation?: string, params?: object) {
console.info(CommonConstants.DATABASE_TAG, 'xx invoke params: '+JSON.stringify(params))
return await this.callWithParams(context, trigger, operation, params);
}
/**
* 插入卡片数据。
*
* @param{Form}Form表单实体。
* @param{DataRdb.RdbStore}RDB存储RDB数据库。
* @return返回操作信息。
*/
async insertForm(context: any, form: Form) {
let res = await this.invoke(context, Triggers.FormFunc, RequestType.Insert, form);
console.info(CommonConstants.DATABASE_TAG, 'xx insertForm result: ' + JSON.stringify(res));
}
......
}
6.2 卡片Ability调用公共类
EntryFormAbility.ets卡片生命周期代码如下:
onAddForm(want) {
// 获取卡片ID:ohos.extra.param.key.form_identity
let formId: string = want.parameters[CommonConstants.FORM_PARAM_IDENTITY_KEY] as string;
// 获取卡片名称:ohos.extra.param.key.form_name
let formName: string = want.parameters[CommonConstants.FORM_PARAM_NAME_KEY] as string;
// 获取卡片规格:ohos.extra.param.key.form_dimension
let dimensionFlag: number = want.parameters[CommonConstants.FORM_PARAM_DIMENSION_KEY] as number;
// 卡片信息
let form: Form = new Form();
form.formId = formId;
form.formName = formName;
form.dimension = dimensionFlag;
// 保存卡片信息到数据库
DatabaseUtils.insertForm(this.context, form);
// 获取最优成绩
getScoreById(this.context, dimensionFlag, formId);
// 每五分钟刷新一次
formProvider.setFormNextRefreshTime(formId, CommonConstants.FORM_NEXT_REFRESH_TIME, (error, data) => {
if (error) {
console.error(CommonConstants.ENTRY_FORM_ABILITY_TAG, 'xx onAddForm 更新卡片失败:' + JSON.stringify(error))
} else {
console.info(CommonConstants.ENTRY_FORM_ABILITY_TAG, 'xx onAddForm 更新卡片成功')
}
});
// 返回初始化卡片数据
let formData: FormData = new FormData();
formData.formId = formId;
formData.bestScore = 0;
formData.matrixNum = '1x1';
formData.totalBestScore = 0;
return formBindingData.createFormBindingData(formData);
}
6.3 主界面调用公共类
@Entry
@Component
struct Index {
@State scoreDataList: Array<FormData> = []
aboutToAppear() {
// 请求通知栏权限
this.requestNotification();
// 更新卡片信息
DatabaseUtils.updateForms(getContext(this));
// 获取成绩历史记录
this.getScoreListData()
}
onPageShow() {
// 更新卡片信息
DatabaseUtils.updateForms(getContext(this));
// 获取成绩历史记录
this.getScoreListData()
}
// 获取成绩历史数据
getScoreListData() {
DatabaseUtils.getScoreListData(getContext(this))
.then((res) => {
this.scoreDataList = res;
// 发送通知
NotificationUtils.sendNotifications(this.scoreDataList[0].totalBestScore);
}).catch((error) => {
console.error(CommonConstants.MAIN_PAGE_TAG, 'xx aboutToAppear or onPageShow getScoreListData error ' + JSON.stringify(error));
});
}
build() {...}
}
7. 总结
通过把之前小游戏元服务-关系型数据库修改为使用Serverless云函数、云数据库,学习到不少知识,开始时不懂得怎么使用云函数调用云数据库,一边参考官方商城模板,一边测试,到使用到这个小游戏上, 总结这个项目用到以下知识点:
1. 使用notification发布通知。
2. 使用端云一体化开发、开发云函数、开发云数据库。
3. 使用FormExtensionAbility创建、更新、删除元服务卡片。
备注:资源文件是我在学习云函数调用云数据库写的一个简单实例,有云函数调用云数据库需求的小伙伴可以下载下来参考一下。
前排学习云开发模式
好长啊~😉😉
前前后后弄了一天😅
使用云数据库对网络的要求高吗
还是要网络好些才行,不然容易请求超时。
赞赞赞,学习一下
互相学习,共同进步
我觉得使用云数据库最大的好处就是能随时随地更新游戏进度。
对的,同时可以开发H5版本,Android版本,IOS版本,小程序版本,都可以通过云函数调用云数据库,这样云数据库上的数据就都共享了。
很详细的讲解
华为在上云这块可以说做的非常好了
是的,把Serverless玩6,前端不用为部署服务器,安装数据库而花更多时间在后端部署上。
云函数使用的云数据库实体类,可以按照下面的图片导出,不用手工编写代码。