效果展示

创意来源
古诗词是中华传统文化的瑰宝,古人借诗词来言情、咏志、抒怀,古诗词记录了古人对于自然、社会、人文的观察与思考,也充满了真挚而多样的情感,有“每逢佳节倍思亲”的感慨孤独,有“但愿人长久,千里共婵娟”的真挚祝福,也有“爆竹声中一岁除,春风送暖入屠苏”的对于辞旧迎新的喜悦。每到季节更替、时节轮转、节日来临,总能唤起诗人对于人生的思考,造化的感悟,于是,诗意充盈在每一个平凡的日子之中,并融于我们的生活。而云服务和服务卡片又特别适合直观的展示诗词,于是有了根据节日或是节气,推荐展示符合主题的诗词的想法。
关于云服务和端云一体化的个人理解
云服务,顾名思义,“元”代表着轻量级和原子化,其具有免安装的特性,服务卡片是其重要的表现形式,既具有信息展示的功能,有可以作为应用的入口,一触即达,简洁直观。
端云一体化,传统的开发模式基本是前后端分离的,前端负责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配置云服务的名称和图标
到此,诗词卡片开发完成
一些建议或意见
- 目前通过SDK的方式调用云函数和云数据库的方式,操作起来还是略显繁琐,如果可以直接集成到系统的API里,可能会更方便一些。
- 目前有关端云一体化的文档比较分散,向云函数和云数据库的文档在AGC里,服务卡片和元服务的文档在鸿蒙这边,如果有一个专区将这些文档放在一起,将学习路径优化一下可能会更好。
- 目前好像云函数的调用没有日志功能,这一点对于开发者来说挺重要的。
506

被折叠的 条评论
为什么被折叠?



