作者:狼哥
團(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. 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ù) -> 開始部署 - 已部署模板




配置云函數(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ù)詳情頁。

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

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

具體參數(shù)說明如下表所示。
| 參數(shù) | 說明 |
|---|---|
| 觸發(fā)器類型 | 選擇“云存儲(chǔ)觸發(fā)器”。 |
| 存儲(chǔ)實(shí)例 | 請配置為配置云存儲(chǔ)中保存的存儲(chǔ)實(shí)例名稱。 |
| 事件名稱 | 選擇“Completed”。 |
1.6 配置完成后,點(diǎn)擊“確定”。

小結(jié):這樣就完成了Serverless圖片尺寸調(diào)整模板使用,雖然可以用逗號隔開調(diào)整生成多個(gè)尺寸不同的圖片,有時(shí)我們只是想上傳到不同目錄下,生成的圖片尺寸不同,告訴大家一個(gè)好消息,也就是可以部署多個(gè)圖片尺寸調(diào)整模板,這樣就可以根據(jù)不同目錄,生成不同尺寸縮略圖。
2. 云存儲(chǔ)開發(fā)
2.1 文件選項(xiàng)是上傳的文件,可以創(chuàng)建文件夾存放不同的文件。

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

小結(jié):其實(shí)云存儲(chǔ)主要設(shè)置就是安全策略,哪些文件只可以只讀,哪些文件夾只可以寫,哪些文件夾可以讀寫。
3. 云數(shù)據(jù)庫開發(fā)
3.1 新增加一個(gè)圖片表,用來保存上傳到云存儲(chǔ)的圖片和縮略圖的訪問URL。

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





小結(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)目,可以移步到官方文檔查看。

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

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

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

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


5.2 云存儲(chǔ)圖

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

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ù)庫操作。