#端云一体化#SHOW出您的元服务#端云一体化开发诗词卡片

效果展示

创意来源

古诗词是中华传统文化的瑰宝,古人借诗词来言情、咏志、抒怀,古诗词记录了古人对于自然、社会、人文的观察与思考,也充满了真挚而多样的情感,有“每逢佳节倍思亲”的感慨孤独,有“但愿人长久,千里共婵娟”的真挚祝福,也有“爆竹声中一岁除,春风送暖入屠苏”的对于辞旧迎新的喜悦。每到季节更替、时节轮转、节日来临,总能唤起诗人对于人生的思考,造化的感悟,于是,诗意充盈在每一个平凡的日子之中,并融于我们的生活。而云服务和服务卡片又特别适合直观的展示诗词,于是有了根据节日或是节气,推荐展示符合主题的诗词的想法。

关于云服务和端云一体化的个人理解

云服务,顾名思义,“元”代表着轻量级和原子化,其具有免安装的特性,服务卡片是其重要的表现形式,既具有信息展示的功能,有可以作为应用的入口,一触即达,简洁直观。

端云一体化,传统的开发模式基本是前后端分离的,前端负责UI的开发,后端负责数据业务和接口的编写,这种模式虽然有很多好处,但也存在着沟通成本高,前后端技术栈差异大,学习成本高等缺点,且后端一般需要部署到服务器上,还需要根据需求对服务器进行运维或者扩容,成本较高,较为繁琐,对于一般小型应用显然不太友好。而端云一体化较好的解决了这些痛点,将华为的serverless和元服务结合起来,使用同一IDE同时开发前后端,使用同一种语言typescript开发前后端,既节约了开发者的学习成本,也降低了开发难度,可以让开发者更专注于应用本身,而不是部署运维等事务。

开发步骤

参考官方给出的文档,和个人的摸索。

1. 在Deveco Studio中新建项目,选择元服务端云一体化模版,点击next

2. 配置项目,填写项目名称,包名,点击next

3. 关联云端项目,如果没有创建,则创建云项目

 

4. 查看项目结构

  

5. 开发第一个云函数,参考官网文档

根据业务需求,我将需求拆分成了两个云函数,一个云函数的功能是根据今天的日期,查询今天是否是传统节日,或是节气,确定今天的诗词主题。另一个云函数的公司根据传入的诗词主题,查询云数据库中的诗词数据并返回。

首先是获取今日的诗词主题,为了获取今天的节日和节气数据,我引入了第三方库lunar-javascript,文档链接https://6tail.cn/calendar/api.html#lunar.jieqi.html,添加方式是在云函数get-theme的目录下的package.json里添加依赖  云函数get-theme代码如下:

  let isFestival: boolean = false;
  let isJieQi: boolean = false;
  let festivals=[]
  let jieQi=""
  // do something here
  const d = Lunar.fromDate(new Date())
  //获取物候名和候次
  const wuhou = d.getWuHou()
  const hou = d.getHou()
  let tag = ""
  let lunar=d.toString()
  let prev = d.getPrevJieQi();
  let next = d.getNextJieQi();
  //获取今天是否是节日
  let l = d.getFestivals();
  if (l.length > 0) {
    tag = l[0]
    festivals=l
    isFestival = true
  } else {
    jieQi = d.getJieQi();
    if (jieQi) {
      tag = jieQi
      isJieQi = true
    } else {
      //如果不是节气,获取上一个节气
      const prev_name = d.getPrevJieQi().getName();
      tag = prev_name
    }
  }
  logger.info("tag:" + tag)
  logger.info(event, context)
  callback({
    isFestival,
    festivals,
    isJieQi,
    jieQi,
    prev:{name:prev.getName(),time:prev.getSolar().toYmdHms()},
    next:{name:next.getName(),time:next.getSolar().toYmdHms()},
    hou,
    wuhou,
    tag,
    lunar,

  });
};

export { myHandler };
复制

部署云函数

可以通过IDE或是浏览器控制台调试云函数

