HarmonyKit | 鸿蒙新特性驱动组件-工具-页面三层架构设计架构从混乱到有序HarmonyKit 的代码架构不是一蹴而就的。项目初期所有代码只有两个文件Index.ets主页和JsonFormatter.ets第一个工具页。没有 utils 目录、没有 components 目录、没有 model 目录。复制按钮的代码在 JSON 格式化页面和 Base64 页面各写了一份——完全相同的逻辑分布在两个文件中。当工具数量从 2 个增加到 5 个时代码重复开始失控。每个工具页都有一份自己的复制到剪贴板代码格式各不一样。有的用了promptAction.showToast()做反馈有的根本没做反馈。有的按钮是蓝色的有的是灰色的。这就是架构重构的触发点。我花了一个下午把项目代码重新组织为四层结构model、utils、components、pages。这篇文章详细记录了每一层的设计原则、约束规则和在 10 个工具的迭代中验证过的模式。项目仓库https://atomgit.com/VON-/harmony-kit数据层model唯一的真相来源model/ToolItem.ets是整个应用的数据中心。一个ToolItem接口定义了一个工具的全部元数据exportinterfaceToolItem{id:string;// 工具唯一标识name:string;// 显示名称description:string;// 卡片描述文案icon:string;// 图标文字等宽字体渲染color:string;// 主题色用于图标圆圈和卡片强调色category:string;// 分类格式化/编解码/计算/文本routerPath:string;// 路由路径}七个字段没有多余的。每个字段都有明确的用途id用于 Tab 筛选和路由匹配name和description用于卡片展示icon和color用于视觉区分category用于分类过滤routerPath用于页面跳转。TOOL_LIST是一个类型安全的数组exportconstTOOL_LIST:ToolItem[][{id:json,name:JSON 格式化,icon:{},color:#007aff,category:格式化,routerPath:pages/tools/JsonFormatter},{id:base64,name:Base64 编解码,icon:64,color:#34c759,category:编解码,routerPath:pages/tools/Base64Tool},// ...];新增一个工具需要在四个地方修改代码在这里追加一条数据、在pages/tools/下创建一个页面文件、在main_pages.json中添加路由、如果工具逻辑复杂则在utils/下创建工具类。四个步骤标准化的流程不会遗漏任何一个环节。getToolsByCategory()函数提供了分类过滤能力exportfunctiongetToolsByCategory(category:string):ToolItem[]{if(category全部)returnTOOL_LIST;returnTOOL_LIST.filter((item:ToolItem)item.categorycategory);}简洁到不需要解释。Index.ets中的每个 Tab 调用这个函数获取自己分类的工具列表。逻辑层utils纯函数的圣殿utils 层的设计约束是最严格的所有方法必须是纯函数或纯异步函数。不依赖任何 UI 模块。输入和输出都是基础类型string、number、boolean或简单的 interface。以Base64Utils为例exportclassBase64Utils{staticencode(input:string):string{letencodernewutil.TextEncoder(utf-8);letuint8Array:Uint8Arrayencoder.encodeInto(input);lethelpernewutil.Base64Helper();returnhelper.encodeToStringSync(uint8Array);}staticdecode(input:string):string{lethelpernewutil.Base64Helper();letuint8Array:Uint8Arrayhelper.decodeSync(input);letdecoderutil.TextDecoder.create(utf-8);returndecoder.decodeToString(uint8Array);}}两个静态方法输入 string 输出 string。没有this没有状态没有副作用。这种纯粹性带来了几个好处可独立测试。不需要创建组件实例不需要 mock UI 上下文直接调Base64Utils.encode(hello)然后断言结果。可跨页面复用。如果需要在一个页面中同时用到编码和解码直接调两次即可没有任何状态冲突。类型安全。输入是string输出是string。编译器可以在调用点验证类型正确性。HashUtils稍微特殊——因为cryptoFramework.createMd()是异步的exportclassHashUtils{staticasynchash(input:string,algorithm:MD5|SHA1|SHA256):Promisestring{letmdcryptoFramework.createMd(algorithm);letuint8ArraynewUint8Array(buffer.from(input,utf-8).buffer);awaitmd.update({data:uint8Array});letresultawaitmd.digest();returnbuffer.from(result.data).toString(hex);}}但核心原则不变调用方在组件中写this.output await HashUtils.hash(this.input, MD5)utils 层依然不碰 UI。组件层componentsProp 驱动的纯展示组件层有两个组件ToolCard和CopyButton。它们的设计原则是内部没有 State所有数据通过 Prop 从父组件传入所有事件通过 onClick 回调冒泡。Componentexportstruct ToolCard{Proptool:ToolItem;Consume(pathStack)pathStack:NavPathStack;build(){Column(){Stack(){Circle().width(48).height(48).fill(this.tool.color18);Text(this.tool.icon).fontSize(18).fontWeight(FontWeight.Bold).fontColor(this.tool.color).fontFamily(monospace);}Text(this.tool.name).fontSize(14).fontWeight(FontWeight.Medium);Text(this.tool.description).fontSize(11).fontColor(#999999);}.width(100%).height(158).justifyContent(FlexAlign.Center).onClick((){this.pathStack.pushPathByName(this.tool.id,undefined);})}}Consume(pathStack)让 ToolCard 能直接操作导航栈——不需要父组件传入回调函数。这种祖先组件向所有后代组件注入的能力是 ArkUI 独有的设计模式。卡片使用固定高度 158vp justifyContent(FlexAlign.Center)保证了 2 列网格中所有卡片高度一致。不依赖 Grid 的行高度计算——Grid 的默认行为是每行高度由最高项决定这会导致同一行两张卡片高度不同时出现底部留白。固定高度从根本上解决了这个问题。CopyButton的实现展示了组件中调用系统服务的模式Componentexportstruct CopyButton{Proptext:string;build(){Button(复制).onClick((){letdatapasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN,this.text);letsysPasteboardpasteboard.getSystemPasteboard();sysPasteboard.setData(data,(err){if(err){this.getUIContext().getPromptAction().showToast({message:复制失败});}else{this.getUIContext().getPromptAction().showToast({message:已复制到剪贴板});}});})}}pasteboard来自kit.BasicServicesKitpromptAction通过this.getUIContext()获取。复制按钮不关心自己处于哪个页面——它只是一个按钮接收一段文本把它写入系统剪贴板然后弹一个 Toast。页面层pages组装一切的指挥官页面层是唯一同时引用 model、utils、components 的地方。一个典型的工具页面结构import { XxxUtils } from ../../utils/XxxUtils; // 逻辑 import { CopyButton } from ../../components/CopyButton; // 组件 import { ToolItem } from ../../model/ToolItem; // 数据 Entry Component struct XxxTool { State input: string ; State output: string ; build() { Column() { // 自定义 Header返回按钮 标题 Row() { ... } // 内容区域 Scroll() { // 输入区 TextArea({ ... }).onChange(v this.input v); // 操作按钮 Button(执行).onClick(() this.doWork()); // 输出区 Row() { Text(结果); CopyButton({ text: this.output }); } TextArea({ text: this.output }); } } } doWork() { this.output XxxUtils.process(this.input); } }这个模板覆盖了全部 10 个工具页。差异只在输入控件的类型单行 vs 多行、操作按钮的数量和布局一个按钮 vs 两个并排 vs 工具栏模式、以及是否有辅助选项缩进大小、算法选择、标志位切换。架构的三个优点经过 10 个工具的开发验证三层架构展现了三个优点新人友好。如果你要为 HarmonyKit 贡献一个新工具你只需要看懂ToolItem接口的七个字段、选择一个输入/输出模式模板、在 utils 中实现核心逻辑。不需要理解整个项目的导航体系或状态管理机制。重构安全。改CopyButton的样式——只改一个文件10 个工具页同时生效。改Base64Utils.encode的实现——不影响任何 UI 代码。改ToolCard的布局——只影响主页卡片不影响工具页内部。可测试。utils 层直接调方法测。components 层传入不同 prop 测渲染。pages 层 mock utils 返回值测 UI 状态。每一层有独立的测试策略。项目仓库https://atomgit.com/VON-/harmony-kit
HarmonyKit | 鸿蒙新特性驱动:组件-工具-页面三层架构设计
HarmonyKit | 鸿蒙新特性驱动组件-工具-页面三层架构设计架构从混乱到有序HarmonyKit 的代码架构不是一蹴而就的。项目初期所有代码只有两个文件Index.ets主页和JsonFormatter.ets第一个工具页。没有 utils 目录、没有 components 目录、没有 model 目录。复制按钮的代码在 JSON 格式化页面和 Base64 页面各写了一份——完全相同的逻辑分布在两个文件中。当工具数量从 2 个增加到 5 个时代码重复开始失控。每个工具页都有一份自己的复制到剪贴板代码格式各不一样。有的用了promptAction.showToast()做反馈有的根本没做反馈。有的按钮是蓝色的有的是灰色的。这就是架构重构的触发点。我花了一个下午把项目代码重新组织为四层结构model、utils、components、pages。这篇文章详细记录了每一层的设计原则、约束规则和在 10 个工具的迭代中验证过的模式。项目仓库https://atomgit.com/VON-/harmony-kit数据层model唯一的真相来源model/ToolItem.ets是整个应用的数据中心。一个ToolItem接口定义了一个工具的全部元数据exportinterfaceToolItem{id:string;// 工具唯一标识name:string;// 显示名称description:string;// 卡片描述文案icon:string;// 图标文字等宽字体渲染color:string;// 主题色用于图标圆圈和卡片强调色category:string;// 分类格式化/编解码/计算/文本routerPath:string;// 路由路径}七个字段没有多余的。每个字段都有明确的用途id用于 Tab 筛选和路由匹配name和description用于卡片展示icon和color用于视觉区分category用于分类过滤routerPath用于页面跳转。TOOL_LIST是一个类型安全的数组exportconstTOOL_LIST:ToolItem[][{id:json,name:JSON 格式化,icon:{},color:#007aff,category:格式化,routerPath:pages/tools/JsonFormatter},{id:base64,name:Base64 编解码,icon:64,color:#34c759,category:编解码,routerPath:pages/tools/Base64Tool},// ...];新增一个工具需要在四个地方修改代码在这里追加一条数据、在pages/tools/下创建一个页面文件、在main_pages.json中添加路由、如果工具逻辑复杂则在utils/下创建工具类。四个步骤标准化的流程不会遗漏任何一个环节。getToolsByCategory()函数提供了分类过滤能力exportfunctiongetToolsByCategory(category:string):ToolItem[]{if(category全部)returnTOOL_LIST;returnTOOL_LIST.filter((item:ToolItem)item.categorycategory);}简洁到不需要解释。Index.ets中的每个 Tab 调用这个函数获取自己分类的工具列表。逻辑层utils纯函数的圣殿utils 层的设计约束是最严格的所有方法必须是纯函数或纯异步函数。不依赖任何 UI 模块。输入和输出都是基础类型string、number、boolean或简单的 interface。以Base64Utils为例exportclassBase64Utils{staticencode(input:string):string{letencodernewutil.TextEncoder(utf-8);letuint8Array:Uint8Arrayencoder.encodeInto(input);lethelpernewutil.Base64Helper();returnhelper.encodeToStringSync(uint8Array);}staticdecode(input:string):string{lethelpernewutil.Base64Helper();letuint8Array:Uint8Arrayhelper.decodeSync(input);letdecoderutil.TextDecoder.create(utf-8);returndecoder.decodeToString(uint8Array);}}两个静态方法输入 string 输出 string。没有this没有状态没有副作用。这种纯粹性带来了几个好处可独立测试。不需要创建组件实例不需要 mock UI 上下文直接调Base64Utils.encode(hello)然后断言结果。可跨页面复用。如果需要在一个页面中同时用到编码和解码直接调两次即可没有任何状态冲突。类型安全。输入是string输出是string。编译器可以在调用点验证类型正确性。HashUtils稍微特殊——因为cryptoFramework.createMd()是异步的exportclassHashUtils{staticasynchash(input:string,algorithm:MD5|SHA1|SHA256):Promisestring{letmdcryptoFramework.createMd(algorithm);letuint8ArraynewUint8Array(buffer.from(input,utf-8).buffer);awaitmd.update({data:uint8Array});letresultawaitmd.digest();returnbuffer.from(result.data).toString(hex);}}但核心原则不变调用方在组件中写this.output await HashUtils.hash(this.input, MD5)utils 层依然不碰 UI。组件层componentsProp 驱动的纯展示组件层有两个组件ToolCard和CopyButton。它们的设计原则是内部没有 State所有数据通过 Prop 从父组件传入所有事件通过 onClick 回调冒泡。Componentexportstruct ToolCard{Proptool:ToolItem;Consume(pathStack)pathStack:NavPathStack;build(){Column(){Stack(){Circle().width(48).height(48).fill(this.tool.color18);Text(this.tool.icon).fontSize(18).fontWeight(FontWeight.Bold).fontColor(this.tool.color).fontFamily(monospace);}Text(this.tool.name).fontSize(14).fontWeight(FontWeight.Medium);Text(this.tool.description).fontSize(11).fontColor(#999999);}.width(100%).height(158).justifyContent(FlexAlign.Center).onClick((){this.pathStack.pushPathByName(this.tool.id,undefined);})}}Consume(pathStack)让 ToolCard 能直接操作导航栈——不需要父组件传入回调函数。这种祖先组件向所有后代组件注入的能力是 ArkUI 独有的设计模式。卡片使用固定高度 158vp justifyContent(FlexAlign.Center)保证了 2 列网格中所有卡片高度一致。不依赖 Grid 的行高度计算——Grid 的默认行为是每行高度由最高项决定这会导致同一行两张卡片高度不同时出现底部留白。固定高度从根本上解决了这个问题。CopyButton的实现展示了组件中调用系统服务的模式Componentexportstruct CopyButton{Proptext:string;build(){Button(复制).onClick((){letdatapasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN,this.text);letsysPasteboardpasteboard.getSystemPasteboard();sysPasteboard.setData(data,(err){if(err){this.getUIContext().getPromptAction().showToast({message:复制失败});}else{this.getUIContext().getPromptAction().showToast({message:已复制到剪贴板});}});})}}pasteboard来自kit.BasicServicesKitpromptAction通过this.getUIContext()获取。复制按钮不关心自己处于哪个页面——它只是一个按钮接收一段文本把它写入系统剪贴板然后弹一个 Toast。页面层pages组装一切的指挥官页面层是唯一同时引用 model、utils、components 的地方。一个典型的工具页面结构import { XxxUtils } from ../../utils/XxxUtils; // 逻辑 import { CopyButton } from ../../components/CopyButton; // 组件 import { ToolItem } from ../../model/ToolItem; // 数据 Entry Component struct XxxTool { State input: string ; State output: string ; build() { Column() { // 自定义 Header返回按钮 标题 Row() { ... } // 内容区域 Scroll() { // 输入区 TextArea({ ... }).onChange(v this.input v); // 操作按钮 Button(执行).onClick(() this.doWork()); // 输出区 Row() { Text(结果); CopyButton({ text: this.output }); } TextArea({ text: this.output }); } } } doWork() { this.output XxxUtils.process(this.input); } }这个模板覆盖了全部 10 个工具页。差异只在输入控件的类型单行 vs 多行、操作按钮的数量和布局一个按钮 vs 两个并排 vs 工具栏模式、以及是否有辅助选项缩进大小、算法选择、标志位切换。架构的三个优点经过 10 个工具的开发验证三层架构展现了三个优点新人友好。如果你要为 HarmonyKit 贡献一个新工具你只需要看懂ToolItem接口的七个字段、选择一个输入/输出模式模板、在 utils 中实现核心逻辑。不需要理解整个项目的导航体系或状态管理机制。重构安全。改CopyButton的样式——只改一个文件10 个工具页同时生效。改Base64Utils.encode的实现——不影响任何 UI 代码。改ToolCard的布局——只影响主页卡片不影响工具页内部。可测试。utils 层直接调方法测。components 层传入不同 prop 测渲染。pages 层 mock utils 返回值测 UI 状态。每一层有独立的测试策略。项目仓库https://atomgit.com/VON-/harmony-kit