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

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

在现代应用中,我们经常需要处理用户上传的图片、视频或者文档。如果自己搭建文件服务器,不仅要考虑带宽、存储容量,还要操心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')里的名称是否和云端一致。

六、总结

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

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

相关推荐
一只大侠的侠2 小时前
Flutter开源鸿蒙跨平台训练营 Day 10特惠推荐数据的获取与渲染
flutter·开源·harmonyos
御承扬8 小时前
鸿蒙NDK UI之文本自定义样式
ui·华为·harmonyos·鸿蒙ndk ui
前端不太难9 小时前
HarmonyOS 游戏上线前必做的 7 类极端场景测试
游戏·状态模式·harmonyos
大雷神9 小时前
HarmonyOS智慧农业管理应用开发教程--高高种地--第29篇:数据管理与备份
华为·harmonyos
讯方洋哥9 小时前
HarmonyOS App开发——关系型数据库应用App开发
数据库·harmonyos
巴德鸟10 小时前
华为手机鸿蒙4回退到鸿蒙3到鸿蒙2再回退到EMUI11 最后关闭系统更新
华为·智能手机·harmonyos·降级·升级·回退·emui
一起养小猫10 小时前
Flutter for OpenHarmony 实战_魔方应用UI设计与交互优化
flutter·ui·交互·harmonyos
一只大侠的侠11 小时前
Flutter开源鸿蒙跨平台训练营 Day7Flutter+ArkTS双方案实现轮播图+搜索框+导航组件
flutter·开源·harmonyos
那就回到过去11 小时前
VRRP协议
网络·华为·智能路由器·ensp·vrrp协议·网络hcip
相思难忘成疾12 小时前
通向HCIP之路:第四步:边界网关路由协议—BGP(概念、配置、特点、常见问题及其解决方案)
网络·华为·hcip