6. 开发第二个云函数,创建数据对象并导入数据

云函数get-potery的功能是根据传入的主题在云数据库中查询符合条件的数据,那么我们需要先创建数据对象,导入数据,这一部分工作可以在IDE里通过编写配置文件完成,也可以通过浏览器控制台完成,推荐后者,因为操作起来比较直观。

感觉数据对象比较类似于关系型数据库定义表结构   

创建数据区,注意不同数据区是相互隔离的,因此一定要注意数据查询和导入时,一定要区分数据区 

导入数据 

导入的Json文件格式大致如下  导入成功后可以看到  数据对象和数据导入完成后,可以开始编写第二个函数,既根据传入的主题查询云数据库。云函数调用云数据库参考官方文档https://developer.huawei.com/consumer/cn/doc/development/AppGallery-connect-Guides/query-clouddb-overview-0000001549142354,文档写的比较详细,因此不再赘述。 云函数结构如下 CloudDBZoneWrapper.js中代码如下

const clouddb = require('@hw-agconnect/database-server/dist/index.js');
const agconnect = require('@agconnect/common-server');
const path = require('path');
const shiju=require('./model/shiju')
/*
    配置区域
*/
//TODO 将AGC官网下载的配置文件放入resources文件夹下并将文件名替换为真实文件名
const credentialPath = "/resources/agc-apiclient-1255177787686308800-7284180544987715810.json";
// 修改为在管理台创建的存储区名称
let zoneName = "MyData"
// 修改为需要操作的对象
let objectName =shiju.shiju;

let logger

let mCloudDBZone

class CloudDBZoneWrapper {

    // AGC & 数据库初始化
    constructor(log) {
        logger = log;

        let agcClient;

        try {
            agcClient = agconnect.AGCClient.getInstance();
        } catch (error) {
            agconnect.AGCClient.initialize(agconnect.CredentialParser.toCredential(path.join(__dirname, credentialPath)));
            agcClient = agconnect.AGCClient.getInstance();
        }

        clouddb.AGConnectCloudDB.initialize(agcClient);

        const cloudDBZoneConfig = new clouddb.CloudDBZoneConfig(zoneName);

        const agconnectCloudDB = clouddb.AGConnectCloudDB.getInstance(agcClient);
        mCloudDBZone = agconnectCloudDB.openCloudDBZone(cloudDBZoneConfig);
    }

    async queryAll() {
        if (!mCloudDBZone) {
            logger.info("CloudDBClient is null, try re-initialize it");
            console.log("CloudDBClient is null, try re-initialize it")
            return;
        }
        try {
            const resp = await mCloudDBZone.executeQuery(clouddb.CloudDBZoneQuery.where(objectName));
            return resp
        } catch (error) {
            logger.info('queryAll=>', error);
            console.log(error)
        }
    }

    async queryCompound(data) {

        if (!mCloudDBZone) {
            logger.info("CloudDBClient is null, try re-initialize it");
            console.log("CloudDBClient is null, try re-initialize it")
            return;
        }
        try {
            // 根据业务需要修改查询条件
            const cloudDBZoneQuery = this.setQuery(data);
            const resp = await mCloudDBZone.executeQuery(cloudDBZoneQuery);
            return resp.getSnapshotObjects()
        } catch (error) {
            logger.info('queryCompound=>', error);
            console.log(error)
        }

    }

    setQuery(data) {
        const cloudDBZoneQuery = clouddb.CloudDBZoneQuery.where(objectName);

        if("contains" in data) {
            let contains = data.contains;
            for (var key in contains) {
                cloudDBZoneQuery.contains(key, contains[key]);
            }
        }
         
        return cloudDBZoneQuery.limit(50);
    }

}

module.exports = CloudDBZoneWrapper;
复制

get-potert.ts中代码如下

