ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

HarmonyOS社交通讯应用开发 27 :数据模型定义与附件打包

HarmonyOS社交通讯应用开发 27 :数据模型定义与附件打包 数据模型定义与附件打包引言分布式数据对象同步的是可序列化的键值属性分布式文件系统同步的是文件实体。两者之间需要一个桥梁把正文 附件文件组织成一个能被分布式数据对象完整同步的数据结构同时把文件在哪个目录、叫什么名字、多大描述清楚让接收端能按图索骥地把文件找回来。这个桥梁就是本工程的数据模型层——entry/src/main/ets/model/ContentInfo.ets里的ContentInfo类、MediaInfo接口、MediaType枚举以及配套的getAssetInfo工具函数。本文把这三个文件串起来讲清楚三件事ContentInfo的七个字段分别代表什么MediaInfo/MediaType如何描述一个媒体附件flatAssets()与getAssetInfo()如何把对象数组压平成可同步的键值。一、ContentInfo一篇文章的全部现场先看entry/src/main/ets/model/ContentInfo.ets中的类定义exportclassContentInfo{mainTitle:string|undefined;// 标题textContent:string|undefined;// 正文mediaUriArray:ArrayMediaInfo |undefined;// 媒体列表PixelMap / 视频 uriisShowLocalInfo:boolean|undefined;// 是否展示位置信息列表isAddLocalInfo:boolean|undefined;// 是否处于添加位置状态selectLocalInfo:string|undefined;// 已选中的发布位置attachments: commonType.Assets|undefined;// 附件描述集合Asset 数组constructor(...) {/* 七个字段逐一赋值 */}flatAssets():object{ ... } }七个字段完整对应编辑页面的全部状态字段类型含义对应 UImainTitlestring文章标题标题输入框textContentstring文章正文正文输入区mediaUriArrayArrayMediaInfo内存态媒体列表媒体九宫格isShowLocalInfoboolean是否显示位置列表位置列表开关isAddLocalInfoboolean是否处于添加位置流程添加位置按钮状态selectLocalInfostring选中的发布位置位置展示文本attachmentscommonType.Assets落盘态附件描述接续时传给对端注意一个有意思的分工mediaUriArray是内存态装着 PixelMap、uri用于本地 UI 渲染attachments是落盘态装着 Asset 描述用于跨设备传递。发送端onContinue里两者都进了ContentInfo——mediaUriArray被JSON.stringify进wantParam做轻量备份attachments经flatAssets()进分布式数据对象做主通道。为什么要把编辑现场收敛成一个模型类而不是在onContinue里临时拼键值三个理由单一数据源——发送端打包与接收端还原共用同一个模型字段名不会因两端手写而漂移可序列化边界清晰——模型层明确区分了能进分布式数据对象的字段与只在本端使用的字段如 PixelMap可扩展——未来新增一个编辑状态比如话题标签只需要给ContentInfo加一个字段两端代码同步感知。模型层是接续工程的地基值得为它单独建文件。二、MediaInfo 与 MediaType一个附件的自我描述MediaInfo描述编辑区里的一个媒体项MediaType区分类型export interface MediaInfo { imagePixelMap?: PixelMap;//图片的位图对象图片时存在 mediaName: string;//媒体文件名去扩展名 mediaType: MediaType;//类型image或videovideoUri?: string;//视频的 uri视频时存在 } export enum MediaType { MEDIA_IMAGE image, MEDIA_VIDEO video}MediaInfo是可选项区分类型的设计图片项带imagePixelMap视频项带videoUri两个可选字段互斥出现mediaType则明确标注类型。UI 渲染时AddMedia.ets的addMediaBuilder正是用if (item.imagePixelMap) ... else if (item.videoUri)来分叉展示图片与视频的。每个字段在编辑期都有明确的产生来源追踪一下AddMedia.ets中的赋值点**mediaName**图片取asset.displayName去掉扩展名的部分asset.displayName.substring(0, displayName.indexOf(.))视频及拖拽/粘贴来的媒体取util.generateRandomUUID()。它是后续写文件与解析 Asset.name 的核心。**imagePixelMap**相册选图通过asset.getThumbnail得到拖拽/粘贴通过image.createImageSource(buffer).createPixelMap()得到。**videoUri**粘贴/跨端投递视频时取自pasteData.getPrimaryUri()或投递数据解码出的 uri 字符串。可见MediaInfo是编辑期各类媒体入口的归一化出口——不管图片来自相册、拖拽还是粘贴最终都归一为{ imagePixelMap, mediaName, mediaType }视频同理归一为{ videoUri, mediaName, mediaType }。归一化让后续的 UI 渲染、写分布式文件、打包 Asset 都只面对一种结构这是接口设计带来的简洁性。MediaType枚举值值得注意MEDIA_IMAGE image、MEDIA_VIDEO video是普通字符串而非数字。这并非随意为之——它直接决定了第 28 篇里attachment.name的解析方式name形如image_xxx/video_xxx按_分割就能还原出类型也让类型信息在 JSON 序列化、日志打印、跨设备传递时保持可读。三、flatAssets把数组压平成可同步键值flatAssets是数据模型里最巧妙的一个方法flatAssets():object{ let obj:objectthis;if(!this.attachments) {returnobj; }for(let i 0; i this.attachments.length; i) { obj[attachments${i}] this.attachments[i]; }returnobj; }它做了什么把attachments数组的每个元素拆出来以attachments0、attachments1、attachments2……为键挂到对象自身上最后返回this本身。为什么要这么做这要从分布式数据对象的同步机制说起第 24 篇。分布式数据对象以属性键值对为同步单位对象的每个字符串键对应一个可序列化的值框架按属性粒度做合并与冲突处理。数组本身可以作为属性值同步但数组内元素的更新粒度很粗——整数组被当作一个值整体覆盖不利于框架按附件维度管理数据。而把数组展开成attachments0、attachments1这样的独立属性后每个附件成为独立的同步单元属性级同步粒度更细键名即索引接收端重建时obj[attachments i]即可按序取回序列化结构扁平跨设备传输与日志排查都更直观。因此发送端onContinue里let source contentInfo.flatAssets();之后distributedDataObject.create(this.context, source)创建出的分布式数据对象的属性就是mainTitle、textContent、isShowLocalInfo、isAddLocalInfo、selectLocalInfo、attachments0、attachments1……接收端restored后取this.distributedObject[attachments]得到 Asset 数组对象属性访问自动支持attachments键——flatAssets 保留了原attachments字段两者并存数组完整版 逐项展开版。实现上还有一个容易被忽略的细节let obj: object this;直接复用了ContentInfo实例本身flatAssets是在原对象上追加键而不是新建对象。这样create(source)之后分布式数据对象同时拥有七个原始字段 attachmentsN 展开键属性完整、无复制开销。if (!this.attachments)的早退保证了没有附件时对象干净利落——接收端restored后取到attachments为undefined时for循环自然不执行媒体列表为空数组逻辑依旧正确。attachmentsN这类动态键名还有一层好处接收端不需要知道总共有几个附件。它只读attachments数组遍历即可而展开键attachments0/1/2...更像是框架层同步粒度的体现业务代码基本不直接碰它们。换言之flatAssets的展开主要是为了分布式数据对象好同步而非为了业务可读性——理解这一点就能明白为什么说它是打包而不是序列化。四、getAssetInfo把 MediaInfo 转成 AssetMediaInfo是内存态描述跨设备传递需要落盘态描述——commonType.Asset。转换函数在entry/src/main/ets/utils/FileUtil.etsexportfunctiongetAssetInfo(context: common.UIAbilityContext, append: MediaInfo): commonType.Asset{letfilePath context.distributedFilesDir/ append.mediaName;letattachment: commonType.Asset;try{ fileIo.statSync(filePath);// 确认文件存在leturi: string fileUri.getUriFromPath(filePath);// 路径转 uriletstat fileIo.statSync(filePath);// 取文件元信息attachment {name:${append.mediaType}_${append.mediaName},// 形如 image_uuid / video_uuiduri: uri,path: filePath,createTime: stat.ctime.toString(),modifyTime: stat.ctime.toString(),size: stat.size.toString() }; hilog.info(DOMAIN,TAG,FORMAT,[getAssetInfo] attachments:${JSON.stringify(attachment)}); }catch(err) { hilog.error(DOMAIN,TAG,FORMAT,StatSync failed. Cause code:${err.code}, message:${err.message}); }returnattachment!; }commonType.Asset是分布式数据管理提供的标准资产描述类型kit.ArkData的commonType命名空间字段包括name、uri、path、createTime、modifyTime、size等。getAssetInfo做了四件事确认文件存在fileIo.statSync(filePath)检查分布式文件目录下确实存在该媒体文件这是第 26 篇writeDistributedFile写入的成果。路径转 urifileUri.getUriFromPath(filePath)生成标准文件 uri供远端通过 uri 直接访问分布式文件。提取元信息再次statSync拿到ctime创建时间与size字节数字符串化后填入 Asset。构造 name${append.mediaType}_${append.mediaName}把类型和文件名拼成一个键——这就是第 28 篇fileCopy里attachment.name.substring(0, indexOf(_))还原类型的依据。例如图片myphoto生成image_myphoto视频 UUIDa1b2...生成video_a1b2...。name的拼装看似简单实则同时完成了三件事类型标记image/video、文件名传递接收端按后半段去distributedFilesDir找文件、与 MediaInfo.mediaType 的字符串约定呼应枚举值恰好是可拼接的普通字符串。再把commonType.Asset的六个字段逐个说明理解它们在还原阶段各自的用途字段取值接收端用途nameimage_uuid/video_uuid解析类型与文件名第 28 篇fileCopyurifileUri.getUriFromPath(filePath)结果视频直接播放图片作后备访问通道path发送端分布式目录的绝对路径日志排查、路径溯源createTimestat.ctime字符串展示/排序示例未深度使用modifyTimestat.ctime字符串同上sizestat.size字符串接收端分配读取 Buffer 的大小依据注意size是字符串——这是commonType.Asset的类型约定便于跨端传输接收端在fileCopy里用Number(attachment.size)转回数字来new ArrayBuffer(...)这正是第 28 篇要讲的细节。字段的类型一致性是跨设备协议的基本功两端对size 是字符串有共同认知才不会出现ArrayBuffer分配失败的隐性 bug。五、打包全流程onContinue 里的组装把模型层串进第 23 篇的onContinue看完整组装过程// 1. 遍历内存态媒体列表逐个转成 AssetletmediaUriArray AppStorage.getArrayMediaInfo(mediaUriArray);letassets: commonType.Assets [];if(mediaUriArray) {for(leti 0; i mediaUriArray.length; i) {letappend mediaUriArray[i];letattachment: commonType.Asset getAssetInfo(this.context, append); assets.push(attachment); } }// 2. 七字段组装 ContentInfoletcontentInfo: ContentInfo newContentInfo( AppStorage.get(mainTitle),// 标题AppStorage.get(textContent),// 正文AppStorage.get(mediaUriArray),// 内存态媒体AppStorage.get(isShowLocalInfo),// 位置列表开关AppStorage.get(isAddLocalInfo),// 添加位置状态AppStorage.get(selectLocalInfo),// 选中位置assets// 落盘态附件描述);// 3. 压平后创建分布式数据对象letsource contentInfo.flatAssets();this.distributedObject distributedDataObject.create(this.context, source);三个步骤对应模型层的三个角色MediaInfo → Asset 的转换由getAssetInfo完成七字段聚合由ContentInfo完成数组压平由flatAssets完成。至此一篇文章的完整现场变成了一组扁平的键值属性可以交给分布式数据对象跨设备同步了。顺带解释一个类型选择ContentInfo用class、MediaInfo用interface。这是刻意的——ContentInfo需要承载行为flatAssets方法且要被new实例化class 正合适MediaInfo只是纯数据结构、由各处字面量构造如{ imagePixelMap, mediaName, mediaType }interface 更轻。另外 class 实例的属性在分布式数据对象里天然可枚举、可序列化flatAssets直接复用this也正是受益于此。六、收发两端的数据流对照把模型层放在完整链路里对照能一眼看清打包与还原如何围绕同一套结构对称运作阶段发送端设备 A接收端设备 B媒体内存态MediaInfoPixelMap/uri 名字 类型重建后的MediaInfofileCopypush 进数组媒体落盘态getAssetInfo→Assetname 拼image_/video_前缀fileCopy从attachment.name拆回类型与文件名正文与状态ContentInfo七字段 flatAssets()展开restored后读distributedObject[mainTitle]等键键名约定mainTitle/textContent/isShowLocalInfo/isAddLocalInfo/selectLocalInfo/attachments与发送端完全一致同一模型保证这张表的要点是两端不是各写一套逻辑而是共享同一份契约——ContentInfo的字段名、MediaType的字符串值、Asset.name 的拼装规则。契约在entry/src/main/ets/model/ContentInfo.ets与FileUtil.ets中一次性定义两端代码都从契约出发因此发送端怎么拼、接收端就怎么拆永远不会失配。这也是把数据模型独立成文件的终极收益契约集中、两端共识。小结数据模型层是接续的翻译官负责把 UI 状态翻译成可同步的数据结构ContentInfo用七个字段收拢一篇编辑内容的全部现场MediaInfo用可选项区分图片/视频并携带文件名MediaType用可读字符串标记类型与 Asset 的name约定呼应flatAssets()把附件数组展开为attachments0/1/2...独立键换取更细的同步粒度getAssetInfo()把内存态MediaInfo转成落盘态Asset其中name形如image_uuid/video_uuid既是类型标签又是文件索引。有了这套模型发送端一次flatAssets()就能把全部现场交给分布式数据对象接收端则依据同样的约定逆向还原——下一篇《接续后媒体还原》就来讲这个逆向过程。
返回列表