端云一体化,极简开发数独闯关游戏元服务

企业动态
数独闯关游戏元服务,支持2*4历史闯关记录卡片、4*4数独闯关游戏卡片,无需进入元服务,可在桌面上开始游戏,同步刷新记录。

前言

数独闯关游戏元服务,支持2*4历史闯关记录卡片、4*4数独闯关游戏卡片,无需进入元服务,可在桌面上开始游戏,同步刷新记录。

端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

1 基本概念

  • 元服务(原名原子化服务)是一种基于HarmonyOS API的全新服务提供方式,以HarmonyOS万能卡片等多种呈现形态,向用户提供更轻量化的服务。具有即用即走、信息外显、服务直达的特性。
  • 万能卡片(简称卡片)是一种界面展示形式,可以将应用的重要信息或操作前置到卡片,以达到服务直达、减少体验层级的目的。
  • HUAWEI DevEco Studio是华为基于IntelljJ IDEA Community开源版本打造的一款面向全场景多设备,提供一站式的分布式应用开发平台,支持分布式多端开发、分布式多端调测、多端模拟仿真,为运行在HarmonyOS/OpenHarmony系统上的应用/服务提供一站式开发平台。
  • HarmonyOS SDK是集成在DevEco Studio中,为应用/服务开发提供所需的一系列系统开放能力,如框架、Ability、分布式服务、安全、方舟编译器、HMS Core、基础软件服务、硬件服务等,其包含开发者可以使用的API以及调试编译等基础的工具链。
  • ArkTS语言是华为自研的开发语言,基于TypeScript(简称TS)基础上匹配了ArkUI框架,扩展了声明式UI、状态管理等相应的能力。
  • ArkUI框架是一套构建分布式应用界面的声明式UI开发框架,其使用极简的UI信息语法、丰富的UI组件、以及实时界面预览工具,帮助开发者提升HarmonyOS应用界面开发效率。
  • ArkCompiler是华为自研的统一编程平台,包括编译器、工具链、运行时等关键部件,支持高级语言在多种芯片平台的编译与运行。
  • AppGallery Connect(简称AGC)致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。
  • 端云一体化是为丰富HarmonyOS对云端开发的支持、实现HarmonyOS生态端云联动,DevEco Studio推出了云开发功能,开发者在创建工程时选择云开发模板,即可在DevEco Studio内同时完成HarmonyOS应用/服务的端侧与云侧开发。

2 约定

  • DevEco Studio 3.1.1 Release
  • HarmonyOS SDK API 9
  • ArkTS语言
  • Stage模型
  • 端云一体化元服务工程
  • 确保已使用已实名认证的华为开发者账号登录DevEco Studio。

3 创建端云一体化元服务工程

3.1 打开创建工程向导

选择以下一种方式,打开工程创建向导界面

  • 当前未打开任何工程,单击DevEco Studio欢迎页中“Create Project”创建新工程。
  • 当前已打开工程,单击菜单栏“File > New > Create Project”创建新工程。

3.2 选择工程模板

在Choose Your Ability Template向导页,单击“Atomic Service”切换工程类型,选择云开发模板,单击“Next”进入下一步。

端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

3.3 配置工程信息

在工程配置界面,配置工程的基本信息。单击“Next”按钮进入关联云开发资源界面。

端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

参数

说明

Project name

工程的名称,由大小写字母、数字和下划线组成。

Bundle name

软件包名称,需保证唯一,由字母、数字、下划线和符合“.”组成,以字母开头,长度为7-127个字符。

Save location

工程文件本地存储路径,由大小写字母、数字和下划线等组成,不能包含中文字符。

Compile SDK

应用的目标API Version,在编译构建时,DevEco Studio会根据指定的Compile API版本进行编译打包。使用云开发模板,版本不低于9。

Model

应用支持的模式,云开发模板仅支持Stage模式。

Language

开发语言。Stage模式仅支持使用ArkTS语言开发。

Compatible SDK

兼容的最低API Version。云开发模板,版本不低于9。

Device Type

工程模板支持的设备类型,当前API 9仅支持手机设备。

Enable CloudDev

是否启用云开发,默认启用且无法更改。

3.4 关联云开发资源

为工程关联云开发所需的资源,即在DevEco Studio中选择您的华为开发者账号加入的开发者团队,将该团队在AGC的同包名应用关联到当前工程,具体操作如下:

  • 若尚未登录DevEco Studio,单击“Sign in”,拉起浏览器在弹出的账号登录页面,使用已实名认证的华为开发者账号完成登录。

    登录成功后,在授权界面单击“允许”按钮为DevEco Studio授权,界面将展示账号昵称。
  • 单击“Team”下拉框,选择开发团队。选中团队后,系统根据工程包名自动查询团队下的同包名应用。若为首次创建且团队下未创建同包名的应用,则提示需要在AGC平台创建应用。

    单击“AppGallery Connect”打开AGC应用创建向导,填写应用信息,单击“确认”按钮创建应用。

    完成以上操作后,DevEco Studio即可获取到同包名应用对应的项目信息。

3.5 工程初始化配置

成功创建工程并关联云开发资源后,DevEco Studio会为工程自动执行一些初始化配置,并开通云开发相关服务:认证服务、云函数、云数据库、云托管、API网关、云存储。

