HarmonyOS:ArkTS数据模型的设计与演进实战

HarmonyOS:ArkTS数据模型的设计与演进实战
引子数据设计好了UI 就是套模板星空运势应用经历了两次数据结构的变化。第一版是四个文件——Types.ts、SignsData.ts、TarotData.ts、Constants.ts每个文件职责单一。重构后合并成了一个 DataModel.ets12 星座的数据从 5 个字段扩展到 8 个加了优势、弱点、最佳配对运势数据从两行文字扩展到四维评分幸运物。改动的动机很简单数据量变大了文件太多反而麻烦——每次改星座数据要打开 SignsData.ts改运势要打开另一个文件改颜色又要打开 Constants.ts。合并成一个文件后所有数据定义在一个地方改起来方便。完整效果数据模型的完整结构DataModel.ets 里定义了六个核心类和四个常量数组DataModel.ets ├── class Pos2D # 坐标点 ├── class StarPt # 星点坐标大小 ├── class SignData # 星座完整数据 ├── class TarotInfo # 塔罗牌数据 ├── class FortuneDetail # 运势维度数据 ├── class LuckyItem # 幸运物数据 ├── const SIGNS: SignData[] # 12 星座数据 ├── const TAROT: TarotInfo[] # 21 张塔罗牌 ├── const WHEEL_COLORS # 12 种转盘颜色 ├── const FORTUNE_LABELS # 12 个运势标签 └── const LUCKY_ITEMS # 4 个幸运物为什么合并成一个文件重构前拆成四个文件的理由是单一职责——每个文件只做一件事。但实际开发中发现数据量不大总共也就 200 多行代码拆成四个文件每个只有几十行反而增加了文件切换成本数据关联紧密SignData 依赖 StarPtFortuneDetail 依赖 LuckyItem拆开后 import 路径复杂修改频率高星座数据和运势数据经常一起改放在一个文件里更方便合并的原则是数据量小且关联紧密时单文件比多文件更高效。六个核心类的设计Pos2D最简单的坐标点classPos2D{x:number0;y:number0;constructor(x:number,y:number){this.xx;this.yy;}}只有 x 和 y 两个属性用于转盘的星座图标定位和星图的星点定位。为什么不用系统自带的 PointArkUI 没有内置的 Point 类。如果用{ x: number, y: number }对象每次都要写类型注解。用class可以通过new Pos2D(x, y)快速创建实例代码更简洁。StarPt带大小的星点classStarPt{x:number0;y:number0;sz:number0;constructor(x:number,y:number,sz:number){this.xx;this.yy;this.szsz;}}比 Pos2D 多一个szsize属性用于控制星点的大小。sz的值是 2-5渲染时乘以 2.5 映射到字体大小5px-12.5px。为什么 sz 不直接存像素值存原始尺寸比如 5、7.5、10更直观但存 2-5 的小数字更紧凑。渲染时再乘以系数方便统一调整——如果要让所有星点变大只需改系数不需要改 12 个星座的数据。SignData星座的完整信息classSignData{name:string;// 名称emoji:string;// 符号date:string;// 日期范围elem:string;// 元素trait:string;// 特质stars:StarPt[][];// 星点坐标strengths:string;// 优势weaknesses:string;// 弱点matches:string[][];// 最佳配对constructor(n:string,e:string,d:string,el:string,t:string,s:StarPt[],str:string,wk:string,mt:string[][]){this.namen;this.emojie;this.dated;this.elemel;this.traitt;this.starss;this.strengthsstr;this.weaknesseswk;this.matchesmt;}}这是最核心的数据类包含了星座的所有信息。9 个字段分三类类别字段用途基础信息name, emoji, date, elem, trait星座网格、详情标签可视化数据stars星点渲染富文本数据strengths, weaknesses, matches详情卡片为什么用位置参数而不是对象参数构造函数用了 9 个位置参数而不是传一个对象// 位置参数当前实现newSignData(白羊座,♈,3.21-4.19,火,热情勇敢,[...],充满活力,冲动急躁,[狮子座])// 对象参数另一种方式newSignData({name:白羊座,emoji:♈,date:3.21-4.19,elem:火,trait:热情勇敢,stars:[...],strengths:充满活力,weaknesses:冲动急躁,matches:[狮子座]})位置参数的优点是写起来短缺点是参数顺序不能错。对象参数的优点是参数顺序随意缺点是写起来长。对于这种数据量固定、很少修改的场景位置参数更高效。如果字段经常增减对象参数更安全。默认参数的作用后三个参数strengths、weaknesses、matches有默认值空字符串和空数组。这样在定义星座数据时如果某个星座暂时没有详细信息可以省略这些参数newSignData(测试座,☆,1.1-1.31,火,测试,[...])// strengths、weaknesses、matches 自动为空TarotInfo塔罗牌数据classTarotInfo{name:string;emoji:string;key:string;text:string;constructor(n:string,e:string,k:string,t:string){this.namen;this.emojie;this.keyk;this.textt;}}四个字段名称、emoji、关键词、解读文本。比 SignData 简单得多因为塔罗牌不需要星点坐标和优劣势。FortuneDetail运势维度数据classFortuneDetail{category:string;emoji:string;stars:number0;text:string;constructor(c:string,e:string,s:number,t:string){this.categoryc;this.emojie;this.starss;this.textt;}}用于运势详解的四维评分维度名称、图标、星级1-5、解读文本。LuckyItem幸运物数据classLuckyItem{emoji:string;label:string;value:string;constructor(e:string,l:string,v:string){this.emojie;this.labell;this.valuev;}}用于今日幸运物图标、标题、具体内容。四个常量数组的设计SIGNS12 星座数据constSIGNS:SignData[][newSignData(白羊座,♈,3.21-4.19,火,热情勇敢·行动力强,[newStarPt(100,30,3),newStarPt(60,70,2),...],充满活力勇于开拓天生的领导者,冲动急躁缺乏耐心容易半途而废,[狮子座,射手座]),// ... 共 12 个];每个星座 9 个参数12 个星座总共 108 个参数。数据量不小但都在一个数组里维护方便。TAROT21 张塔罗牌constTAROT:TarotInfo[][newTarotInfo(愚者,,新开始·冒险,新的旅程即将开始...),newTarotInfo(魔术师,,创造力·技巧,你拥有实现目标的所有资源...),// ... 共 21 张];21 张大阿尔卡纳每张 4 个参数。数据量比星座少但每张牌的解读文本较长。WHEEL_COLORS12 种转盘颜色constWHEEL_COLORS:string[][#7B4FBF,#3B6FC2,#20A39E,#E89240,#C44569,#5D5FEF,#2E9C7A,#D4594C,#6B5CE7,#3D8FD1,#18A085,#E07030];12 种颜色和 12 个星座一一对应。颜色选择考虑了对比度——在深色背景上都能看清。FORTUNE_LABELS12 个运势标签constFORTUNE_LABELS:string[][大吉,中吉,小吉,末吉,大吉,中吉,小吉,大吉,中吉,末吉,小吉,中吉];12 个运势标签和 12 个星座一一对应。运势结果由转盘停止位置决定不是随机的。LUCKY_ITEMS4 个幸运物constLUCKY_ITEMS:LuckyItem[][newLuckyItem(,幸运色,蓝色),newLuckyItem(,守护石,青金石),newLuckyItem(,吉时,下午4点),newLuckyItem(,方位,北方),];4 个固定幸运物不随星座变化。如果要做更精细的运势可以让不同星座有不同的幸运物。数据如何驱动整个应用整个应用的 UI 完全由数据驱动SIGNS → 转盘的星座图标ForEach wheelPos → 星图的星座网格ForEach Grid → 星图的详情卡片selectedSign 索引 TAROT → 塔罗的洗牌结果tarotIdx 索引 → 塔罗的牌面显示ForEach tarotOpen WHEEL_COLORS → 转盘的背景色ForEach backgroundColor FORTUNE_LABELS → 转盘的运势标签resultIdx 索引 LUCKY_ITEMS → 运势详解的幸运物ForEach修改任何数据对应的 UI 自动更新。这就是数据驱动的核心思想。数据流向的单向性数据流是单向的数据 → UI没有反向流UI → 数据除非用户交互触发状态变化。这种单向数据流让代码更可预测——看到数据就知道 UI 长什么样看到 UI 就知道数据是什么。数据的不可变性常量数组SIGNS、TAROT 等用const声明不可修改。如果要修改数据必须创建新数组// 错误const 数组不能修改SIGNS[0].name新名字;// 正确创建新数组constnewSIGNS[...SIGNS];newSIGNS[0]newSignData(新名字,...);不可变性的好处是避免意外修改——如果某个组件不小心改了 SIGNS 数据会影响所有使用 SIGNS 的组件。从多文件到单文件的演进过程第一版四个文件model/ ├── Types.ts # 类定义 ├── SignsData.ts # 星座数据 ├── TarotData.ts # 塔罗数据 └── Constants.ts # 颜色、运势标签优点每个文件职责单一修改一个不影响另一个。缺点文件切换频繁import 路径复杂。第二版两个文件model/ ├── DataModel.ts # 类定义 常量数据 └── index.ts # 统一导出优点减少文件切换import 更简洁。缺点DataModel.ts 还是有点大。第三版当前一个文件model/ └── DataModel.ets # 所有数据定义优点所有数据在一个文件里改起来最方便。缺点文件有点大200 行但还在可接受范围内。演进的判断标准什么时候该合并什么时候该拆分判断标准是数据量300 行可以合并500 行应该拆分修改频率经常一起改的数据应该合并很少同时改的数据可以拆分关联性关联紧密的数据应该合并独立的数据可以拆分当前 DataModel.ets 大约 200 行在可接受范围内。如果以后加更多星座比如 24 星座或更多塔罗牌78 张完整牌组可能需要再拆分。踩坑记录坑 1类的默认值必须在声明时赋值classSignData{name:string;// 必须在声明时赋值// name: string; // 错误编译器会报错}ArkTS 要求类的属性在声明时赋默认值不能只声明类型。这是因为 ArkTS 编译后会生成 JavaScript 代码如果没有默认值属性会是undefined。坑 2const 数组的深拷贝问题consta[{x:1},{x:2}];constb[...a];// 浅拷贝b[0].x100;// a[0].x 也会变成 100展开运算符[...]只做浅拷贝数组里的对象还是同一个引用。如果要深拷贝需要用 JSON 序列化constbJSON.parse(JSON.stringify(a));但当前应用的数据都是只读的不需要深拷贝。坑 3类的构造函数参数顺序构造函数有 9 个参数顺序不能错。如果记不住顺序可以看 IDE 的参数提示或者用对象参数代替。坑 4export 的写法ArkTS 的 export 语法和 TypeScript 略有不同// TypeScriptexportclassSignData{...}// ArkTSclassSignData{...}export{SignData};如果在类声明前加export编译器可能会报错。代码改进建议1. 数据的外部化如果星座数据经常变动比如从网络获取可以考虑把数据放到外部文件JSON 或数据库运行时加载。这样修改数据不需要重新编译。2. 类型的强化当前类的属性都是string或number没有更精确的类型。比如elem只能是 ‘火’ | ‘土’ | ‘风’ | ‘水’可以用枚举或联合类型typeElement火|土|风|水;3. 数据的校验当前数据没有校验——如果某个星座的 stars 数组为空渲染时会出错。可以加校验逻辑constructor(...){if(s.length0){console.warn(${n}has no stars);}// ...}4. 数据的可扩展性如果以后要支持自定义星座或编辑运势需要把数据存到本地。可以用 Preferences API 或关系型数据库。总结DataModel.ets 是整个应用的数据基础——转盘的旋转结果、星图的星座详情、塔罗的洗牌结果都依赖这里的定义。数据结构设计好了UI 就是套模板数据结构没设计好UI 怎么改都别扭。适用边界这个部分适合用作 ArkTS 数据模型设计的学习案例涵盖了类定义、常量数组、构造函数、数据驱动、文件组织等核心知识点。但如果要上架应用商店还需要补充数据校验、类型强化、外部化存储、可扩展性设计等内容。建议在此基础上逐步扩展而不是一次性做完所有功能。