云存儲(chǔ)圖片生成縮略圖開發(fā)

作者:狼哥
團(tuán)隊(duì):堅(jiān)果派
團(tuán)隊(duì)介紹:堅(jiān)果派由堅(jiān)果等人創(chuàng)建,團(tuán)隊(duì)擁有12個(gè)華為HDE帶領(lǐng)熱愛HarmonyOS/OpenHarmony的開發(fā)者,以及若干其他領(lǐng)域的三十余位萬粉博主運(yùn)營。專注于分享HarmonyOS/OpenHarmony、ArkUI-X、元服務(wù)、倉頡。團(tuán)隊(duì)成員聚集在北京,上海,南京,深圳,廣州,寧夏等地,目前已開發(fā)鴻蒙原生應(yīng)用,三方庫60+,歡迎交流。

注意

當(dāng)前API12的端云一體化開發(fā)工程僅支持手動(dòng)簽名

簡介

通過此案例學(xué)習(xí),可以學(xué)習(xí)到Serverless模板使用,云存儲(chǔ)、云數(shù)據(jù)庫、云函數(shù);同時(shí)可以學(xué)習(xí)到如何在云函數(shù)里調(diào)用云數(shù)據(jù)庫操作。

知識點(diǎn)

  1. 圖片尺寸調(diào)整模板
  2. 云存儲(chǔ)
  3. 云數(shù)據(jù)庫
  4. 云函數(shù)

1. Serverless模板使用

使用流程

序號 步驟 詳情
1 創(chuàng)建項(xiàng)目及應(yīng)用 使用此Serverless模板之前,您需要先創(chuàng)建項(xiàng)目和添加應(yīng)用。
2 部署模板 一鍵部署模板,配置模板參數(shù),請參見部署模板
3 使用模板 部署完成后,即可使用模板,請參見使用模板。

1.1 登錄AppGallery Connect 進(jìn)入到創(chuàng)建好的項(xiàng)目,開通云函數(shù)、云數(shù)據(jù)庫和云存儲(chǔ),這里就不詳細(xì)講解如何開通云函數(shù)、云數(shù)據(jù)庫、云存儲(chǔ),官方文檔有詳細(xì)講解。

1.2 在左邊菜單欄 云開發(fā)(Serverless) -> Serverless模板 -> 瀏覽更多Serverless模板 -> 圖片尺寸調(diào)整 (點(diǎn)擊部署) -> 選擇 之前創(chuàng)建好的項(xiàng)目 -> 選擇 數(shù)據(jù)處理位置 -> 配置參數(shù) -> 開始部署 - 已部署模板

image.png

image.png

image.png

image.png

配置云函數(shù)

圖片尺寸調(diào)整模板會(huì)在一鍵部署時(shí)自動(dòng)生成模板的函數(shù)接口,模板部署成功后,您還需在“云函數(shù)”頁面為對應(yīng)的函數(shù)接口添加對應(yīng)的云存儲(chǔ)觸發(fā)器,以實(shí)現(xiàn)在云存儲(chǔ)的實(shí)例中存放圖片后自動(dòng)觸發(fā)云函數(shù)。

1.3 選擇“云開發(fā)(Serverless)> 云函數(shù)”,在“函數(shù)列表”頁面根據(jù)已部署模板的“實(shí)例ID”找到模板對應(yīng)的函數(shù),點(diǎn)擊函數(shù)名稱進(jìn)入函數(shù)詳情頁。

image.png

1.4 在函數(shù)詳情頁選擇“觸發(fā)器”頁簽,點(diǎn)擊“添加觸發(fā)器”。

image.png

1.5 在彈出的“添加觸發(fā)器”窗口中配置觸發(fā)器相關(guān)參數(shù)。

image.png

具體參數(shù)說明如下表所示。

參數(shù) 說明
觸發(fā)器類型 選擇“云存儲(chǔ)觸發(fā)器”。
存儲(chǔ)實(shí)例 請配置為配置云存儲(chǔ)中保存的存儲(chǔ)實(shí)例名稱。
事件名稱 選擇“Completed”。

1.6 配置完成后,點(diǎn)擊“確定”。

image.png

小結(jié):這樣就完成了Serverless圖片尺寸調(diào)整模板使用,雖然可以用逗號隔開調(diào)整生成多個(gè)尺寸不同的圖片,有時(shí)我們只是想上傳到不同目錄下,生成的圖片尺寸不同,告訴大家一個(gè)好消息,也就是可以部署多個(gè)圖片尺寸調(diào)整模板,這樣就可以根據(jù)不同目錄,生成不同尺寸縮略圖。

2. 云存儲(chǔ)開發(fā)

2.1 文件選項(xiàng)是上傳的文件,可以創(chuàng)建文件夾存放不同的文件。

image.png

2.2 安全選項(xiàng)是限制上傳權(quán)限,為了方便開發(fā)測試,可以臨時(shí)把讀寫公開,如下面,方便學(xué)習(xí)此案例。

image.png

小結(jié):其實(shí)云存儲(chǔ)主要設(shè)置就是安全策略,哪些文件只可以只讀,哪些文件夾只可以寫,哪些文件夾可以讀寫。

3. 云數(shù)據(jù)庫開發(fā)

3.1 新增加一個(gè)圖片表,用來保存上傳到云存儲(chǔ)的圖片和縮略圖的訪問URL。

