作者:狼哥
团队:坚果派
团队介绍:坚果派由坚果等人创建,团队拥有12个华为HDE带领热爱HarmonyOS/OpenHarmony的开发者,以及若干其他领域的三十余位万粉博主运营。专注于分享HarmonyOS/OpenHarmony、ArkUI-X、元服务、仓颉。团队成员聚集在北京,上海,南京,深圳,广州,宁夏等地,目前已开发鸿蒙原生应用,三方库60+,欢迎交流。
注意
当前API12的端云一体化开发工程仅支持手动签名。
简介
通过此案例学习,可以学习到Serverless模板使用,云存储、云数据库、云函数;同时可以学习到如何在云函数里调用云数据库操作。
知识点
1. Serverless模板使用
使用流程
序号 | 步骤 | 详情 |
---|---|---|
1 | 创建项目及应用 | 使用此Serverless模板之前,您需要先创建项目和添加应用。 |
2 | 部署模板 | 一键部署模板,配置模板参数,请参见部署模板。 |
3 | 使用模板 | 部署完成后,即可使用模板,请参见使用模板。 |
1.1 登录AppGallery Connect 进入到创建好的项目,开通云函数、云数据库和云存储,这里就不详细讲解如何开通云函数、云数据库、云存储,官方文档有详细讲解。
1.2 在左边菜单栏 云开发(Serverless) -> Serverless模板 -> 浏览更多Serverless模板 -> 图片尺寸调整 (点击部署) -> 选择 之前创建好的项目 -> 选择 数据处理位置 -> 配置参数 -> 开始部署 - 已部署模板
配置云函数
图片尺寸调整模板会在一键部署时自动生成模板的函数接口,模板部署成功后,您还需在“云函数”页面为对应的函数接口添加对应的云存储触发器,以实现在云存储的实例中存放图片后自动触发云函数。
1.3 选择“云开发(Serverless)> 云函数”,在“函数列表”页面根据已部署模板的“实例ID”找到模板对应的函数,点击函数名称进入函数详情页。
1.4 在函数详情页选择“触发器”页签,点击“添加触发器”。
1.5 在弹出的“添加触发器”窗口中配置触发器相关参数。
具体参数说明如下表所示。
参数 | 说明 |
---|---|
触发器类型 | 选择“云存储触发器”。 |
存储实例 | 请配置为配置云存储中保存的存储实例名称。 |
事件名称 | 选择“Completed”。 |
1.6 配置完成后,点击“确定”。
小结:这样就完成了Serverless图片尺寸调整模板使用,虽然可以用逗号隔开调整生成多个尺寸不同的图片,有时我们只是想上传到不同目录下,生成的图片尺寸不同,告诉大家一个好消息,也就是可以部署多个图片尺寸调整模板,这样就可以根据不同目录,生成不同尺寸缩略图。
2. 云存储开发
2.1 文件选项是上传的文件,可以创建文件夹存放不同的文件。
2.2 安全选项是限制上传权限,为了方便开发测试,可以临时把读写公开,如下面,方便学习此案例。
小结:其实云存储主要设置就是安全策略,哪些文件只可以只读,哪些文件夹只可以写,哪些文件夹可以读写。
3. 云数据库开发
3.1 新增加一个图片表,用来保存上传到云存储的图片和缩略图的访问URL。
3.2 点击新增按钮,新增对象类型,也就是数据库表。
小结:根据图片步骤,就可以创建好t_images表,为下面云函数调用保存数据到这个表里
4. 云函数开发
4.1 云函数开发是基于端云一体化项目开发,关于端云一体化项目创建,就是在创建项目时,选择下图模板就行,前提是要先在AGC上创建了项目和应用,这里就不介绍如何创建端云一体化项目,可以移步到官方文档查看。
4.2 右击cloudfunctions目录,创建云函数,如下图
4.3 输入云函数名称,选择Cloud Function类型
4.4 云函数目录结构
4.5 云数据库操作类
const clouddb = require('@hw-agconnect/database-server/dist/index.js');
const agconnect = require('@agconnect/common-server');
const path = require('path');
import {t_images} from'./resources/t_images'
/*
配置区域
*/
//TODO 将AGC官网下载的配置文件放入resources文件夹下并将文件名替换为真实文件名
const credentialPath = "/resources/agc-apiclient-883106708808174848-7405487728880614016.json";
// 修改为在管理台创建的存储区名称
let zoneName = "Images"
let logger
let mCloudDBZone
export default 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 executeUpsert(data) {
if (!mCloudDBZone) {
console.log("CloudDBClient is null, try re-initialize it");
return;
}
try {
const resp = await mCloudDBZone.executeUpsert(data);
return resp;
} catch (error) {
logger.info('upsertBookInfo=>', error);
console.warn('upsertBookInfo=>', error)
}
}
// 写入数据,主键相同则报错
async executeInsert(data) {
if (!mCloudDBZone) {
console.log("CloudDBClient is null, try re-initialize it");
return;
}
try {
const resp = await mCloudDBZone.executeInsert(data);
return resp;
} catch (error) {
logger.info('insertBookInfos=>', error);
console.warn('insertBookInfos=>', error)
}
}
// 组装需要插入或删除的数据对象
getDataList(data) {
let dataList = [];
for(var i of data) {
const unit = new t_images();
unit.setId(i.id);
unit.setImg_name(i.img_name);
unit.setImg_big_url(i.img_big_url);
unit.setImg_small_url(i.img_small_url);
dataList.push(unit);
}
return dataList;
}
// 设置需要更新的主键
setMainKey(mainKey) {
const unit = new t_images();
unit.setId(mainKey);
return unit
}
}
4.6 云函数操作
import CloudDBZoneWrapper from './CloudDBZoneWrapper'
module.exports.myHandler = async function(event, context, callback, logger) {
logger.info("event: " + JSON.stringify(event))
var action;
var data;
const cloudDBZoneWrapper = new CloudDBZoneWrapper(logger);
if (event.body) {
var _body = JSON.parse(event.body);
action = _body.action;
data = _body.extraData;
} else {
action = event.action;
data = event.extraData;
}
logger.info("data: " + JSON.stringify(data))
let queryResult;
switch(action) {
case 'upsert':
let upsertData = cloudDBZoneWrapper.getDataList(data);
queryResult = await cloudDBZoneWrapper.executeUpsert(upsertData);
console.log(queryResult);
break;
case 'insert':
let insertData = cloudDBZoneWrapper.getDataList(data);
queryResult = await cloudDBZoneWrapper.executeInsert(insertData);
break;
default:
logger.info("invalid action");
console.log("invalid action");
}
callback(queryResult);
};
5. ArkTS开发
5.1 界面UI
5.2 云存储图
5.3 云数据库表数据图
5.4 在EntryAbility的onCreate回调函数初始化AGC
// 初始化SDK
let input = await this.context.resourceManager.getRawFileContent('agconnect-services.json')
let jsonString = util.TextDecoder.create('utf-8', {
ignoreBOM: true
}).decodeWithStream(input, {
stream: false
});
// hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate '+jsonString);
initialize(this.context, JSON.parse(jsonString));
5.5 界面布局
Column() {
Navigation()
.title($r('app.string.cloudStorage_label'))
.height('50vp')
.width('100%')
.margin({ bottom: 10 })
.titleMode(NavigationTitleMode.Mini)
Column() {
Row() {
Text($r('app.string.cloudStorage_description')).fontSize($r('app.float.body_font_size'))
}.margin({ bottom: 15 })
Row() {
Button($r('app.string.cloudStorage_uploadButton'), { type: ButtonType.Normal })
.borderRadius(4)
.width('45%')
.opacity(!this.isUploading ? 1 : 0.5)
.enabled(!this.isUploading)
.height(40)
.onClick(() => {
this.upLoadImage()
})
Button('获取尺寸调整后URL', { type: ButtonType.Normal })
.borderRadius(4)
.width('45%')
.opacity(!this.isUploading ? 1 : 0.5)
.enabled(!this.isUploading)
.height(40)
.onClick(() => {
this.getDownloadUrl(this.smallPath)
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
if (this.isUploading) {
Row() {
Text($r('app.string.cloudStorage_progressLabel')).fontSize($r('app.float.body_font_size'))
Text(`: ${this.updateProgress.toString().substring(0, 5)} %`).fontSize($r('app.float.body_font_size'))
}.margin({ top: 10 })
}
}.alignItems(HorizontalAlign.Start).width('90%').margin({ bottom: 20 })
Column() {
Row() {
Image(this.image).objectFit(ImageFit.Contain).height(250).backgroundColor($r('app.color.black'))
}
}.width('90%').margin({ bottom: 15 })
}.height('100%')
5.6 打开图库选择一张图片,并把图片拷贝到缓存目录下。
private selectImage(): Promise<string> {
return new Promise((resolve: (selectUri: string) => void, reject: (err: Error) => void) => {
// 使用photoAccessHelper选择指定的文件
let photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; // 过滤选择媒体文件类型为IMAGE
photoSelectOptions.maxSelectNumber = 1; // 选择媒体文件的最大数目
let photoViewPicker = new photoAccessHelper.PhotoViewPicker();
photoViewPicker.select(photoSelectOptions).then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
let fileUri = photoSelectResult.photoUris[0];
console.info(`xx pick file ${fileUri}`);
let fileName = fileUri.split('/').pop() as string;
console.info(`xx file name ${fileName}`);
let cacheFilePath = getContext().cacheDir + '/' + fileName;
console.info(`xx cacheFilePath ${cacheFilePath}`);
// 将选中文件copy至cache目录下,文件名为cacheFile
try {
let srcFile = fs.openSync(fileUri);
let dstFile = fs.openSync(cacheFilePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
fs.copyFileSync(srcFile.fd, dstFile.fd);
fs.closeSync(srcFile);
fs.closeSync(dstFile);
console.info(`xx 返回缓存文件路径: ${cacheFilePath}`);
resolve(cacheFilePath);
} catch (e) {
console.info(`xx copy file failed ${e.message}`);
reject(e)
}
});
})
}
5.7 上传文件到云存储
// localPicPath为缓存文件路径
let localPicName = localPicPath.split('/').pop() as string;
let imgExtension = getImageExtension(localPicName);
let fileName: string = `${Date.now()}_a`;
let bigPath: string = 'study/'+fileName+'.'+imgExtension;
this.smallPath = 'study/thumbnail/resized_'+fileName+'144x221'+'.'+imgExtension;
console.info(`xx 云存储原图路径: ${bigPath}`)
// ArkUI上下文
bucket.uploadFile(getContext(this), {
localPath: localPicPath, // 本地文件路径
cloudPath: bigPath // 云侧文件路径
}).then((task: request.agent.Task) => {
task.on('progress', (p) => {
console.info(`xx on progress ${JSON.stringify(p)}`);
this.updateProgress = p.processed / p.sizes[0] * 100;
});
task.on('completed', (progress) => {
console.info(`xx on completed ${JSON.stringify(progress)}`);
this.isUploading = false
// 此处图片已成功上传到云存储,由于生成缩略图是异步的,此处简单处理延时10秒后,
// 再获取原图和缩略图的下载URL
setTimeout(async() => {
let bigUrl: string = await this.getDownloadUrl(bigPath)
let smailUrl: string = await this.getDownloadUrl(this.smallPath)
this.isUploading = false;
// 此处封装保存到数据库表数据对象
let obj: ImageObj = {
id: 2,
img_name: fileName,
img_big_url: bigUrl,
img_small_url: smailUrl
}
console.info(`xx 调用云函数参数:${JSON.stringify(obj)}`);
// 调用自定义调用云函数方法
this.callUploadImages(obj)
}, 10000)
});
task.on('failed', (progress) => {
console.error(`xx on failed ${JSON.stringify(progress)}`);
this.isUploading = false
});
task.on('response', (response) => {
console.info(`xx on response ${JSON.stringify(response)}`);
});
// start task
task.start((err: BusinessError) => {
if (err) {
console.error(`xx Failed to start the uploadFile task, Code: ${err.code}, message: ${err.message}`);
} else {
console.info(`xx Succeeded in starting a uploadFile task.`);
}
});
}).catch((err: BusinessError) => {
console.error(`xx Upload file failed, Code: ${err.code}, message: ${err.message}`);
});
5.8 调用云函数
private callUploadImages(obj: ImageObj) {
let arr: Array<ImageObj> = new Array<ImageObj>();
arr.push(obj)
let params: Params = {
action: "insert",
extraData: arr
} as Params
// 此处调用云侧云函数
cloudFunction.call({ name: 'upload-images', data: params }).then((res: cloudFunction.FunctionResult) => {
hilog.info(0x0000, 'CloudFunction', 'xx call upload-images, ResultMessage: %{public}s',
res.result);
}).catch((err: BusinessError) => {
hilog.error(0x0000, 'CloudFunction', 'xx call upload-images, ErrCode: %{public}d ErrMessage: %{public}s',
err.code, err.message);
});
}
5.9 获取图片下载URL
private getDownloadUrl(path: string):Promise<string> {
return new Promise((resolve: (selectUri: string) => void, reject: (err: Error) => void) => {
bucket.getDownloadURL(path).then(async (downloadURL: string) => {
hilog.info(0x0000, 'CloudStorage', 'xx DownloadURL: %{public}s', downloadURL);
resolve(downloadURL);
}).catch((err: BusinessError) => {
hilog.error(0x0000, 'CloudStorage', 'xx getDownloadURL fail, error code: %{public}d, message: %{public}s',
err.code, err.message);
reject(err)
});
});
}
5.10 获取文件名后辍
function getImageExtension(imagePath: string): string | null {
// 使用正则表达式来匹配文件名中的最后一个点(.)之后的所有字符
const match = imagePath.match(/\.([^.]+)$/);
return match ? match[1] : null;
}
总结
此案例主要流程就是点击按钮打开图库,选择一张图片,把图片拷贝到缓存目录一下,因为目前上传文件到云存储,只支持从缓存目录下获取,图片上传到云存储后,触发图片尺寸调整云函数,生成指定尺寸缩略图,并存放到指定路径的云存储位置上,前端监听到图片上传成功后,调用获取图片下载URL接口,获取到原图和缩略图的访问URL后,调用云侧云函数,并判断出是插入数据到云数据库,从而调用云数据库保存数据,案例整体流程就是这样,覆盖到了Serverless模板使用,云存储,云函数,云数据库操作。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。