
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 接口定义了一个工具的全部元数据:
typescript
export interface ToolItem {
id: string; // 工具唯一标识
name: string; // 显示名称
description: string; // 卡片描述文案
icon: string; // 图标文字(等宽字体渲染)
color: string; // 主题色(用于图标圆圈和卡片强调色)
category: string; // 分类(格式化/编解码/计算/文本)
routerPath: string; // 路由路径
}

七个字段,没有多余的。每个字段都有明确的用途:id 用于 Tab 筛选和路由匹配,name 和 description 用于卡片展示,icon 和 color 用于视觉区分,category 用于分类过滤,routerPath 用于页面跳转。
TOOL_LIST 是一个类型安全的数组:
typescript
export const TOOL_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() 函数提供了分类过滤能力:
typescript
export function getToolsByCategory(category: string): ToolItem[] {
if (category === '全部') return TOOL_LIST;
return TOOL_LIST.filter((item: ToolItem) => item.category === category);
}

简洁到不需要解释。Index.ets 中的每个 Tab 调用这个函数获取自己分类的工具列表。
逻辑层(utils):纯函数的圣殿
utils 层的设计约束是最严格的:所有方法必须是纯函数或纯异步函数。不依赖任何 UI 模块。输入和输出都是基础类型(string、number、boolean)或简单的 interface。
以 Base64Utils 为例:
typescript
export class Base64Utils {
static encode(input: string): string {
let encoder = new util.TextEncoder('utf-8');
let uint8Array: Uint8Array = encoder.encodeInto(input);
let helper = new util.Base64Helper();
return helper.encodeToStringSync(uint8Array);
}
static decode(input: string): string {
let helper = new util.Base64Helper();
let uint8Array: Uint8Array = helper.decodeSync(input);
let decoder = util.TextDecoder.create('utf-8');
return decoder.decodeToString(uint8Array);
}
}
两个静态方法,输入 string 输出 string。没有 this,没有状态,没有副作用。这种纯粹性带来了几个好处:
可独立测试。 不需要创建组件实例,不需要 mock UI 上下文,直接调 Base64Utils.encode('hello') 然后断言结果。
可跨页面复用。 如果需要在一个页面中同时用到编码和解码,直接调两次即可,没有任何状态冲突。
类型安全。 输入是 string,输出是 string。编译器可以在调用点验证类型正确性。
HashUtils 稍微特殊------因为 cryptoFramework.createMd() 是异步的:
typescript
export class HashUtils {
static async hash(input: string, algorithm: 'MD5' | 'SHA1' | 'SHA256'): Promise<string> {
let md = cryptoFramework.createMd(algorithm);
let uint8Array = new Uint8Array(buffer.from(input, 'utf-8').buffer);
await md.update({ data: uint8Array });
let result = await md.digest();
return buffer.from(result.data).toString('hex');
}
}
但核心原则不变:调用方在组件中写 this.output = await HashUtils.hash(this.input, 'MD5'),utils 层依然不碰 UI。
组件层(components):Prop 驱动的纯展示
组件层有两个组件:ToolCard 和 CopyButton。它们的设计原则是:内部没有 @State,所有数据通过 @Prop 从父组件传入,所有事件通过 onClick 回调冒泡。
typescript
@Component
export struct ToolCard {
@Prop tool: ToolItem;
@Consume('pathStack') pathStack: NavPathStack;
build() {
Column() {
Stack() {
Circle().width(48).height(48).fill(this.tool.color + '18');
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 的实现展示了组件中调用系统服务的模式:
typescript
@Component
export struct CopyButton {
@Prop text: string;
build() {
Button('复制')
.onClick(() => {
let data = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.text);
let sysPasteboard = pasteboard.getSystemPasteboard();
sysPasteboard.setData(data, (err) => {
if (err) {
this.getUIContext().getPromptAction().showToast({ message: '复制失败' });
} else {
this.getUIContext().getPromptAction().showToast({ message: '已复制到剪贴板' });
}
});
})
}
}
pasteboard 来自 @kit.BasicServicesKit,promptAction 通过 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 状态。每一层有独立的测试策略。