image.png

3.2 點(diǎn)擊新增按鈕,新增對象類型,也就是數(shù)據(jù)庫表。

image.png

image.png

image.png

image.png

image.png

小結(jié):根據(jù)圖片步驟,就可以創(chuàng)建好t_images表,為下面云函數(shù)調(diào)用保存數(shù)據(jù)到這個(gè)表里

4. 云函數(shù)開發(fā)

4.1 云函數(shù)開發(fā)是基于端云一體化項(xiàng)目開發(fā),關(guān)于端云一體化項(xiàng)目創(chuàng)建,就是在創(chuàng)建項(xiàng)目時(shí),選擇下圖模板就行,前提是要先在AGC上創(chuàng)建了項(xiàng)目和應(yīng)用,這里就不介紹如何創(chuàng)建端云一體化項(xiàng)目,可以移步到官方文檔查看。

image.png

4.2 右擊cloudfunctions目錄,創(chuàng)建云函數(shù),如下圖

image.png

4.3 輸入云函數(shù)名稱,選擇Cloud Function類型

image.png

4.4 云函數(shù)目錄結(jié)構(gòu)

image.png

4.5 云數(shù)據(jù)庫操作類

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'

/*
    配置區(qū)域
*/
//TODO 將AGC官網(wǎng)下載的配置文件放入resources文件夾下并將文件名替換為真實(shí)文件名
const credentialPath = "/resources/agc-apiclient-883106708808174848-7405487728880614016.json";
// 修改為在管理臺(tái)創(chuàng)建的存儲(chǔ)區(qū)名稱
let zoneName = "Images"
let logger
let mCloudDBZone

export default class CloudDBZoneWrapper {
    // AGC & 數(shù)據(jù)庫初始化
    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);
    }
    // 寫入數(shù)據(jù),主鍵相同則更新
    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)
        }
    }

    // 寫入數(shù)據(jù),主鍵相同則報(bào)錯(cuò)
    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)
        }
    }
    // 組裝需要插入或刪除的數(shù)據(jù)對象
    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;
    }
    // 設(shè)置需要更新的主鍵
    setMainKey(mainKey) {
        const unit = new t_images();
        unit.setId(mainKey);
        return unit
    }
}

4.6 云函數(shù)操作

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開發(fā)

5.1 界面UI

image.png

image.png

5.2 云存儲(chǔ)圖

image.png

5.3 云數(shù)據(jù)庫表數(shù)據(jù)圖

image.png

5.4 在EntryAbility的onCreate回調(diào)函數(shù)初始化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('獲取尺寸調(diào)整后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; // 選擇媒體文件的最大數(shù)目
      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 上傳文件到云存儲(chǔ)

      // 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 云存儲(chǔ)原圖路徑: ${bigPath}`)

      // ArkUI上下文
      bucket.uploadFile(getContext(this), {
        localPath: localPicPath,  // 本地文件路徑
        cloudPath: bigPath        // 云側(cè)文件路徑
      }).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
          // 此處圖片已成功上傳到云存儲(chǔ),由于生成縮略圖是異步的,此處簡單處理延時(shí)10秒后,
          // 再獲取原圖和縮略圖的下載URL
          setTimeout(async() => {
            let bigUrl: string = await this.getDownloadUrl(bigPath)
            let smailUrl: string = await this.getDownloadUrl(this.smallPath)
            this.isUploading = false;
            // 此處封裝保存到數(shù)據(jù)庫表數(shù)據(jù)對象
            let obj: ImageObj = {
              id: 2,
              img_name: fileName,
              img_big_url: bigUrl,
              img_small_url: smailUrl
            }
            console.info(`xx 調(diào)用云函數(shù)參數(shù):${JSON.stringify(obj)}`);
            // 調(diào)用自定義調(diào)用云函數(shù)方法
            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 調(diào)用云函數(shù)

  private callUploadImages(obj: ImageObj) {
    let arr: Array<ImageObj> = new Array<ImageObj>();
    arr.push(obj)
    let params: Params = {
      action: "insert",
      extraData: arr
    } as Params
    // 此處調(diào)用云側(cè)云函數(shù)
    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 {
  // 使用正則表達(dá)式來匹配文件名中的最后一個(gè)點(diǎn)(.)之后的所有字符
  const match = imagePath.match(/\.([^.]+)$/);
  return match ? match[1] : null;
}

總結(jié)

此案例主要流程就是點(diǎn)擊按鈕打開圖庫,選擇一張圖片,把圖片拷貝到緩存目錄一下,因?yàn)槟壳吧蟼魑募皆拼鎯?chǔ),只支持從緩存目錄下獲取,圖片上傳到云存儲(chǔ)后,觸發(fā)圖片尺寸調(diào)整云函數(shù),生成指定尺寸縮略圖,并存放到指定路徑的云存儲(chǔ)位置上,前端監(jiān)聽到圖片上傳成功后,調(diào)用獲取圖片下載URL接口,獲取到原圖和縮略圖的訪問URL后,調(diào)用云側(cè)云函數(shù),并判斷出是插入數(shù)據(jù)到云數(shù)據(jù)庫,從而調(diào)用云數(shù)據(jù)庫保存數(shù)據(jù),案例整體流程就是這樣,覆蓋到了Serverless模板使用,云存儲(chǔ),云函數(shù),云數(shù)據(jù)庫操作。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容