作者:狼哥
团队:坚果派
团队介绍:坚果派由坚果等人创建,团队拥有12个华为HDE带领热爱HarmonyOS/OpenHarmony的开发者,以及若干其他领域的三十余位万粉博主运营。专注于分享HarmonyOS/OpenHarmony、ArkUI-X、元服务、仓颉。团队成员聚集在北京,上海,南京,深圳,广州,宁夏等地,目前已开发鸿蒙原生应用,三方库60+,欢迎交流。

注意

当前API12的端云一体化开发工程仅支持手动签名

简介

通过此案例学习,可以学习到Serverless模板使用,云存储、云数据库、云函数;同时可以学习到如何在云函数里调用云数据库操作。

知识点

  1. 图片尺寸调整模板
  2. 云存储
  3. 云数据库
  4. 云函数

1. Serverless模板使用

使用流程

序号步骤详情
1创建项目及应用使用此Serverless模板之前,您需要先创建项目和添加应用
2部署模板一键部署模板,配置模板参数,请参见部署模板
3使用模板部署完成后,即可使用模板,请参见使用模板

1.1 登录AppGallery Connect 进入到创建好的项目,开通云函数、云数据库和云存储,这里就不详细讲解如何开通云函数、云数据库、云存储,官方文档有详细讲解。

1.2 在左边菜单栏 云开发(Serverless) -> Serverless模板 -> 浏览更多Serverless模板 -> 图片尺寸调整 (点击部署) -> 选择 之前创建好的项目 -> 选择 数据处理位置 -> 配置参数 -> 开始部署 - 已部署模板

image-20240828182910803image-20240828183043558

image-20240828183212276

image-20240828183318129

配置云函数

图片尺寸调整模板会在一键部署时自动生成模板的函数接口,模板部署成功后,您还需在“云函数”页面为对应的函数接口添加对应的云存储触发器,以实现在云存储的实例中存放图片后自动触发云函数。

1.3 选择“云开发(Serverless)> 云函数”,在“函数列表”页面根据已部署模板的“实例ID”找到模板对应的函数,点击函数名称进入函数详情页。

image-20240828220456316

1.4 在函数详情页选择“触发器”页签,点击“添加触发器”。

image-20240828220655961

1.5 在弹出的“添加触发器”窗口中配置触发器相关参数。

image-20240828220825998

具体参数说明如下表所示。

参数说明
触发器类型选择“云存储触发器”。
存储实例请配置为配置云存储中保存的存储实例名称。
事件名称选择“Completed”。

1.6 配置完成后,点击“确定”。

image-20240828220655961

小结:这样就完成了Serverless图片尺寸调整模板使用,虽然可以用逗号隔开调整生成多个尺寸不同的图片,有时我们只是想上传到不同目录下,生成的图片尺寸不同,告诉大家一个好消息,也就是可以部署多个图片尺寸调整模板,这样就可以根据不同目录,生成不同尺寸缩略图。

2. 云存储开发

2.1 文件选项是上传的文件,可以创建文件夹存放不同的文件。

image-20240828223152902

2.2 安全选项是限制上传权限,为了方便开发测试,可以临时把读写公开,如下面,方便学习此案例。

image-20240828223559534

小结:其实云存储主要设置就是安全策略,哪些文件只可以只读,哪些文件夹只可以写,哪些文件夹可以读写。

3. 云数据库开发

3.1 新增加一个图片表,用来保存上传到云存储的图片和缩略图的访问URL。

image-20240828224057204

3.2 点击新增按钮,新增对象类型,也就是数据库表。

image-20240828224325725

image-20240828224433722

image-20240828224625107

image-20240828224702615

image-20240828224758499

小结:根据图片步骤,就可以创建好t_images表,为下面云函数调用保存数据到这个表里

4. 云函数开发

4.1 云函数开发是基于端云一体化项目开发,关于端云一体化项目创建,就是在创建项目时,选择下图模板就行,前提是要先在AGC上创建了项目和应用,这里就不介绍如何创建端云一体化项目,可以移步到官方文档查看。

image-20240828225406217

4.2 右击cloudfunctions目录,创建云函数,如下图

image-20240828225751135

4.3 输入云函数名称,选择Cloud Function类型

image-20240828230034110

4.4 云函数目录结构

image-20240828230611419

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

image-2024082823234xximage-2024082823234xx

5.2 云存储图

image-20240828232342429

5.3 云数据库表数据图

image-20240828232157512

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模板使用,云存储,云函数,云数据库操作。


狼哥
1 声望0 粉丝