
引子类型系统到底能帮我们什么状态管理、自定义构建器、动画效果。但一个完整的应用不能只有 UI数据层的设计同样重要。朋友拿到我写的骰子应用后提了一个问题“你这个RollResult接口是怎么设计的为什么要加rolledAt字段”这个问题触及了 ArkTS 开发中一个容易被忽视的话题类型系统和数据建模。很多开发者觉得能跑就行类型定义随便写写。但在实际项目中好的类型设计能帮你在编译阶段就发现错误而不是在线上环境崩溃后才去排查。完整效果项目文件结构先看一下整个项目的文件组织entry/src/main/ets/ ├── model/ │ ├── Dice.ts # 骰子类型定义和核心逻辑 │ └── Database.ts # 数据持久化层 ├── utils/ │ └── Theme.ts # 主题配置和颜色系统 ├── pages/ │ └── Index.ets # 主页面组件 └── common/ # 公共工具这种分层结构很常见model放数据定义utils放工具函数pages放 UI 组件。但常见不代表好——很多项目的分层是形式主义实际上model和pages之间互相引用耦合严重。对于这个骰子应用分层还算合理。Dice.ts只定义数据结构和纯函数不依赖任何 UI 组件Database.ts封装存储逻辑对外暴露简洁的 APITheme.ts提供颜色配置不涉及业务逻辑。这种单向依赖是好架构的基础。Dice.ts类型定义的艺术先看Dice.ts的核心定义exportinterfaceDiceType{key:string;// 骰子类型标识如 d4、d6label:string;// 显示名称如 四面骰emoji:string;// 显示图标sides:number;// 面数}exportconstDICE_TYPES:DiceType[][{key:d4,label:四面骰,emoji:,sides:4},{key:d6,label:六面骰,emoji:,sides:6},{key:d8,label:八面骰,emoji:,sides:8},{key:d10,label:十面骰,emoji:,sides:10},{key:d12,label:十二面骰,emoji:⬡,sides:12},{key:d20,label:二十面骰,emoji:,sides:20},{key:d100,label:百面骰,emoji:,sides:100}];这里用interface而不是type来定义DiceType是有原因的。ArkTS继承自 TypeScript中interface和type都可以定义对象结构但有几个关键区别声明合并interface支持声明合并多个同名interface会自动合并type不支持extends vs interface用extends继承type用交叉类型语义interface通常用于定义对象的形状type更灵活可以定义联合类型、元组等在这个项目中DiceType是一个简单的对象结构用interface或type都可以。但考虑到未来可能需要扩展比如添加稀有骰子类型interface的声明合并特性更灵活。RollResult为什么需要 rolledAt 字段exportinterfaceRollResult{diceType:string;// 骰子类型results:number[];// 各骰子的结果total:number;// 总和rolledAt:number;// 投掷时间戳}rolledAt字段看起来可有可无但它是整个数据模型的关键。没有时间戳历史记录就是一堆无序的数字有了时间戳就能做排序、筛选、统计分析。在实际开发中我见过很多项目在 V1 版本不加时间戳等到 V2 要加筛选功能时才去改数据结构然后痛苦地处理旧数据兼容问题。这个教训很深刻数据模型要为未来的功能扩展留余地。createRollResult工厂函数的设计exportfunctioncreateRollResult(diceType:string,results:number[]):RollResult{lettotal0;for(leti0;iresults.length;i){totalresults[i];}return{diceType,results,total,rolledAt:Date.now()};}这个工厂函数封装了RollResult的创建逻辑。为什么不用直接构造对象因为自动计算 total调用者不需要手动计算总和自动填充 rolledAt时间戳由函数内部生成保证一致性单一职责如果将来要添加校验逻辑比如 results 不能为空只需修改这一个函数更重要的是工厂函数提供了类型安全保障。如果直接构造对象可能会漏掉某个字段或者类型写错而工厂函数返回的是RollResult类型编译器会自动检查。rollDice纯函数的价值exportfunctionrollDice(sides:number,count:number):number[]{constresults:number[][];for(leti0;icount;i){results.push(Math.floor(Math.random()*sides)1);}returnresults;}这个函数没有任何副作用——不修改外部状态不依赖外部变量只根据输入返回输出。这种纯函数在测试时特别好用你可以 mockMath.random()来控制输出确保测试结果可预测。但这里有个设计决策值得讨论为什么把rollDice放在model/Dice.ts而不是pages/Index.ets从功能上讲放在哪里都能跑。但从架构上讲model层应该包含所有业务逻辑和数据操作pages层只负责 UI 渲染。这样做的好处是可测试性纯函数可以直接写单元测试不需要启动 UI可复用性如果将来要做命令行版本或 Web 版本可以直接复用model层的代码关注点分离UI 开发者不需要关心随机数怎么生成业务逻辑开发者不需要关心按钮怎么布局Database.ts数据持久化的封装DiceDatabase类封装了底层的存储逻辑。虽然看不到完整代码但从使用方式可以推断出它的接口设计classDiceDatabase{asyncinit():Promisevoid{...}add(result:RollResult):void{...}getHistory():RollResult[]{...}asyncclear():Promisevoid{...}}这个接口设计有几个值得注意的地方同步 vs 异步init()和clear()是异步的add()和getHistory()是同步的。这种设计合理吗init()数据库初始化涉及文件 I/O 或网络操作必须异步clear()清空数据也是 I/O 操作必须异步add()如果用 Preferences API写入操作可能是同步的因为数据量小getHistory()如果数据量小且在内存中缓存同步返回是合理的但这里有个潜在问题add()是同步的如果底层存储是异步的比如 RDB就会有问题。建议统一用异步 API即使底层是同步的也能保持接口一致性。错误处理当前代码几乎没有错误处理。在实际项目中数据库操作可能失败磁盘满、文件损坏等应该添加 try-catch 和错误提示privateasyncclr():Promisevoid{if(this.db){try{awaitthis.db.clear();this.history[];}catch(error){console.error(清空历史失败:,error);// 这里应该弹出 Toast 提示用户}}}数据校验从数据库取出的数据不一定符合预期比如旧版本的数据结构可能缺少rolledAt字段。建议在getHistory()中添加数据校验getHistory():RollResult[]{constrawthis.getRawData();returnraw.filter((item:unknown):itemisRollResult{return(typeofitemobjectitem!nulldiceTypeinitemresultsinitemtotalinitem);});}这样即使数据库中有脏数据也不会导致 UI 崩溃。Theme.ts配置驱动的 UI主题系统的代码展示了配置驱动的设计思想exportinterfaceThemeColors{bg:string;surface:string;primary:string;accent:string;text:string;text2:string;border:string;}exportfunctiongetThemeColors(theme:ThemeType):ThemeColors{if(themeThemeType.DARK){return{bg:#1A1A1A,surface:#2A2A2A,primary:#6366F1,accent:#F59E0B,text:#FFFFFF,text2:#9CA3AF,border:#374151};}// 浅色主题预留return{bg:#FFFFFF,surface:#F3F4F6,primary:#4F46E5,accent:#D97706,text:#111827,text2:#6B7280,border:#E5E7EB};}这种模式的好处是单一数据源所有颜色都从ThemeColors对象获取不会出现同一个颜色在不同地方用不同值的情况主题切换简单只需修改ThemeType的值整个应用的颜色就会更新类型安全ThemeColors接口保证了所有颜色字段都有定义Spacing、BorderRadius、FontSize 的设计除了颜色主题配置还包括间距、圆角、字号等设计 tokenexportconstSpacing{xs:4,sm:8,md:12,lg:16,xl:24};exportconstBorderRadius{sm:4,md:8,lg:12,xl:16};exportconstFontSize{xs:12,sm:14,md:16,lg:18,title:22,xxl:28};这些常量看起来简单但它们是设计系统的基础。在大型项目中这些值通常由设计师定义开发者只引用常量。这样做的好处是一致性整个应用的间距、圆角、字号都遵循统一规范可维护性如果要调整全局样式只需修改这些常量可协作性设计师和开发者用同一套语言沟通ArkTS 类型系统的几个关键特性从这个项目中可以总结出 ArkTS 类型系统的几个关键特性1. 接口Interfacevs 类型别名Type如前所述interface适合定义对象的形状type更灵活。在实际项目中定义 API 响应结构、组件 Props → 用interface定义联合类型、工具类型 → 用type需要声明合并或继承 → 用interface2. 枚举Enum的使用项目中用了ThemeType枚举exportenumThemeType{DARKdark,LIGHTlight}ArkTS 的枚举和 TypeScript 一样支持数字枚举和字符串枚举。字符串枚举更直观但数字枚举性能更好。在这个项目中字符串枚举的可读性更重要。3. 泛型Generics的潜在应用当前项目没有用到泛型但在更复杂的场景中泛型很有用。比如DiceDatabase如果要支持不同类型的数据存储可以用泛型classDatabaseT{privatedata:T[][];add(item:T):void{this.data.push(item);}getAll():T[]{returnthis.data;}}// 使用示例constdiceDbnewDatabaseRollResult();constsettingsDbnewDatabaseUserSettings();4. 类型断言Type Assertion的谨慎使用代码中有几处类型断言this.ctxgetContext(this)ascommon.UIAbilityContext;constdDICE_TYPES.find((d2:DiceType)d2.keythis.diceType);returnd||DICE_TYPES[1];类型断言告诉编译器我知道这个值的类型但编译器不会检查。如果断言错误运行时会崩溃。建议尽量用类型守卫Type Guard代替类型断言functionisDiceType(d:DiceType|undefined):disDiceType{returnd!undefined;}constdDICE_TYPES.find((d2:DiceType)d2.keythis.diceType);if(isDiceType(d)){returnd;}returnDICE_TYPES[1];工程化实践从这个项目学到的1. 模块化设计这个项目的模块化做得不错Dice.ts只管数据Database.ts只管存储Theme.ts只管样式。每个模块职责单一依赖关系清晰。在更大的项目中模块化更重要。比如要做一个完整的桌游工具可能需要dice/模块骰子相关的数据和逻辑card/模块卡牌相关的数据和逻辑timer/模块计时器相关的数据和逻辑player/模块玩家管理相关的数据和逻辑每个模块独立开发、独立测试最后在 UI 层组合。2. 错误处理策略这个项目的错误处理比较薄弱但在实际项目中错误处理是必须的。建议采用分层错误处理策略数据层捕获存储错误返回错误码或错误对象业务层根据错误码决定重试、降级或报错UI 层给用户友好的提示Toast、弹窗等3. 性能优化ArkTS 的性能优化和 Web 前端类似核心原则是减少不必要的渲染。在这个项目中ForEach要提供稳定的 key避免不必要的 DOM 操作大列表用List的懒加载而不是ForEach渲染所有项状态更新要批量处理避免频繁触发重新渲染4. 测试策略这个项目的测试可以分三层单元测试测试rollDice、createRollResult等纯函数集成测试测试DiceDatabase的增删改查UI 测试测试 Tab 切换、按钮点击等交互单元测试最容易写建议优先覆盖。UI 测试需要 DevEco Studio 的测试框架支持可以后续补充。总结这个骰子应用虽然简单但涉及了 ArkTS 开发的多个核心知识点类型系统、数据建模、模块化设计、错误处理、性能优化。在实际开发中这些知识点会反复出现只是复杂度不同。适用边界这个应用适合用作 ArkTS 入门学习项目涵盖了 UI 开发、状态管理、数据持久化等核心知识点。但如果要上架应用商店还需要补充错误处理、无障碍支持、性能优化、用户引导等内容。建议在此基础上逐步扩展而不是一次性做完所有功能。对于 ArkTS 新手建议从类似的小项目入手逐步理解框架的设计哲学。ArkTS 的声明式 UI 和 React 有相似之处但状态管理和生命周期有明显区别需要花时间适应。对于有经验的开发者重点是理解 ArkTS 的约束——它不是 TypeScript 的简单扩展而是一个有自己规则的框架。遵循框架的最佳实践才能写出可维护、高性能的代码。