尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

鸿蒙端云一体化云存储实战:从入门到生产环境避坑

鸿蒙端云一体化云存储实战:从入门到生产环境避坑 鸿蒙端云一体化开发三云存储我琢磨了好一阵子才动笔。前两篇分别聊了云函数和云数据库这篇把云存储单独拎出来讲是因为它在实际项目里藏的东西比表面上多得多。很多人觉得云存储不就是上传下载文件嘛照着官方文档跑通demo就完事了可真到了生产环境token过期、权限配置、目录规划、断点续传这些问题一个个全冒出来了。这篇我把从入门到实战踩过的坑、总结的经验按一个完整项目的推进顺序捋一遍希望能帮正在做鸿蒙应用开发的朋友少走几步弯路。先说清楚这篇要讲的是什么。云存储Cloud Storage是华为AppGallery Connect提供的一项服务本质上是给应用挂一个云端文件仓库图片、视频、音频、文档随便往里扔通过SDK完成上传、下载、删除、列举这些操作。它解决的核心问题是本地存储空间不够用、用户换设备数据不跟随、多端数据不同步。适合的场景包括头像上传、用户相册、聊天附件、App内发布的图文内容、文件分享等。适合谁来参考呢正在做鸿蒙应用且选了端云一体化这套体系或者对Cloud Storage感兴趣但不想去翻几十页文档的开发者这篇的内容你都能直接拿去用。1. 整体设计与思路拆解1.1 云存储在整个端云一体化里的角色定位鸿蒙的端云一体化方案说白了就是华为把后端那摊子事打包成了一个统一的开发平台——AppGallery ConnectAGC。云函数处理逻辑云数据库存结构化数据云存储管非结构化文件三个服务配合起来就是一个完整的小后端。我一开始有件事没想通既然云数据库里能存字符串为什么不把文件转成base64塞进去后来被数据量教做人了。一张手机照片随便就是几MB转成base64体积还要膨胀三分之一左右云数据库是按读写次数和存储容量计费的存几张照片没感觉存几百张试试账单会告诉你什么叫心疼。云存储的优势在于它是为文件设计的有专门的上传下载通道、有断点续传、有目录结构、有粗粒度的访问权限控制而且计价方式是按存储量和CDN流量走的和数据库那种按操作数计费的逻辑完全不是一个路数。在整体设计上我的建议是把云存储当成文件层来用和云数据库保持清晰的边界。云数据库负责记录文件的元信息比如文件的cloudPath、大小、上传时间、归属用户ID、是否公开云存储负责实际文件的存取。这样设计的好处是查询列表的时候不需要扫描文件系统数据库里一个collection就搞定了而且删除文件的时候只要先删数据库记录再删云存储文件逻辑非常清爽。1.2 认证机制的设计逻辑云存储访问的第一道门槛是认证。AGC云存储使用的是基于华为账号体系的token鉴权机制不从华为账号体系走的人用起来确实有点绕但理解了设计逻辑就觉得合理了。整个过程是这样的应用启动后SDK会向AGC服务器发起一次匿名或实名认证服务器返回一个token。这个token的默认有效期我印象中是24小时左右过期之后SDK会自动刷新。在token有效期内所有云存储操作都是合法的。这种做法和很多服务端直传方案里的STS临时凭证是一个套路只是STS通常只给几小时的有效期AGC给了一天日常开发还算宽裕。实操层面要注意的点是token的获取和刷新是SDK自动完成的开发者理论上不用管。但一旦你的应用做了多端登录、账号切换这类操作必须自己处理token的失效问题。我在开发的时候就碰到过这种场景用户A上传到一半退出登录换用户B登录结果上传还在继续文件进了A的目录。后来我的做法是在账号切换的时候先销毁AGC实例再重新初始化把上传任务彻底断掉。1.3 网络协议与存取效率的取舍云存储在传输层用的底层协议其实是HTTP/HTTPSSDK把上传下载的细节都封装好了开发者甚至不用关心分片是怎么切的。但从效率设计的角度你还是需要理解一下它的传输策略否则遇到性能问题不知道从哪里下手。AGC云存储的上传走的是就近接入 分片并行的策略。SDK会把一个文件切成多个分片然后并发上传最后在服务端合并。这种方式对弱网环境的改善非常明显一个50MB的文件在普通4G网络下比整包上传快很多。下载走的则是CDN加速首次访问会回源到存储节点之后就从边缘节点走了所以热文件的下载速度很快。设计上的另一个取舍是安全规则。云存储的访问控制粒度比云数据库粗它不能做到某一条记录只能被特定用户读而只能通过目录级的通配规则来控制。你设置的规则本质上就是一套JSON格式的声明式配置声明哪些路径下的文件可以被谁读、谁写。这种设计虽然灵活度不如数据库的行级权限但对于文件这种要么整包可见、要么整包不可见的资源来说已经足够了。2. 核心细节解析与实操要点2.1 存储实例的创建与配置藏着最多坑在代码层面动手之前得先把云存储的实例在AGC控制台上创建好。这一步看着简单实际上有几个关键的配置项稍不注意就直接影响线上使用。进入AppGallery Connect控制台找到你的项目下的构建 - 云存储菜单首次进入会让你开通服务然后就是创建存储实例。实例的名字是个重点你可以自定义一个bucket名称但一旦创建这个名称就绑定了后续改不了。如果起名起得不规范比如用了一堆特殊字符后面在拼接完整路径时会很麻烦。我的建议是纯小写字母加数字加短横线别用下划线也别用大写字母。创建实例后你会在服务配置里看到一个类似这样的字段agc_cloud_storage: { bucket: your-bucket-name }这个bucket名称是拼接云存储完整路径的一部分后面所有的文件操作都要基于它来定位。配置里还需要重点关注一下安全规则这个功能。云存储的安全规则类似一个JSON配置文件在这个文件里声明谁有权读写哪些路径。默认规则通常是允许所有已认证用户读写这对demo项目没毛病但真要上线一个用户量大的应用这种配置就是灾难。后面我会专门讲怎么设计安全规则。2.2 目录结构设计把自己的老巢规划好云存储虽然没有真正的文件系统但它模拟了一套目录结构。路径格式长这样gs://your-bucket-name/user-avatar/12345.jpg所以你在上传文件时必须自己规划好目录结构。这个规划直接影响后续的权限控制、数据统计、数据迁移成本而且改起来特别麻烦。我根据自己的实际项目教训总结了一套比较稳妥的规划方式/user/{uid}/存放用户私有数据如私密文件、草稿、未发布内容/user/{uid}/avatar.jpg存放头像/public/存放公开资源比如用户发布的、所有人可见的图片/temp/存放临时文件用于上传中转或者不需要长期保留的数据/app-assets/存放App内置资源更新包为什么要把公开和私有分开目录因为安全规则是路径级别的。如果公开文件和私有文件混在一起你想要公开目录所有人可读、私有目录仅本人可读写规则就很难写要么太宽要么太窄。分开之后规则就是几条明确的路径匹配逻辑。经验之谈目录结构设计越扁平越好。文件名里可以带语义但目录层级控制在两层以内。为什么因为列举文件时层级越深意味着路径前缀越长匹配效率越低可读性也越差。我见过一个项目建了五层目录每次定位一个文件都要写一大串路径代码可读性差到让人崩溃。2.3 权限与安全规则必知必会的JSON规则语法云存储的安全规则是授权访问的关键。这种规则语法类似于一套简易的声明式语言可用的变量不多但表达力足够覆盖大部分场景。默认规则长这样{ rules: { read: true, write: true } }这表示所有操作包括未登录用户全都放行。开发阶段测试用没问题但上线前必须收紧。常见的规则设计参考如下{ rules: { read: true, write: auth ! null, user/{uid}/**: { read: auth.uid $uid, write: auth.uid $uid } } }解释一下第一条read为true意思是存储桶内所有文件都可读这样头像、公开图片之类的资源就能被任何人访问不需要额外鉴权。write字段设为auth ! null表示只有登录用户才能写文件匿名用户不行。第三段是路径级规则匹配user/开头的路径$uid是路径通配符规则要求访问者的auth.uid必须等于路径里的uid这样每个用户只能读自己的私有文件。我强烈建议安全规则用最小权限原则能不给读权限就不给能给读权限就不给写权限路径能细化就细化。在上线前专门花半小时审视一遍规则能避免大量安全隐患。真出恶性事故的方式通常就是安全规则写得和默认规则一样宽。2.4 初始化SDK不搞对会卡在你没耐心继续看的地方初始化是第一个脚本级操作也是很多人第一次卡住的地方。鸿蒙开发环境我用的是HarmonyOS NEXT DevEco Studio的Stage模型。首先在entry/oh-package.json5里加依赖dependencies: { hw-agconnect/cloud-storage: ^1.0.0 }然后在入口模块的UIAbility里加载配置文件。AGC SDK是从agconnect-services.json文件读取项目配置的这个文件在AGC控制台的项目设置里下载放到entry/src/main/resources/rawfile/目录下。初始化代码import { hilog } from kit.PerformanceAnalysisKit; import { agConnect } from hw-agconnect/api; import { cloudStorage } from hw-agconnect/cloud-storage; Entry Component export struct Index { aboutToAppear(): void { const instanceConfig { platform: harmony, // 从agconnect-services.json读取无需手动填写 filePath: rawfile/agconnect-services.json }; agConnect.initialize(instanceConfig) .then(() { hilog.info(0x0000, AGConnect, init success); // 初始化云存储实例 const storage cloudStorage.getInstance(); this.storage storage; }) .catch((err: Error) { hilog.error(0x0000, AGConnect, init failed: JSON.stringify(err)); }); } }注意初始化这个动作是异步的。很多新人踩的坑是在初始化还没完成的时候就去调上传下载结果拿到了undefined或者报错说not initialized。正确的姿势是把云存储相关操作统一放进初始化成功的回调或者封装的Promise里。3. 实操过程与核心环节实现3.1 完整项目级封装初始化、Token管理、引用获取直接调SDK拿结果当然快但那会让业务代码和SDK耦合得非常紧。我强烈建议在项目里先做一层薄薄的封装把初始化、实例获取、路径拼接这些重复劳动统一收编。先定义一个管理类import { agConnect } from hw-agconnect/api; import { cloudStorage, StorageManagement, TaskStatus } from hw-agconnect/cloud-storage; export class CloudStorageManager { private static instance: CloudStorageManager | null null; private storage: StorageManagement | null null; private initPromise: Promisevoid | null null; private constructor() {} static getInstance(): CloudStorageManager { if (!CloudStorageManager.instance) { CloudStorageManager.instance new CloudStorageManager(); } return CloudStorageManager.instance; } initialize(): Promisevoid { if (this.initPromise) { return this.initPromise; } this.initPromise agConnect.initialize({ platform: harmony, filePath: rawfile/agconnect-services.json }).then(() { this.storage cloudStorage.getInstance(); }).catch((error: Error) { this.initPromise null; throw error; }); return this.initPromise; } getStorage(): StorageManagement { if (!this.storage) { throw new Error(CloudStorage not initialized. Call initialize() first.); } return this.storage; } }为什么要单例因为存储实例是全局共享的资源重复创建既浪费资源还可能产生token相互覆盖的诡异问题。在我自己用let变量到处传来传去踩了几次坑之后痛定思痛写成了单例模式。然后封装路径拼接的工具函数export function buildCloudPath(prefix: string, uid: string, fileName: string): string { const safeUid uid.replace(/[^a-zA-Z0-9_-]/g, _); const safeFileName fileName.replace(/[\/\\:*?|]/g, _); return ${prefix}/${safeUid}/${safeFileName}; }路径里的非法字符一定要处理不然上传一个名字里带/的文件分分钟给你创建出意外的子目录。我也是被用户上传了一个名叫a/b.jpg的文件直接整蒙圈眼睁睁看着桶里多了一个a目录。3.2 上传功能核心参数从哪来进度条怎么做上传这块要讲的东西其实不多全在细节里。import { cloudStorage, UploadTask } from hw-agconnect/cloud-storage; async function uploadFile(cloudPath: string, fileUri: string, onProgress?: (percent: number) void): Promisestring { const manager CloudStorageManager.getInstance(); const storage manager.getStorage(); const uploadTask: UploadTask storage.upload(cloudPath, fileUri); if (onProgress) { uploadTask.on(progress, (status: TaskStatus) { const percent Math.round((status.transferredBytes / status.totalBytes) * 100); onProgress(percent); }); } try { const result await uploadTask; return result.downloadUrl; } catch (error) { throw error; } }这里要注意几个关键参数第一个是cloudPath云端的完整路径必须以bucket为根。如果需要访问完整URLSDK会在上传结果里返回一个可用于下载的URL不需要自己拼。第二个是fileUri本地文件的URI。这个不是随便传个字符串完事的必须是一个有效的file://开头或通过文件选择器拿到的URI。我在实际开发中用的是PhotoAccessHelper来获取图库图片的URI然后用fs.open检查一下文件是否真实存在再交给上传方法这样能提前排查很多无效路径问题。第三个是上传任务的生命周期。UploadTask对象有pause、resume、cancel方法。踩坑经验是一旦页面销毁你必须及时cancel掉上传任务否则回调会继续触发更新到已经销毁的UI组件上轻则报null错误重则内存泄漏。很多人在上传时会特意加一个压缩逻辑在本地把图片压缩到一定尺寸再上传。AGC云存储本身不限制图片大小但大图和原图在流量和加载速度上差异很大尤其是移动端,压缩后上传能明显提升体验。我现在的处理方式是头像类图片统一压缩到200x200以内再传封面图压到1200宽原图保留在相册就好。3.3 下载与缓存策略直接getFile还是走URL云存储的下载有两种方式一种是通过网络的URL直接访问另一种是调用SDK把文件下载到本地指定目录。先看第一种最简单也更常用。上传成功后我习惯把downloadUrl保存到云数据库里作为文件的公开访问地址。前端展示图片时直接Image组件加载这个URL不需要额外鉴权浏览器和ArkUI都能正常访问。这个URL就是CDN加速的加载速度实测相当可观。再看第二种如果业务逻辑需要在应用里把文件放在本地缓存以便离线访问或反复处理就调SDK的下载方法async function downloadFile(cloudPath: string, localPath: string, onProgress?: (percent: number) void): Promisevoid { const manager CloudStorageManager.getInstance(); const storage manager.getStorage(); const downloadTask storage.download(cloudPath, localPath); if (onProgress) { downloadTask.on(progress, (status: TaskStatus) { const percent Math.round((status.transferredBytes / status.totalBytes) * 100); onProgress(percent); }); } try { const result await downloadTask; if (result.status SUCCESS) { hilog.info(0x0000, CloudStorage, download success: localPath); } } catch (error) { hilog.error(0x0000, CloudStorage, download failed: JSON.stringify(error)); } }下载策略这里我有一条经验不要每次都从云端下载一定要做本地缓存判断。我的做法是在本地缓存目录里先检查文件是否存在以及创建时间是否是当天若存在且当天已经下载过就直接用本地文件否则再触发下载。这个策略配合进度回调实测能把同一个文件的平均加载时间从几百毫秒降到几十毫秒。缓存目录的管理也有讲究。鸿蒙应用可以用getContext().cacheDir拿缓存目录这个目录系统可能随时清理适合放临时缓存如果要长期保留的离线资源比如用户下载的离线包得放到filesDir下。两种用途别搞混免得系统一回收缓存你要用的文件也没了。3.4 列举文件和删除操作管好自己的库房工作里有时候需要按目录列举云端文件典型场景是我的相册页面一个用户上传了一堆图片总不能每次去翻数据库再拼URL吧可以走云存储的list接口。import { cloudStorage, ListResult, ListOptions } from hw-agconnect/cloud-storage; async function listFiles(prefix: string): PromiseListResult { const manager CloudStorageManager.getInstance(); const storage manager.getStorage(); const options: ListOptions { prefix, maxResults: 100 }; try { const result: ListResult await storage.list(prefix, options); return result; } catch (error) { throw error; } }需要注意list()返回的是一次最多100条的结果如果文件总数超过100条接口会返回一个pageToken需要拿着这个token再请求下一页。这个分页设计和大列表处理很常见问题在于很多新人不看文档以为一次list就能列出全部结果页面上永远只显示前100条排查了很久都找不到原因。删除操作比较简单async function deleteFile(cloudPath: string): Promisevoid { const manager CloudStorageManager.getInstance(); const storage manager.getStorage(); try { await storage.delete(cloudPath); hilog.info(0x0000, CloudStorage, delete success: cloudPath); } catch (error) { hilog.error(0x0000, CloudStorage, delete failed: JSON.stringify(error)); } }不过我建议删除文件时不要只删云存储数据库里的元信息也得同步处理否则就出现文件不在了记录还在的脏数据。处理方式就是在同一个业务函数里先删库记录再删存储文件。如果你用了云函数做统一入口那就更规范了在云函数里把这两个操作串起来前端只调用云函数就好。3.5 访问URL的构造与CDN加速说明前面说上传结果里会给一个downloadUrl但有时候你提前规划好了文件路径或者需要把URL分享给其他人这时候可以自己构造访问地址。云存储的URL格式大致是https://{bucket-name}.{region}.agccloudstorage.example.com/{cloudPath}region是什么是存储实例所在的地域。如果你在创建实例时选择的默认地域是亚洲-新加坡那region就是类似sg这样的标识。这个信息在AGC控制台云存储的概览页能看到也可以从SDK下载返回的完整URL里推断出来。自己拼URL有一个好处可以结合安全规则做到需要登录才能访问的私有资源用带签名参数的URL临时授权访问。虽然这条进阶线路的操作比较多但了解原理之后排查问题会方便很多。大多数情况下直接把上传接口返回的downloadUrl存起来用就行简单可靠没必要自找麻烦。4. 常见问题与排查技巧实录4.1 经典报错初始化失败与文件权限问题我见过的项目里初始化失败占了云存储问题的一半以上。报错信息通常是INIT_FAILED或者getToken failed。排查看几个点agconnect-services.json是否真的放到了rawfile/目录下文件有没有被编译器过滤初始化代码有没有在UIAbility的onCreate里被调用有些开发者放在了一个冷门生命周期里结果初始化完成得太晚。控制台创建存储实例时指定的应用和当前正在调试的应用包名是否一致包名不一致token就签不了名初始化自然失败。如果用的是模拟器可能存在网络代理导致token请求失败的问题这时候试试真机。权限相关的报错常见是PERMISSION_DENIED或者403。出现这种情况的先不用怀疑SDK而是去控制台检查安全规则。我遇到过一次很隐蔽的情况规则的路径通配符写错位置导致/public/avatar目录明明有allow但请求就是被拒了。后来把规则JSON下载下来逐行拆解才发现在auth ! null的判定里还嵌套了一层request.path匹配逻辑写死了前缀通配符没覆盖到。4.2 上传慢、失败多怎么定位上传慢这事首先要区分是网络问题还是SDK配置问题。最简单的验证方式是在同一网络环境下使用第三方工具直接上传一个同样大小的文件到同地域看看速度差异。如果第三方直传也慢说明是网络链路的问题可以考虑调整分片大小。AGC云存储的分片大小是SDK内部管理的确实可以通过初始化参数调但我不建议动这个参数。默认分片在大多数移动网络下都表现得很稳定强行调大分片在某些路由器上反而会导致连接重置。如果第三方直传快、SDK上传慢重点排查两件事一是证书配置AGC SDK在某些开发环境里需要配置网络安全级别的TrustAnchor相关设置二是代理设置开发阶段开了抓包代理的话HTTPS握手会变慢应该给调试代理加上例外名单。另外上传过程中突然失败最常见的罪魁祸首是App进入后台后网络被系统挂起。手机锁屏或者切后台超过一段时间网络连接会被收回。这种情况的解法是在传大文件前提示用户保持前台或者把上传任务放到后台长任务里执行配置好对应的长时任务权限。4.3 列举文件的分页问题与时限问题分页问题前面提过了很多人的列表只显示100条就是没做分页循环。我在这里直接给一段可以循环取值直到pageToken为空的示例async function listAllFiles(prefix: string): Promisestring[] { const manager CloudStorageManager.getInstance(); const storage manager.getStorage(); let pageToken: string | undefined; const allFiles: string[] []; do { const options: ListOptions { prefix, maxResults: 100, pageToken }; const result: ListResult await storage.list(prefix, options); allFiles.push(...result.files.map(f f.name)); pageToken result.pageToken; } while (pageToken); return allFiles; }还有一类问题也容易在列表页翻车list返回的文件列表是按字典序排序的不是按上传时间或者更新时间排序所以做最近上传优先这种排序时必须依赖云数据库的元信息字段不能指望云存储自己按时间排序。我身边不少人一开始没意识到这点做完发现顺序不对白排半天。4.4 常见问题速查表到这里把我在实际开发中频繁遇到、也经常在技术社区看到的问题整理成一张表方便你定位问题现象可能原因排查与解决初始化返回INIT_FAILEDagconnect-services.json缺失或放错目录检查rawfile目录、包名是否匹配上传报403安全规则里write权限不匹配控制台查看安全规则检查auth条件上传成功但downloadUrl访问404bucket名称或cloudPath拼错用存储桶里实际文件路径比对别用自定义拼接的路径列表只返回100条未处理分页检查pageToken循环下载大量文件时OOM内存中同时缓存太多下载任务的流改用串行任务或限制并发数图片加载模糊压缩尺寸过小或CDN旧缓存检查压缩参数更新文件名或带版本参数Token过期导致切换账号后任务写错目录账号切换后旧任务未被取消在切换账号前cancel所有上传下载任务并重新初始化这张表你可以直接截图或者收藏真踩到相关问题了再翻对照比自己翻官方issue效率高很多。4.5 避坑清单与经验总结最后分享几条带节奏的结论不是泛泛而谈都是从项目里磨出来的经验。第一云存储的路径规划要在一开始就定死不要想着后来改。目录层级、命名规则、公开私有划分这些一旦上线就很难动了因为文件存量在你总不能写个脚本遍历所有记录改路径。我做过一次迁移改了三个月才改干净那滋味不好受。第二安全规则记得按最小权限来写。哪怕项目再小、再赶也不能直接用默认的全开放规则上线。我有一次忘了改直接发布结果上线当天用户上传的相册就被爬虫抓了个遍。这条底线必须要守。第三缓存策略和断点续传一定要做。云存储作为云端基础设施面向的是通用场景它不会为你特定的使用习惯专门优化。你在应用层做好本地缓存判断、文件校验、任务管理才能真正做到体验流畅、流量省心。第四结合云函数一起设计整个流程。我真正用顺这套体系是在把文件处理逻辑放进云函数之后。前端只传文件路径相关的参数云函数负责拿到临时凭证、处理文件转存、调用图片处理服务这样云存储就不再是一个独立的文件仓库而是整个端云协作里的一个可靠底板。关于云存储在鸿蒙端云一体化里的经验暂时就回顾到这里。如果你正在开发鸿蒙应用建议先从小功能入手比如给应用加一个简单的头像上传把整套流程走通再慢慢扩展。后面我打算继续写云函数和云数据库的实战细节如果你有具体想了解的点也可以评论区交流。
返回列表