鸿蒙端云一体化开发(三):云存储

欢迎来到鸿蒙端云一体化开发教程的第三篇。前两篇我们分别学习了云函数和云数据库,今天咱们来聊聊应用开发中另一个非常重要的功能------云存储

在现代应用中,我们经常需要处理用户上传的图片、视频或者文档。如果自己搭建文件服务器,不仅要考虑带宽、存储容量,还要操心CDN加速、安全防护等一堆麻烦事。Cloud Foundation Kit的云存储服务就是为了解决这些问题而生的。它提供了一个可扩展、安全可靠的云端存储服务,让你在端侧就能轻松实现文件的上传、下载和管理。

一、实战场景:图片上传与展示

为了让大家快速上手,我们今天模拟一个最常见的场景:用户上传头像并展示

我们需要实现以下功能:

  1. 用户从相册选择一张图片。
  2. 将图片上传到云存储。
  3. 获取图片的下载地址进行展示。
  4. 不再需要时删除云端图片。

二、准备工作与初始化

2.1 申请权限

首先,云存储需要网络权限。打开entry/src/main/module.json5文件,添加网络权限:

json 复制代码
"requestPermissions": [
  {
    "name": "ohos.permission.INTERNET"
  }
]

2.2 初始化全局应用上下文

云存储在上传下载时需要用到应用的上下文(Context)。为了方便在各个地方使用,建议封装一个全局Context工具类。

entry/src/main/ets/common目录下新建GlobalContext.ets

typescript 复制代码
import { common } from '@kit.AbilityKit';

export class GlobalContext {
  private static context: common.UIAbilityContext;

  public static initContext(context: common.UIAbilityContext): void {
    GlobalContext.context = context;
  }

  public static getContext(): common.UIAbilityContext {
    return GlobalContext.context;
  }
}

然后,在你的EntryAbility.ets文件的onCreate方法中进行初始化:

typescript 复制代码
import { GlobalContext } from '../common/GlobalContext';

export default class EntryAbility extends UIAbility {
  onCreate(want, launchParam) {
    // 初始化全局上下文
    GlobalContext.initContext(this.context);
  }
  // ... 其他代码
}

2.3 初始化存储实例

在需要使用云存储的页面或组件中,我们需要先获取存储实例(StorageBucket)。

typescript 复制代码
import { cloudStorage } from '@kit.CloudFoundationKit';

// 使用默认实例,建议只初始化一次
let storageBucket: cloudStorage.StorageBucket = cloudStorage.bucket();

三、核心功能实现

3.1 上传文件

上传文件的流程通常是:

  1. 使用PhotoViewPicker选择图片。
  2. 将图片复制到应用的缓存目录(因为云存储接口只支持上传缓存目录下的文件)。
  3. 调用uploadFile接口上传。
typescript 复制代码
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { BusinessError, request } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

@Component
export struct UploadPage {
  
  // 1. 选择图片
  async selectAndUpload() {
    try {
      let photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
      photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
      photoSelectOptions.maxSelectNumber = 1;
      
      let photoViewPicker = new photoAccessHelper.PhotoViewPicker();
      let photoSelectResult = await photoViewPicker.select(photoSelectOptions);
      
      if (photoSelectResult.photoUris.length > 0) {
        let fileUri = photoSelectResult.photoUris[0];
        let fileName = fileUri.split('/').pop() as string;
        // 构造缓存目录下的路径
        let cacheFile = GlobalContext.getContext().cacheDir + '/' + fileName;
        
        // 2. 复制文件到缓存目录
        this.copyFile(fileUri, cacheFile);
        
        // 3. 上传到云端,路径为 "avatar/文件名"
        this.uploadFile(cacheFile, `avatar/${fileName}`);
      }
    } catch (err) {
      hilog.error(0x0000, 'testTag', `选择图片失败: ${err.message}`);
    }
  }