端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

  • 端侧工程自动集成AGC SDK,包括AGC SDK 配置文件entry/src/main/resources/rawfile/agconnect-services.json 和在entry/oh-package.json5配置文件中引入的AGC相关云服务最新版本SDK。
  • 端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

  • 云侧工程自动集成云数据库最新版本Node.js Server SDK。
  • 端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

4 端云协同元服务工程介绍

端云一体化元服务开发工程目录分为三个子工程:元服务开发工程(Application)、云开发工程(CloudProgram)、端侧公共库(External Libraries)。

端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

4.1 元服务开发工程(Application)

元服务开发工程主要用于开发应用端侧的业务代码,元服务开发工程目录结构如下:

端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

- Application
    - AppScope
        app.json5    // 应用的全局配置信息
    - entry           // 应用/服务模块,编译构建生成一个HAP
        oh_modules   // 用于存放三方库依赖信息
        - src/main
            - ets    // 用于存放ArkTS源码
            - resources    // 用于存放应用/服务所用到的资源文件
            module.json5    // Stage模型配置文件
        build-profile.json5    // 当前模块信息、编译信息配置项
        hvigorfile.ts          // 模块级编译构建任务脚本
        oh-package.json5        // 配置三方包声明的入口及包名
    build-profile.json5    // 应用配置信息,包括签名、产品配置等
    hvigorfile.ts          // 应用级编译构建任务脚本

4.2 云开发工程(CloudProgram)

云开发工程中开发者可以为应用开发云函数和云数据库服务资源,云开发工程目录结构如下:

端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

- CloudProgram
    - clouddb    // 云数据库工程目录
        dataentry    // 用于存放数据条目文件
        objecttype    // 用于存放对象类型文件
        db-config.json    // 模块配置文件
    - cloudfunctions    // 云函数工程目录
        cloudFunctionName // 云函数名称
          node_modules    // 包含所有三方依赖
          cloud-config.json    // 云开发工程配置文件
          package.json    // 定义了TypeScript公共依赖

5 云函数开发指南

5.1 创建函数