const CloudDBZoneWrapper = require("./CloudDBZoneWrapper.js");
module.exports.myHandler = async function(event, context, callback, logger) {
  logger.info(event);
  const cloudDBZoneWrapper = new CloudDBZoneWrapper(logger);
  let tag="冬至"
  if(event.body){
    //接收传入的参数
    tag=JSON.parse(event.body)['tag']
  }
  logger.info("tag="+tag)
  let queryResult;
  queryResult = await cloudDBZoneWrapper.queryCompound({
    "contains": {
      "theme": tag
    }
  });
  //返回查询的结果
  callback({ theme:tag,poem:queryResult});
};
复制

返回的结果如下

{
    "poem":[
        {
            "author":"白居易",
            "theme":"夏至",
            "id":834,
            "title":"和梦得夏至忆苏州呈卢宾客",
            "content":"粽香筒竹嫩,炙脆子鹅鲜。"
        },
        {
            "author":"白玉蟾",
            "theme":"夏至",
            "id":835,
            "title":"赠潘高士二首·冬至炼朱砂",
            "content":"冬至炼朱砂,夏至炼水银。"
        }
    ],
    "theme":"夏至"
}
复制

云端的开发大致完毕,将云函数部署到云端并调试完成后,进入端侧的开发

7. 服务卡片的开发

在“src/main/ets/widget/pages/WidgetCard.ets”文件中编写界面UI代码 代码如下:


@Entry
@Component
struct WidgetCard {
  @LocalStorageProp('title') title:string="水调歌头";
  @LocalStorageProp('author') author:string="苏轼";
  @LocalStorageProp('content') content:string="但愿人长久,千里共婵娟";
  /*
   * The max lines.
   */
  readonly MAX_LINES: number = 2;

  /*
   * The action type.
   */
  readonly ACTION_TYPE: string = 'router';

  /*
   * The message.
   */
  readonly MESSAGE: string = 'add detail';

  /*
   * The ability name.
   */
  readonly ABILITY_NAME: string = 'EntryAbility';

  /*
   * The with percentage setting.
   */
  readonly FULL_WIDTH_PERCENT: string = '100%';

  /*
   * The height percentage setting.
   */
  readonly FULL_HEIGHT_PERCENT: string = '100%';

  build() {
    Stack() {
      Image($r("app.media.card_bg"))
        .width(this.FULL_WIDTH_PERCENT)
        .height(this.FULL_HEIGHT_PERCENT)
        .objectFit(ImageFit.Cover)
      Column() {
        Text(this.content)
          .fontSize($r('app.float.title_immersive_font_size'))
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .fontColor($r('app.color.text_font_color'))
          .maxLines(this.MAX_LINES)
        Text(`——${this.author} 《${this.title}》`)
          .fontSize($r('app.float.detail_immersive_font_size'))
          .opacity($r('app.float.detail_immersive_opacity'))
          .margin({ top: $r('app.float.detail_immersive_margin_top') })
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .fontColor($r('app.color.text_font_color'))
          .maxLines(this.MAX_LINES)
        Button("换一个").onClick(()=>{
          //点击按钮,刷新卡片内容
          postCardAction(this,{
            "action":"message",
            'params': {
              'msgTest': 'messageEvent'
            }
          })

        })
      }
      .width(this.FULL_WIDTH_PERCENT)
      .height(this.FULL_HEIGHT_PERCENT)
      .alignItems(HorizontalAlign.Center)
      .justifyContent(FlexAlign.Center)
      .padding($r('app.float.column_padding'))
    }
    .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
        }
      });
    })
  }
}
复制

卡片预览效果 在“src/main/ets/entryformability/EntryFormAbility.ts”中编写卡片生命周期 代码如下:

import formInfo from '@ohos.app.form.formInfo';
import formBindingData from '@ohos.app.form.formBindingData';
import FormExtensionAbility from '@ohos.app.form.FormExtensionAbility';
import update from  "../services/util"
export default class EntryFormAbility extends FormExtensionAbility {
  onAddForm(want) {
    // Called to return a FormBindingData object.
    let formData = {};
    return formBindingData.createFormBindingData(formData);
  }

