游戲萬能卡片- Serverless—端云一體化開發
1、前言
舒爾特方格游戲-關系型數據庫版本,請移步到上一篇帖子查看,本篇帖子內容還是舒爾特方格游戲,只是換了后宮_云數據庫,此項目是用最新版DevEco Studio 3.1 Release并創建端云一體開發,由于目前此版本不支持直接調用云數據庫,不過可以通過云函數調用云數據庫,也就是在服務卡片業務邏輯里通過調用云函數來完成游戲數據保存到云數據庫,開發工具支持本地函數調用測試,大大方便了開發,此貼重點講解云函數和云數據庫開發,本地和遠端調用,從而進一步學習Serverless知識,舒爾特方格游戲效果圖如下:
2、知識點
為豐富HarmonyOS對云端開發的支持、實現HarmonyOS生態端云聯動,DevEco Studio推出了云開發功能,開發者在創建工程時選擇云開發模板,即可在DevEco Studio內同時完成HarmonyOS應用/服務的端側與云側開發,體驗端云一體化協同開發。
相比于傳統開發模式,云開發模式具備成本低、效率高、門檻低等優勢,具體區別見下表。
區別點 | 傳統開發模式 | 云開發模式 |
開發工具 | 端側與云側各需一套開發工具,云側需自建服務器,工具成本高。 | DevEco Studio一套開發工具即可支撐端側與云側同時開發,無需搭建服務器,工具成本低。 |
開發人員 | 端側與云側要求不同的開發語言,技能要求高。需多人投入,且開發人員之間需持續、準確溝通,人力與溝通成本高、效率低。 | 依托AppGallery Connect(以下簡稱AGC)Serverless云服務開放的接口,端側開發人員也能輕松開發云側代碼,大大降低開發門檻。開發人員數量少,降低人力成本,提高溝通效率。 |
運維 | 需自行構建運營與運維能力,成本高、負擔重。 | 直接接入AGC Serverless云服務,實現免運維,無運維成本或資源浪費。 |
(1)開發流程
HarmonyOS應用端云一體化開發流程如下圖所示。
(2)創建端云一體化開發工程
(3)開發云工程
(4)部署云工程
(5)小結
了解這些端云一體化開發知識點后,下面圍繞舒爾特方格游戲,在云數據庫里設計卡片表結構和成績表結構,然后再編寫相關云函數,并在本地測試通過后,再測試遠程,最后在元服務業務邏輯調用云函數。
3、云數據庫開發講解
(1)objecttype創建
展開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": [...]
}
展開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": [...]
}
(2)dataentry創建
展開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
}
]
}
展開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)小結
其實dataentry文件可以不創建,這里對兩個表都初始化了一條數據,是方便下面的調用使用,云數據庫就是定義好表結構、權限配置就可以,數據的添加、修改、刪除、查詢都可以通過云函數來完成。
4、云函數開發講解
(1)卡片云函數創建
展開CloudProgram -> cloudfunctions 右擊cloudfunctions目錄,創建 -> Cloud Function 輸入Cloud Function Name為form-func,點擊確認, 卡片云函數里包含了增刪改查操作,所以在form-func下,創建不同的文件夾來區分,目錄結構如下:
首先說一下與云數據庫交互文件,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}
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);
}
}
}
新增卡片函數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" },
});
}
};
卡片云函數主入口,關鍵代碼如下:
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;
(2)成績云函數創建
展開CloudProgram -> cloudfunctions 右擊cloudfunctions目錄,創建 -> Cloud Function 輸入Cloud Function Name為score-func,點擊確認, 成績云函數里包含了增刪改查操作,所以在score-func下,創建不同的文件夾來區分,目錄結構如下:
成績表云數據庫操作與卡片操作一樣,這里就不在重復了,可以參考一下上面卡片操作方法就可以。
5、云函數本地與遠程調試
(1)Run模式啟動調試
右擊“cloudfunctions”目錄,選擇“Run Cloud Functions”。
查看“Run”面板。如果出現“Cloud Functions loaded successfully”,表示所有函數已成功加載到本地運行的HTTP Server中,并生成對應的POST URL。
在菜單欄選擇“Tools > CloudDev > Cloud Functions Requestor”,使用Cloud Functions Requestor觸發函數調用。
在彈出的“Cloud Functions Requestor”面板,填寫觸發事件參數。
點擊“Save”,可保存當前觸發事件。
(2)Debug模式啟動調試
右擊“cloudfunctions”目錄,選擇“Run Cloud Functions”。
查看Console面板。如果出現“Cloud Functions loaded successfully”,表示函數成功加載到本地運行的HTTP Server中,并生成對應的POST URL。
如需設置斷點調試,在函數代碼中選定要設置斷點的有效代碼行,在行號后單擊鼠標左鍵設置斷點,設置斷點后,調試能夠在斷點處中斷,并高亮顯示該行。
在菜單欄選擇“Tools > CloudDev > Cloud Functions Requestor”,使用Cloud Functions Requestor觸發函數調用。
在彈出的“Cloud Functions Requestor”面板,填寫觸發事件參數。
點擊“Save”,可保存當前觸發事件。
(3)自定義Run/Debug配置
在菜單欄選擇“Run > Edit Configurations”。
在“Run/Debug Configurations”窗口,點擊+,選擇“Cloud Functions”,新增一個Run/Debug配置。
自定義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”。添加成功后,您便可以將變量配置信息傳入到函數執行環境中,用于函數運行時讀取。
選擇剛剛自定義的Run/Debug配置,分別點擊Run或Debug。后續調試步驟與默認配置下的調試步驟一致,請分別參見Run模式啟動調試或Debug模式啟動調試。
(4)測試
實現云函數調用云數據庫,需要您先部署云工程,云端才會有相關數據及環境變量。同時,云函數為訪問云數據庫使用了“PROJECT_CREDENTIAL”環境變量,部署函數到AGC云端時,云端會自動配置好“PROJECT_CREDENTIAL”以運行環境變量。但在本地調試函數時,需要您手動將“PROJECT_CREDENTIAL”環境變量添加到Run/Debug配置中。否則,函數調試代碼執行會因獲取不到“PROJECT_CREDENTIAL”環境變量而中斷。
從AGC獲取的“PROJECT_CREDENTIAL”環境變量添加到調試配置中。您也可以添加您需要的其他環境變量。
添加完環境變量后,啟動函數,再點擊Trigger, 就可以看到成功返回數據了。
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));
}
......
}
(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);
}
(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云函數、云數據庫,學習到不少知識,開始時不懂得怎么使用云函數調用云數據庫,一邊參考官方商城模板,一邊測試,到使用到這個小游戲上, 總結這個項目用到以下知識點:
- 使用notification發布通知。
- 使用端云一體化開發、開發云函數、開發云數據庫。
- 使用FormExtensionAbility創建、更新、刪除元服務卡片。
備注:資源文件是我在學習云函數調用云數據庫寫的一個簡單實例,有云函數調用云數據庫需求的小伙伴可以下載下來參考一下。
文章相關附件可以點擊下面的原文鏈接前往下載:
https://ost.51cto.com/resource/2803