在云端工程(CloudProgram)中可以创建函数、编写函数业务代码、为函数配置调用触发器。

    1.单击“cloudfunctions”目录,选择“New > Cloud Function”创建云函数。

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    2.输入函数名称,单击“OK”按钮DevEco Studio自动生成函数目录。函数名称仅支持小写英文字母、数字、中划线(-),首字母必须为小写字母,结尾不能为中划线(-)。

    为当前工程创建生成数独9*9宫格二维数组算法,云函数名称为sudoku-algorithm 

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    3. 云函数目录结构。

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    - sudoku-algorithm
       node_modules    // 自动为该函数引入依赖包
       function-config.json    // 函数的配置文件,可配置触发器,通过触发器暴露的触发条件来实现函数调用。
       package.json            // 包含了当前函数的名称、版本等函数元数据。
       sudoku-algorithm.ts    // 函数入口文件

    4.云函数触发器

    云函数触发器在function-config.json文件中triggers属性中配置,当前支持HTTP触发器、CLOUDDB触发器、AUTH触发器、CLOUDSTORAGE触发器、CRON触发器五种。

    • HTTP触发器


      工程创建完成后默认自动在function-config.json文件生成HTTP触发器配置。函数部署到云端后会自动生成触发URL,开发者向URL发起HTTP请求时触发函数。

    {
        "type": "http",
        "properties": {
            "enableUrlDecode": true,
            "authFlag": "true",
            "authAlgor": "HDA-SYSTEM",
            "authType": "apigw-client"
        }
    }

    参数

    说明

    enableUrlDecode

    通过HTTP触发器触发函数,对于contentType为“application/x-www-form-urlencoded”的触发请求,是否使用URLDecoder对请求body进行解码再转发到函数中。

    - true:启用。

    - false:不启用。

    authFlag

    是否鉴权,默认为true。

    authAlgor

    鉴权算法,默认为HDA-SYSTEM。

    authType

    HTTP触发器的认证类型。

     - apigw-client:端侧网关认证,适用于来自APP客户端侧的函数调用。

     - cloudgw-client:云侧网关认证,适用于来自APP服务器侧的函数调用。

    5.2 开发云函数

    云函数的代码实现基于不同的语言运行环境可分为Node.js、Java、Python,还有一种比较特别运行环境为Custom Runtime(自定义运行环境)。本工程的语言运行环境为Node.js。

    1. 云函数的入口方法:
      module.exports.myHandler = function(event, context, callback, logger)
    • myHandler:入口方法名称。
    • event:调用方传递的事件对象,JSON格式。
    • context:函数运行时上下文对象,封装了日志接口、回调接口、环境变量env对象等。
    • callback:事件处理结果。
    • logger:记录日志。开发者在代码中使用logger接口记录日志,当前支持四种级别。
      • logger.debug()
      • logger.error()
      • logger.warn()
      • logger.info()

    函数必须通过显示调用callback(object)将事件处理结果返回给AGC,结果可以是任意对象,但必须与JSON.stringify兼容,AGC会将结果转换成JSON字符串,返回给调用方。callback执行完成后,函数即执行结束。

    2. 为云函数添加返回内容

    let myHandler = async function (event, context, callback, logger) {
      logger.info(event);
    
      // do something here
      callback({
        code: 0,
        desc: "Success.",
        data: "请求成功!"
      });
    };
    export { myHandler };

    3.调试云函数

    函数开发过程中,开发者可在本地进行调试,或者将函数部署到AGC云端后,在本地触发调用云端函数。当前本地调试支持Run和Debug两种模式,Debug模式支持使用断点来追踪函数的运行情况。

    1. 本地云函数调试,单击"cloudfunctions > Run/Debug Cloud Function"运行/调试云函数。

      端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    2. 查看Run面板,若出现“Cloud Functions loaded successfully”,标识云函数启动成功(云函数启动/调试将部署cloudfunctions中所有的云函数),并生成对应的POST URL。

      端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    3. 在菜单栏选择“Tools > CloudDev > Cloud Functions Requestor”触发云函数调用。

      端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    4. 在弹出的云函数调用界面填写触发事件参数。

      端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

      1. Environment:选择函数调用环境,Local表示本地调用,Remote表示远程调用(需要先将函数部署到AGC云端)。
      2. Cloud Function:选择需要触发的云函数。
      3. Event:输入事件参数,内容为JSON格式请求体数据。
    • 单击Trigger按钮,触发执行云函数,执行结果展示在Result框内,Run面板同时打印运行日志。

      端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    4. 部署云函数

    完成函数代码开发后,开发者可将函数部署到AGC控制台,支持单个部署和批量部署。

    • 右键单击需要部署的函数目录,选择“Deploy Function”。
    • 底部状态栏右侧将显示函数打包与部署进度,直至出现“Deploy successfully”消息表示函数部署成功。

      端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    • 登录AGC控制台,进入当前项目的云函数服务菜单,可查看开发者部署的函数。

      端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    • 远程函数测试
      在“Cloud Functions Reuestor”面板中,更改Environment为Remote远程调用,单击“Trigger”按钮,在Result中显示返回结果。

      端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    5.3 查看触发器标识

    当开发者创建的函数或函数别名中创建一个HTTP类型的触发器后,在应用客户端调用函数时需要传入HTTP触发器的标识,查询方法如下:在函数的触发器页面点击“HTTPTrigger”触发器,查看“触发URL”的后缀,获取触发器标识,格式为“函数名-版本号”。

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    5.4 调用云函数

    应用集成云函数SDK后,可以在应用内直接通过SDK API调用AGC中的云函数,云函数SDK与AGC的函数调用基于HTTPS的安全访问。

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    • 在端侧应用(Application)中“entry > src/main/ets > services”目录创建SudokuAlgorithmFunction.ts文件,编写调用云函数方法。
    import agconnect from '@hw-agconnect/api-ohos';
    import "@hw-agconnect/function-ohos";
    import { Constants } from '../common/Constants';
    import { Log } from '../common/Log';
    import { getAGConnect } from './AgcConfig';
    
    export function getSudokuPuzzle(context: any) {
      return new Promise((resolve, reject) => {
        getAGConnect(context);
        // 调用wrap方法设置函数,在方法中传入触发器标识,返回得到可执行云函数对象
        let functionCallable = agconnect.function().wrap("sudoku-algorithm-$latest");
        // 调用call方法运行云函数,若函数有入参,可以将参数转化为JSON对象或JSON字符串传入,若无参则不传
        functionCallable.call().then((ret: any) => {
          // 可调用getValue方法获取函数的返回值
          let result = ret.getValue();
          Log.info(Constants.LOG_TAG_NAME, `sudoku-algorithm called,  result: ${JSON.stringify(result)}`);
          resolve(result);
        }).catch((err: any) => {
          Log.error(Constants.LOG_TAG_NAME, `sudoku-algorithm failed, cause: ${JSON.stringify(err)}`);
        })
      });
    }
    • 在端侧应用(Application)中“entry > src/main/ets > pages > Index.ets”文件中增加请求云函数方法。
    Button('请求自定义云函数')
      .fontSize(16)
      .onClick(() => {
        getSudokuPuzzle(getContext(this)).then((ret) => {
          Log.info(Constants.LOG_TAG_NAME, `单击按钮调用云函数返回结果: ${JSON.stringify(ret)}`)
        })
      })
    • 运行端侧(Application)应用程序,单击“请求自定义云函数”按钮,在Log控制台过滤日志查看返回结果。

      端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    5.5 实现云函数返回9*9宫格二位数组的数独谜题和所有解

    使用回溯算法填充数独谜题,并随机移除一些数字将其作为数独谜题,然后求解指定数独谜题的所有解。

    let myHandler = async function (event, context, callback, logger) {
      // 传递的关卡值作为需要填充的空格数
      let body = event.body;
      let params = JSON.parse(body);
      let levelNum = params.level;
      // 创建一个9*9的空白数独谜题
      let sudoku = Array.from({ length: 9 }, () => Array(9).fill(0));
      // 使用回溯算法填充数独谜题
      solve_sudoku(sudoku);
      // 随机移除一些数字,生成数独谜题
      remove_number(sudoku, levelNum);
      let solutions = answer_sudoku(sudoku);
      let sudokuPuzzle = {
        "original": sudoku,
        "answer": solutions
      }
      callback({
        code: 0,
        desc: "Success.",
        data: JSON.stringify(sudokuPuzzle)
      });
    };
    
    function solve_sudoku(sudoku){...}
    function remove_number(sudoku, level){...}
    function answer_sudoku(sudoku){...}
    
    export { myHandler };

    6 认证服务-邮箱认证

    当前AGC认证服务为HarmonyOS应用/服务提供的登录认证方式有手机、邮箱和关联账号三种方式。本工程使用“邮箱+验证码”的方式作为应用的登录入口。

    6.1 前提条件

    • 需要在AGC控制台开通认证服务(工程创建时默认开通),并在“认证方式”页签中启用“邮箱地址”。
    • 需要在应用中集成SDK(工程创建时默认开通)。

    6.2 扩展邮箱认证

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    • 调用AGConnectAuth.requestEmailVerifyCode申请验证码,在entry/src/main/ets/services/Auth.ts认证工具类中添加邮箱验证码获取方法。
    // 申请邮箱验证码
    public requestEmailVerifyCode(email: string) {
        let verifyCodeSettings = new VerifyCodeSettingBuilder()
            .setAction(VerifyCodeAction.REGISTER_LOGIN)
            .setLang('zh_CN')
            .setSendInterval(60)
            .build();
        this.agc.auth().requestEmailVerifyCode(email, verifyCodeSettings)
            .then((ret) => {
                Log.info(TAG, JSON.stringify({ "Verify Code Result: ": ret }));
            }).catch((error) => {
            Log.error(TAG, "Error: " + JSON.stringify(error));
        });
    }
    • 调用EmailUserBuilder生成EmailUser,然后调用AGConnectAuth.createEmailUser注册用户。注册成功后,系统会自动登录,无需再次调用登录接口。也可以使用signIn登录接口,通过第三方认证来登录AGConnect平台,在entry/src/main/ets/services/Auth.ts认证工具类中添加邮箱账号注册用户方法。
    // 邮箱账号注册登录
    public async loginByEmail(email: string, verifyCode: string): Promise<AgUser> {
        return new Promise((resolve, reject) => {
            // 如果创建账户的时候没有设置过密码,则只能通过此接口进行登录
            let credential = EmailAuthProvider.credentialWithVerifyCode(email, verifyCode);
            // 登录接口,通过第三方认证来登录AGConnect平台
            this.agc.auth().signIn(credential).then(async (ret) => {
                Log.info(TAG, `User has signed in. User: ${JSON.stringify(ret)}`);
                let user = ret.getUser();
                let userExtra = await ret.getUser().getUserExtra();
                let loginRes = new AgUser(
                    user.getUid(),
                    user.getPhotoUrl(),
                    user.getPhone(),
                    user.getDisplayName(),
                    userExtra.getCreateTime(),
                    userExtra.getLastSignInTime())
    
                resolve(loginRes);
            }).catch((error) => {
                Log.error(TAG, "Error: ", error);
                reject(error);
            });
        });
    }

    6.3 构建邮箱登录页面

    通过容器组件Flex、Row、Column以及基础组件Text、Image、Button、Navigation、TextInput构建邮箱登录页面。

    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      Navigation() {
        Column() {
          Row({ space: Constants.LENGTH_5_PX }) {
            TextInput({ placeholder: '请输入邮箱账号..'})
              .type(InputType.Email)
              .layoutWeight(Constants.LENGTH_3_PX)
              .borderRadius(Constants.BORDER_RADIUS_4_PX)
              .maxLength(Constants.LENGTH_20_PX)
              .height(Constants.HEIGHT_40)
              .enabled(this.timer === 60)
              .onChange((val) => {
                this.email = val;
              })
          }
          .width(Constants.PERCENT_100)
          .justifyContent(FlexAlign.Center)
          .margin({ bottom: Constants.LENGTH_20_PX })
    
          Row({ space: Constants.LENGTH_5_PX }) {
            TextInput({ placeholder: '请输入验证码..', text: this.verificationCode })
              .layoutWeight(Constants.LENGTH_3_PX)
              .borderRadius(Constants.BORDER_RADIUS_4_PX)
              .maxLength(Constants.LENGTH_6_PX)
              .height(Constants.HEIGHT_40)
              .onChange((val) => {
                this.verificationCode = val;
              })
    
            Button(this.timer === 60 ? '获取验证码' : this.timer.toString(), {
              type: ButtonType.Normal
            })
              .backgroundColor('#f9fcfb')
              .layoutWeight(Constants.LENGTH_2_PX)
              .borderColor('#169cd5')
              .borderWidth(Constants.LENGTH_1_PX)
              .fontColor('#169cd5')
              .borderRadius(Constants.BORDER_RADIUS_4_PX)
              .height(Constants.HEIGHT_40)
              .enabled(this.validateEmailAddress(this.email) && this.timer === 60)
              .onClick(() => this.onGetCodeButtonClicked())
          }
          .width(Constants.PERCENT_100)
          .justifyContent(FlexAlign.Center)
          .margin({ bottom: Constants.LENGTH_20_PX })
    
          Button('登录', { type: ButtonType.Normal })
            .width(Constants.PERCENT_100)
            .borderRadius(Constants.BORDER_RADIUS_4_PX)
            .backgroundColor('#169cd5')
            .enabled(this.canAuthorize() && this.verificationCode.length > 5 && this.canLogin)
            .opacity(this.canLogin ? 1 : 0.5)
            .height(Constants.HEIGHT_40)
            .onClick(() => this.onAuthButtonClicked())
        }
        .width(Constants.PERCENT_90).height(Constants.HEIGHT_50)
        .justifyContent(FlexAlign.Center)
        .margin({ top: Constants.LENGTH_40_PX })
        .padding({
          right: Constants.LENGTH_15_PX,
          left: Constants.LENGTH_15_PX
        })
        .borderRadius(Constants.LENGTH_8_PX)
        .backgroundColor(0xFFFFFF)
      }
      .title(this.NavigationTitle())
      .titleMode(NavigationTitleMode.Full)
      .hideTitleBar(false)
      .hideToolBar(false)
    }
    .width(Constants.PERCENT_100).height(Constants.PERCENT_100)
    .backgroundColor(Constants.VIEW_BG_COLOR)

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    6.4 用户登录成功信息写入缓存

    调用自定义的登录接口实现登录,并使用首选项自定义工具接口将用户信息写入缓存。

    onAuthButtonClicked = () => {
      this.canLogin = false;
      this.agcAuth.loginByEmail(this.email, this.verificationCode).then((user) => {
        PreferencesUtil.putPreference(getContext(this), Constants.USER_AUTH_INFO, JSON.stringify(user));
        Log.info(Constants.LOG_TAG_NAME, `Logged in successfully. Data: ${JSON.stringify(user)}`);
        this.canLogin = true;
      }).catch((err) => {
        this.canLogin = true;
        Log.error(Constants.LOG_TAG_NAME, `Logged in failed. Cause: ${JSON.stringify(err)}`);
      })
    }

    6.5 登录成功后跳转设置昵称头像界面

    右键单击“entry > src/main/ets > pages”目录选择“New > Pages”新建Setting设置页面。

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    在页面中使用容器组件Grid实现头像选择(提供可选头像6个)和使用基础组件TextInput实现昵称设置。

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    头像昵称设置成功后,跳转到游戏主界面,点击“开始”按钮从云函数中获取数独谜题及对应的解,然后通过容器组件Grid和其子组件GridItem构建9*9宫格并使用ForEach渲染宫格的对应组件。

    // 获取数独谜题和解
    getSudoPuzzle = () => {
      let _this = this;
      getSudokuPuzzle(getContext(this), this.levelNum).then((ret: string) => {
        let result: SudokuPuzzle = JSON.parse(ret);
        _this.puzzles = result.original;
        _this.answers = result.answer;
      }).catch((err) => {
        Log.error(Constants.LOG_TAG_NAME, `cause: ${JSON.stringify(err)}`);
      })
    }
    
    Grid() {
      ForEach(this.puzzles, (item: Array<number>, i: number) => {
        ForEach(item, (temp: number, j: number) => {
          GridItem() {
            if (temp === 0) {
              TextInput()
                .type(InputType.Number)
                .maxLength(1)
                .backgroundColor(0xf47721)
                .caretColor(Color.White)
                .onChange((val) => {
                  let answer = this.puzzles;
                  answer[i][j] = parseInt(val);
                  this.userAnswer = answer;
                  Log.info(Constants.LOG_TAG_NAME, JSON.stringify(this.userAnswer));
                  if (val == "") {
                    this.userAnswer = [];
                  }
                })
            } else {
              Text(temp.toString())
                .fontSize(16)
            }
          }
          .borderWidth(1)
        }, (temp: number) => temp.toString())
      }, (item: Array<number>) => item.toString())
    }
    .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr')
    .rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr')
    .width(Constants.PERCENT_96)
    .height(400)
    .backgroundColor(0xFFFFFF)

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    7 创建闯关游戏万能卡片

    万能卡片(以下简称“卡片”)是一种界面展示形式,可以将应用的重要信息或操作前置到卡片,以达到服务直达、减少体验层级的目的。卡片常用于嵌入到其他应用(当前卡片使用方只支持系统应用,如桌面)中作为其界面显示的一部分,并支持拉起页面、发送消息等基础的交互功能。

    7.1 拉起页面的2*2卡片

    工程在创建初会自动创建2*2服务卡片,位于“entry > src/main/ets > widget > pages”目录。在WidgetCard.ets文件中编写服务卡片呈现内容及样式。

    Column() {
      Image($r("app.media.card_start"))
        .width(this.FULL_WIDTH_PERCENT)
        .height(this.FULL_HEIGHT_PERCENT)
        .objectFit(ImageFit.Cover)
    }
    .width(this.FULL_WIDTH_PERCENT)
    .height(this.FULL_HEIGHT_PERCENT)
    .onClick(() => {
      postCardAction(this, {
        "action": this.ACTION_TYPE,
        "abilityName": this.ABILITY_NAME,
        "params": {
          "message": this.MESSAGE
        }
      });
    })

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    ArkTS卡片提供了postCardAction()接口用于卡片内部和提供方应用间交互,当前支持router、message和call三种类型的事件,仅在卡片中可以调用。

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    接口定义:postCardAction(component: Object, action: Object): void

    • 接口参数说明

    参数名

    参数类型

    必填

    参数描述

    component

    Object


    当前自定义组件的实例,通常传入this。

    action

    Object


    action的具体描述详见下表。

    • action参数说明

    Key

    Value

    描述

    action

    string

    action的类型,支持三种预定义的类型:

     - router:跳转到提供方应用的指定UIAbility。

     - message:自定义消息。触发后会调用提供方FormExtensionAbility的onFormEvent()生命周期回调。

     - call:后台启动提供方应用。触发后会拉起提供方应用的指定UIAbility(仅支持launchType为singleton的UIAbility,即启动模式为单实例的UIAbility),但不会调度到前台。提供方应用需要具备后台运行权限ohos.permission.KEEP_BACKGROUND_RUNNING。

    bundleName

    string

    router/call类型时跳转的包名,可选。

    moduleName

    string

    router/call类型时跳转的模块名,可选。

    abilityName

    string

    router/call类型跳转的UIAbility名,必填。

    params

    Object

    当前action携带的额外参数,内容使用JSON格式的键值形式。call类型时需填入参数method,且类型需要为string类型,用于触发UIAbiltiy中对应的方法,必填。

    2*2万能卡片提供点击卡片进入元服务主界面。

    7.2 创建4*4闯关游戏卡片

    4*4服务卡片用于在桌面玩游戏,没关通关后需要通过message事件刷新卡片内容生成新的关卡。

    7.2.1 创建一个ArkTS卡片

    创建ArkTS卡片有两种方式:

    • 通过在”entry“目录右键单击“New > Service Widget”创建卡片。
    • 通过在”entry > src/main/ets > widget > pages“目录右键单击“New > ArkTS File”创建文件,并在卡片配置文件form_config.json中配置卡片信息。
      使用第二种方式创建卡片,在”entry > src/main/ets > widget > pages“目录右键单击“New > ArkTS File”创建GameCard.ets文件,接着打开"entry > src/main/resources > base > profile"目录下的form_config.json文件,配置名称为game的4*4卡片。
    {
      "name": "game",
      "description": "数独闯关游戏",
      "src": "./ets/widget/pages/GameCard.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDefault": false,
      "updateEnabled": false,
      "scheduledUpdateTime": "10:30",
      "updateDuration": 1,
      "defaultDimension": "4*4",
      "supportDimensions": [
        "4*4"
      ]
    }

    7.2.2 卡片配置文件

    卡片相关的配置主要包括FormExtensionAbility的配置和卡片的配置两部分:

    • 卡片需要在module.json5配置文件中的extensionAbilityes标签下,配置FormExtensionAbility相关信息。FormExtensionAbility需要填写metadata元素信息标签,其中键名为固定字符串”ohos.extension.form“,资源为卡片的具体配置信息的索引。
    {
      "module": {
        ...
        "extensionAbilities": [
          {
            "name": "EntryFormAbility",
            "srcEntrance": "./ets/entryformability/EntryFormAbility.ts",
            "label": "$string:EntryFormAbility_label",
            "description": "$string:EntryFormAbility_desc",
            "type": "form",
            "metadata": [
              {
                "name": "ohos.extension.form",
                "resource": "$profile:form_config"
              }
            ]
          }
        ]
      }
    }
    • 卡片的具体配置信息。在上述FormExtensionAbility的元信息("metadata"配置项)中,可以指定卡片具体配置信息的索引资源。如当resource指定为$profile:form_config时,会使用"entry > src/main/resources > base > profile"目录下的form_config.json作为卡片profile配置文件。

    属性名称

    含义

    数据类型

    是否可缺省

    name

    表示卡片的类名,字符串最大的长度为127字节。

    字符串


    description

    表示卡片的描述。取值可以时描述性内容,也可以是对描述内容的资源索引,以支持多语言。字符串最大长度为255字节。

     该属性将显示在卡片预览界面上,以便用户识别不同的卡片。

    字符串

    可缺省,缺省为空。

    src

    表示卡片对应的UI代码的完整路径。当为ArkTS卡片时,完整路径需要包含卡片文件的后缀,如:“./ets/widget/pages/WidgetCard.ets”。当为JS卡片式,完整路径无需包含卡片文件的后缀。

    字符串


    uiSyntax

    表示该卡片的类型,当前支持如下两种类型:

    - arkts:当前卡片为ArkTS卡片。

    - hml:当前卡片为JS卡片。

    字符串

    可缺省,缺省为hml

    window

    用于定义与显示窗口相关的配置。

    对象

    可缺省

    isDefault

    表示该卡片是否为默认卡片,每个UIAbility有且只有一个默认卡片。

    - true:默认卡片。

    - false:非默认卡片。

    布尔值


    colorMode

    表示卡片的主题样式,取值范围如下:

    - auto:自适应。

    - dark:深色主题。

    - light:浅色主题。

    字符串

    可缺省,缺省值为“auto”。

    supportDimensions

    表示卡片支持的外观规格,取值范围:

    - 1*2:表示1行2列的二宫格。

    - 2*2:表示卡片为2行2列的四宫格。

    - 2*4:表示2行4列的八宫格。

    - 4*4:表示4行4列的十六宫格。

    字符串数组


    defaultDimension

    表示卡片的默认外观规格,取值必须在该卡片supportDimensions配置的列表中。

    字符串


    updateEnabled

    表示卡片是否支持周期性刷新(包含定时刷新和定点刷新),取值范围:

    - true:表示支持周期性刷新,可以在定时刷新(updateDuration)和定点刷新(scheduledUpdateTime)两种方式任选其一,当两者同时配置时,定时刷新优先生效。

    - false:表示不支持周期性刷新。

    布尔类型


    scheduledUpdateTime

    表示卡片的定点刷新的时刻,采用24小时制,精确到分钟。

    说明:updateDuration参数优先级高于scheduledUpdateTime,两者同时配置时,以updateDuration配置的刷新时间为准。

    字符串

    可缺省,缺省时不进行定点刷新。

    updateDuration

    表示卡片定时刷新的更新周期,单位为30分钟,取值为自然数。当取值为0时,表示该参数不生效。当取值为正整数N时,表示刷新周期为30*N分钟。

    说明:updateDuration参数优先级高于scheduledUpdateTime,两者同时配置时,以updateDuration配置的刷新时间为准。

    数值

    可缺省,缺省值为“0”。

    formConfigAbility

    表示卡片的配置跳转链接,采用URI格式。

    字符串

    可缺省,缺省值为空。

    formVisibleNotify

    标识是否允许卡片使用卡片可见性通知。

    字符串

    可缺省,缺省值为空。

    metadata

    表示卡片的自定义信息,包含customizeData数组标签。

    对象

    可缺省,缺省值为空。

    7.2.3 通过message事件刷新卡片内容

    在卡片页面可以通过postCardAction接口触发message事件拉起FormExtensionAbility,然后由FormExtensionAbility刷新卡片内容。

    • 在页面中通过注册Button的onClick点击事件互调,并在回调中调用postCardAction接口触发事件至FormExtensionAbility。
    Image($r('app.media.game_start'))
      .width(120).height(54)
      .onClick(() => {
        postCardAction(this, {
          'action': 'message',
          'params': {
            'functionName': 'getSudoPuzzle'
          }
        })
      })
    • 在FormExtensionAbility的onFormEvent生命周期调用updateForm接口刷新卡片,通过getSudokuPuzzle()方法获取云函数生成的数独谜题和解,在刷新卡片时传递给卡片。
    onFormEvent(formId, message) {
      let params = JSON.parse(message);
      Log.info(Constants.LOG_TAG_NAME, `message ===> ${params.functionName}`);
      // Called when a specified message event defined by the form provider is triggered.
      if (params.functionName === "getSudoPuzzle") {
        let promise = PreferenceUtil.getPreference(this.context, Constants.GAME_LEVEL);
        promise.then((ret) => {
          let level = parseInt(ret);
          let puzzles = [], answers = [];
          getSudokuPuzzle(this.context, level).then((ret: string) => {
            let result: SudokuPuzzle = JSON.parse(ret);
            puzzles = result.original;
            answers = result.answer;
            let formData = {
              flag: true,
              puzzles: puzzles,
              answers: answers,
              level: level
            }
            let formBD = formBindingData.createFormBindingData(formData);
            Log.info(Constants.LOG_TAG_NAME, `level. ${JSON.stringify(formBD)}`);
            formProvider.updateForm(formId, formBD).then((data) => {
              Log.info(Constants.LOG_TAG_NAME, `FormAbility updateForm success. ${JSON.stringify(data)}`);
            }).catch((err) => {
              Log.error(Constants.LOG_TAG_NAME, `FormAbility updateForm failed. ${JSON.stringify(err)}`);
            })
          }).catch((err) => {
            Log.error(Constants.LOG_TAG_NAME, `cause: ${JSON.stringify(err)}`);
          })
        })
      }
    }

    卡片中需要使用@LocalStorageProp装饰器接收。

    @LocalStorageProp("puzzles") puzzles: Array<Array<number>> = [];
    @LocalStorageProp("answers") answers: Array<Array<Array<number>>> = [];

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    7.2.4 卡片不支持TextInput的替代方案

    ArkTS卡片具备JS卡片的全量能力,并且新增了动效能力和自定义绘制的能力,支持声明式的部分组件、事件、动效、数据管理、状态管理能力。在数独游戏中需要使用输入框录入谜题解,而ArkTS卡片暂时不具备TextInput组件能力,因此使用点击空白区域与数字按钮互换的方式替代TextInput组件能力。

    • 点击空白区域记录宫格坐标。
    Row() {}
    .backgroundColor(0xD1D3D5)
    .width(this.FULL_HEIGHT_PERCENT)
    .height(this.FULL_HEIGHT_PERCENT)
    .onClick(() => {
      this.selectArr = [i, j];
    })
    • 点击数字按钮,填入空白区域,通过postCardAction将空白区域填入解的数组传递给FormExtensionAbility的onFormEvent方法。
    ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9], (item: number) => {
      Button({ type: ButtonType.Normal }) {
        Text(item.toString()).fontSize(14).fontWeight(700)
      }
      .backgroundColor(Color.Orange)
      .borderRadius(4)
      .width(30).height(30)
      .fontSize(12)
      .onClick(() => {
        if (this.selectArr.length !== 0) {
          let row = this.selectArr[0];
          let col = this.selectArr[1];
          this.puzzles[row][col] = item;
          postCardAction(this, {
            'action': 'message',
            'params': {
              'functionName': 'refresh',
              'puzzles': this.puzzles
            }
          })
          this.userAnswer = this.puzzles;
          this.selectArr = [];
        }
      })
    })
    • 在onFormEvent方法中根据params.functionName.refresh通过调用updateForm刷新卡片。
    if (params.functionName === 'refresh') {
      let formData = {
        puzzles: params.puzzles
      }
      let formBD = formBindingData.createFormBindingData(formData);
      formProvider.updateForm(formId, formBD);
    }

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    7.2.5 卡片不支持setInterval的替代方案

    在应用程序中可以使用setInterval进行计时操作,但当前ArkTS卡片不支持setInterval,因此使用new Date().getTime()开始时间和结束时间差值作为游戏时长替代setInterval方法。

    7.3 闯关记录实时刷新的2*4卡片

    使用第二种方式创建卡片,在"entry > src/main/ets > widget > pages"目录右键单击"New > ArkTS File"创建HistoryCard.ets文件,接着打开"entry > src/main/resources > base > profile"目录下的form_config.json文件,配置名称为history的2*4卡片。

    {
      "name": "history",
      "description": "历史闯关记录",
      "src": "./ets/widget/pages/HistoryCard.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDefault": false,
      "updateEnabled": false,
      "scheduledUpdateTime": "02:35",
      "updateDuration": 1,
      "defaultDimension": "2*4",
      "supportDimensions": [
        "2*4"
      ]
    }
    • 在添加卡片时,将2*4卡片记录在缓存中并刷新卡片内容,移除卡片时删除缓存中对应的卡片信息。
    let formStorage = PreferenceUtil.getPreference(this.context, Constants.FORM_CARD_Dimension_2_4);
    formStorage.then((ret) =>{
      let formArr: Array<any> = new Array<any>();
      if (ret !== "") {
        formArr = JSON.parse(ret);
        formArr.push(formInfoStorage);
      } else {
        formArr.push(formInfoStorage);
      }
      PreferenceUtil.putPreference(this.context, Constants.FORM_CARD_Dimension_2_4, JSON.stringify(formArr));
    })
    let promise = PreferenceUtil.getPreference(this.context, Constants.HISTORY_RECORDS);
    promise.then((ret) => {
      if (ret !== "") {
        let historyArr: Array<History> = JSON.parse(ret);
        formData = {
          histories: historyArr
        }
        let formBD = formBindingData.createFormBindingData(formData);
        formProvider.updateForm(formId, formBD);
        return formBD;
      }
    })
    • 闯关成功后,刷新所有已添加的2*4卡片内容,已达到闯关历史记录效果。
    PreferenceUtil.getPreference(this.context, Constants.HISTORY_RECORDS).then((ret) => {
      let historyArr: Array<History> =  [];
      if (ret !== "") {
        historyArr = JSON.parse(ret);
        historyArr.push(history);
      } else {
        historyArr.push(history);
      }
      PreferenceUtil.putPreference(this.context, Constants.HISTORY_RECORDS, JSON.stringify(historyArr));
      let formStorage = PreferenceUtil.getPreference(this.context, Constants.FORM_CARD_Dimension_2_4);
      formStorage.then((ret) =>{
        if (ret !== "") {
          let formArr: Array<any> = JSON.parse(ret);
          formArr.forEach((item) => {
            let promise = PreferenceUtil.getPreference(this.context, Constants.HISTORY_RECORDS);
            promise.then((ret) => {
              if (ret !== "") {
                let historyArr: Array<History> = JSON.parse(ret);
                let formData = {
                  histories: historyArr
                }
                let formBD = formBindingData.createFormBindingData(formData);
                formProvider.updateForm(item.formId, formBD);
              }
            })
          })
        }
      })
    })

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区端云一体化,极简开发数独闯关游戏元服务-开源基础软件社区

    8 结语

    大家可以在华为应用市场元服务专区、服务中心入口,体验已经上架的元服务。也可以点击进入元服务官网,了解更多相关信息。

    责任编辑:张燕妮
    相关推荐

    2023-08-09 15:01:21

    2023-06-14 15:10:36

    鸿蒙游戏开发

    2012-05-07 17:09:52

    2023-03-14 21:19:29

    云函数云数据库

    2019-07-26 15:25:23

    青云QingCloud云计算

    2023-03-15 16:24:43

    云数据库代码开发

    2012-05-08 09:44:38

    2012-06-07 08:52:08

    微软云计算Windows

    2014-05-12 15:51:03

    浪潮BIM一体化

    2022-08-18 11:12:51

    Cloudera​数据湖仓SaaS

    2009-09-07 23:09:17

    2017-06-30 14:32:07

    红帽一体化混合云

    2009-07-02 09:32:00

    2009-12-03 15:34:41

    Suse Linux

    2011-05-24 09:26:02

    有线无线3G

    2009-08-17 22:32:25

    IT运维管理监控运维一体化摩卡
    点赞
    收藏

    51CTO技术栈公众号