  copyFile(srcPath: string, dstPath: string) {
    let srcFile = fs.openSync(srcPath);
    let dstFile = fs.openSync(dstPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
    fs.copyFileSync(srcFile.fd, dstFile.fd);
    fs.closeSync(srcFile);
    fs.closeSync(dstFile);
  }

  uploadFile(localPath: string, cloudPath: string) {
    storageBucket.uploadFile(GlobalContext.getContext(), {
      localPath: localPath,
      cloudPath: cloudPath
    }).then((task: request.agent.Task) => {
      // 监听上传进度
      task.on('progress', (progress) => {
        hilog.info(0x0000, 'testTag', `上传进度: ${JSON.stringify(progress)}`);
      });
      
      task.on('completed', () => {
        hilog.info(0x0000, 'testTag', `上传完成!`);
        // 上传完成后删除本地缓存文件
        fs.unlink(localPath);
      });
      
      task.on('failed', (err) => {
        hilog.error(0x0000, 'testTag', `上传失败: ${JSON.stringify(err)}`);
      });

      // 启动任务
      task.start((err: BusinessError) => {
        if (err) {
          hilog.error(0x0000, 'testTag', `启动任务失败: ${err.message}`);
        }
      });
    }).catch((err: BusinessError) => {
      hilog.error(0x0000, 'testTag', `创建任务失败: ${err.message}`);
    })
  }
  
  build() {
    // ... UI代码
  }
}

3.2 获取下载地址

上传成功后,我们需要获取图片的URL来展示。

typescript 复制代码
async getImageUrl(cloudPath: string) {
  try {
    let downloadURL = await storageBucket.getDownloadURL(cloudPath);
    hilog.info(0x0000, 'testTag', `图片地址: ${downloadURL}`);
    // 这里可以将downloadURL保存到状态变量中,在Image组件中显示
    return downloadURL;
  } catch (err) {
    hilog.error(0x0000, 'testTag', `获取地址失败: ${err.code}, ${err.message}`);
  }
}

3.3 下载文件到本地

有时候我们需要把文件下载下来保存到本地,而不是仅仅获取URL。

typescript 复制代码
downloadFile(cloudPath: string, localFileName: string) {
  storageBucket.downloadFile(GlobalContext.getContext(), {
    cloudPath: cloudPath,
    localPath: localFileName // 文件将保存在context.cacheDir目录下
  }).then((task: request.agent.Task) => {
    task.on('completed', () => {
      hilog.info(0x0000, 'testTag', `下载完成`);
    });
    
    task.start((err) => {
      if (err) {
        hilog.error(0x0000, 'testTag', `启动下载失败: ${err.message}`);
      }
    });
  });
}

3.4 删除文件

如果用户更换了头像,旧头像可以删除以节省空间。

typescript 复制代码
async deleteCloudFile(cloudPath: string) {
  try {
    await storageBucket.deleteFile(cloudPath);
    hilog.info(0x0000, 'testTag', `删除成功`);
  } catch (err) {
    hilog.error(0x0000, 'testTag', `删除失败: ${err.code}, ${err.message}`);
  }
}

3.5 获取文件列表

如果你想做一个相册功能,展示所有上传的图片,可以使用list接口。

typescript 复制代码
async listFiles() {
  try {
    // 获取avatar目录下的文件
    let result = await storageBucket.list('avatar/');
    hilog.info(0x0000, 'testTag', `文件列表: ${JSON.stringify(result)}`);
    // result.files 包含了所有文件名
  } catch (err) {
    hilog.error(0x0000, 'testTag', `获取列表失败: ${err.code}, ${err.message}`);
  }
}

四、元数据管理

元数据(Metadata)就是"关于数据的数据",比如文件大小、类型,或者你自定义的标签。

4.1 设置元数据

比如我们想给上传的图片打个标签:

typescript 复制代码
async setFileMetadata(cloudPath: string) {
  try {
    await storageBucket.setMetadata(cloudPath, {
      customMetadata: {
        category: "portrait",
        tag: "hd"
      }
    });
    hilog.info(0x0000, 'testTag', `设置元数据成功`);
  } catch (err) {
    hilog.error(0x0000, 'testTag', `设置失败: ${err.message}`);
  }
}

4.2 获取元数据

下载前可以先看看文件多大,或者查看自定义标签:

typescript 复制代码
async getFileMetadata(cloudPath: string) {
  try {
    let metadata = await storageBucket.getMetadata(cloudPath);
    hilog.info(0x0000, 'testTag', `文件大小: ${metadata.size}, 类型: ${metadata.contentType}`);
  } catch (err) {
    hilog.error(0x0000, 'testTag', `获取元数据失败: ${err.message}`);
  }
}

五、常见问题排查

在使用云存储时,你可能会遇到以下错误,这里有几个锦囊:

  1. 404 Product does not exist

    • 原因:云存储服务没开通。
    • 解决:去AGC控制台开通云存储服务。
  2. 403 Forbidden

    • 原因:权限问题,通常是签名不对或者没登录。
    • 解决
      • 检查应用的签名方式(推荐使用自动签名)。
      • 如果是需要登录的场景,确保已经通过AuthProvider获取了用户凭据。
  3. 404 Not Found (初始化时)

    • 原因:指定的存储实例名称不对。
    • 解决 :检查cloudStorage.bucket('name')里的名称是否和云端一致。

六、总结

云存储为应用提供了强大的文件管理能力,结合前两篇的云函数和云数据库,你已经具备了开发一个完整全栈应用的核心能力。

下一篇教程,我将介绍预加载服务,看看如何让你的应用"快人一步"!

相关推荐
A懿轩A2 小时前
【2026 最新】Flutter 编译开发 OpenHarmony 工程目录结构全解析
flutter·harmonyos·openharmony·开源鸿蒙
纯爱掌门人2 小时前
鸿蒙端云一体化开发(四):预加载
华为·harmonyos
不会写代码0003 小时前
Flutter 框架跨平台鸿蒙开发 - 免费英语口语评测:AI智能发音纠正
人工智能·flutter·华为·harmonyos
木斯佳3 小时前
HarmonyOS 6实战(源码教学篇)— AVSession Kit 新特性【仿某云音乐实现媒体会话和后台播放管理】【API20】
华为·harmonyos·媒体
Miguo94well3 小时前
Flutter框架跨平台鸿蒙开发——失物招领APP的开发流程
flutter·华为·harmonyos
实时云渲染dlxyz66883 小时前
鸿蒙系统下,点盾云播放器使用一段时间后忽然读取不到视频解决方法
音视频·harmonyos·点盾云播放·纯鸿蒙系统播放·应用权限授权
小风呼呼吹儿3 小时前
Flutter 框架跨平台鸿蒙开发 - 全国公积金查询:智能公积金管理助手
flutter·华为·harmonyos
大雷神3 小时前
HarmonyOS智慧农业管理应用开发教程--高高种地--第11篇:任务管理与提醒系统
harmonyos
翰德恩咨询3 小时前
DSTE咨询洞见:华为战略管理体系的进化之路
华为·华为战略·dste