  onCastToNormalForm(formId) {
    // Called when the form provider is notified that a temporary form is successfully
    // converted to a normal form.
  }

  onUpdateForm(formId) {
      update(this.context,formId)
    // Called to notify the form provider to update a specified form.
  }

  onChangeFormVisibility(newStatus) {
    // Called when the form provider receives form events from the system.
  }

  onFormEvent(formId, message) {
    console.info(`FormAbility onEvent, formId = ${formId}, message: ${JSON.stringify(message)}`);
    update(this.context,formId)
  }

  onRemoveForm(formId) {
    // Called to notify the form provider that a specified form has been destroyed.
  }
  onAcquireFormState(want) {
    // Called to return a {@link FormState} object.
    return formInfo.FormState.READY;
  }
};
复制

其中,update是我编写的工具函数,util.ts的代码如下:

import data_preferences from '@ohos.data.preferences';
import { getPoem, getTheme } from "../services/Function"
import formProvider from '@ohos.app.form.formProvider';
import formBindingData from '@ohos.app.form.formBindingData';

export default function update(context, formId) {
  data_preferences.getPreferences(context, 'mystore').then((preferences) => {
    //请求诗句主题
    getTheme(context).then((theme) => {
      let tag = theme['tag']
      console.log(tag)
      preferences.has(tag).then((has) => {
        if (has) {
          //如果有缓存,直接取出
          preferences.get(tag, "冬至").then((res) => {
            updateCard(formId, JSON.parse(res as string))
          })
        } else {
          //如果没有缓存,请求数据并缓存
          getPoem(context, tag).then((result) => {
            console.log(JSON.stringify(result))
            preferences.put(tag, JSON.stringify(result))
            updateCard(formId, result)
          })
        }
      })
    })
  })
}

function updateCard(formId, data) {
  console.log(JSON.stringify(data))
  let poem = data['poem']
  console.log("poem=" + poem)
  let count = poem.length
  let index = Math.floor(Math.random() * (count + 1));
  poem = poem[index]
  formProvider.updateForm(formId, formBindingData.createFormBindingData({
    title: poem['title'],
    author: poem['author'],
    content: poem['content']
  }))
}
复制

其中,getPoem和getTheme的代码如下,代码参考模板给的调用云函数的代码

import agconnect from '@hw-agconnect/api-ohos';
import "@hw-agconnect/function-ohos";
import { Log } from '../common/Log';
import { getAGConnect } from './AgcConfig';
const TAG = "[AGCFunction]";
export function getPoem(context,tag): Promise<string> {
    return new Promise((resolve, reject) => {
        getAGConnect(context);
        let functionResult;
        //这里的名字在AGC控制台中的函数触发器里可以找到
        let functionCallable = agconnect.function().wrap("get-potery-$latest");
        functionCallable.call({tag}).then((ret: any) => {
            functionResult = ret.getValue();
            console.log(JSON.stringify(functionResult)+"_____________________")
            Log.info(TAG, "Cloud Function Called, Returned Value: " + JSON.stringify(ret.getValue()));
            resolve(functionResult);
        }).catch((error: any) => {
            Log.error(TAG, "Error - could not obtain cloud function result. Error Detail: " + JSON.stringify(error));
            reject(error);
        });
    });
}
export function getTheme(context): Promise<string> {
    return new Promise((resolve, reject) => {
        getAGConnect(context);
        let functionResult;
        let functionCallable = agconnect.function().wrap("get-theme-$latest");
        functionCallable.call().then((ret: any) => {
            functionResult = ret.getValue();
            Log.info(TAG, "Cloud Function Called, Returned Value: " + JSON.stringify(ret.getValue()));
            resolve(functionResult);
        }).catch((error: any) => {
            Log.error(TAG, "Error - could not obtain cloud function result. Error Detail: " + JSON.stringify(error));
            reject(error);
        });
    });
}
复制

8. 元服务页面开发

在ets/pages下新建页面Main,并在entryability里修改入口页面为新建的页面。 Main.ets代码如下:

import { getTheme, getPoem } from "../services/Function"
import data_preferences from '@ohos.data.preferences';

@Entry
@Component
struct Main {
  @State lunar: string = "二〇二三年八月二十"
  @State isFestival: boolean = false
  @State isJieQi: boolean = false
  @State festivals: string = ""
  @State jieQi: string = ""
  @State wuHou: string = "水始涸"
  @State hou: string = "秋分 三候"
  @State prev: string = ""
  @State next: string = ""
  @State tag: string = "秋分"
  @State poem: Array<Object> = new Array()

  aboutToAppear() {
    getTheme(getContext(this)).then((res) => {
      console.log(res['hou'])
      this.lunar = res['lunar']
      this.isFestival = res['isFestival']
      this.isJieQi = res['isJieQi']
      this.festivals = res['festivals'].join(',')
      this.jieQi = res['jieQi']
      this.wuHou = res['wuhou']
      this.hou = res['hou']
      this.prev = res['prev']['name'] + " " + res['prev']['time']
      this.next = res['next']['name'] + " " + res['next']['time']
      this.tag = res['tag']

      data_preferences.getPreferences(getContext(this), 'mystore').then((preferences) => {

        preferences.has(this.tag).then((has) => {
          if (has) {
            //如果有缓存,直接取出
            preferences.get(this.tag, "冬至").then((res) => {
              res = JSON.parse(res as string)
              let poem = res['poem']
              this.poem.push(...poem)

            })
          } else {
            getPoem(getContext(this), this.tag).then((result) => {
              console.log(JSON.stringify(result))
              preferences.put(this.tag, JSON.stringify(result))
              let poem = result['poem']
              this.poem.push(...poem)
            })
          }
        })

      })
    })
  }

  build() {
    Column() {
      Text(this.lunar).fontSize(32).fontWeight(FontWeight.Bold).fontColor($r("app.color.text_blue")).margin(12)
      if (this.isFestival) {
        Text(this.festivals).fontSize(24)
      }
      if (this.isJieQi) {
        Text(this.jieQi).fontSize(24)
      }
      Text(this.hou + " " + this.wuHou).fontSize(24).margin(12).fontColor($r("app.color.text_blue"))
      Text("上一节气:").fontSize(16).margin({ top: 12 })
      Text(this.prev).fontSize(18).margin(12).fontColor($r("app.color.text_blue"))
      Text("下一节气:").fontSize(16)
      Text(this.next).fontSize(18).margin(12).fontColor($r("app.color.text_blue"))
      Text("今日主题是:" + this.tag).fontSize(16).margin(12)
      Swiper(){
        ForEach(this.poem,(item,index)=>{
          Stack() {
            Image(index%2==0?$r("app.media.card_bg"):$r("app.media.card_bg2")).width("100%").height(200)
            Column() {
              Text(item.content).fontSize(18).margin(5).fontColor($r("app.color.white"))
              Text(`——${item.author}《${item.title}》`).fontSize(16).fontColor($r("app.color.white")).textOverflow({ overflow: TextOverflow.Ellipsis })
            }
          }.borderRadius(24).padding(12)
        },item=>item.title)
      }

      }.width("100%").height("100%")

    }
  }
复制

预览效果如下:

9. 图标及云服务名称配置

在APPSCOPE里的app.json5配置云服务的名称和图标

到此,诗词卡片开发完成

一些建议或意见

  1. 目前通过SDK的方式调用云函数和云数据库的方式,操作起来还是略显繁琐,如果可以直接集成到系统的API里,可能会更方便一些。
  2. 目前有关端云一体化的文档比较分散,向云函数和云数据库的文档在AGC里,服务卡片和元服务的文档在鸿蒙这边,如果有一个专区将这些文档放在一起,将学习路径优化一下可能会更好。
  3. 目前好像云函数的调用没有日志功能,这一点对于开发者来说挺重要的。

进入华为专区,解锁更多精彩内容

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值