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 接口定义了一个工具的全部元数据:

typescript 复制代码
export interface ToolItem {
  id: string;          // 工具唯一标识
  name: string;        // 显示名称
  description: string; // 卡片描述文案
  icon: string;        // 图标文字(等宽字体渲染)
  color: string;       // 主题色(用于图标圆圈和卡片强调色)
  category: string;    // 分类(格式化/编解码/计算/文本)
  routerPath: string;  // 路由路径
}

七个字段,没有多余的。每个字段都有明确的用途:id 用于 Tab 筛选和路由匹配,namedescription 用于卡片展示,iconcolor 用于视觉区分,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 驱动的纯展示

组件层有两个组件:ToolCardCopyButton。它们的设计原则是:内部没有 @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.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

相关推荐
●VON3 小时前
HarmonyKit | 鸿蒙新特性应用:10 个开发工具的选型与分类
华为·harmonyos·鸿蒙
最爱老式锅包肉5 小时前
《HarmonyOS技术精讲-ArkWeb》进阶配置:自适应与交互控制
华为·交互·harmonyos
最爱老式锅包肉5 小时前
《HarmonyOS技术精讲-Core Speech Kit(基础语音服务)》第5篇:实战项目——构建全功能语音助手
华为·harmonyos
最爱老式锅包肉5 小时前
《HarmonyOS技术精讲-Core Speech Kit(基础语音服务)》第4篇:语音唤醒进阶——打造免提交互体验
交互·语音识别·harmonyos
我是小a6 小时前
鸿蒙 ArkUI 数据可视化图例对照表:组件化设计与实现
华为·信息可视化·harmonyos·鸿蒙·鸿蒙系统
TrisighT6 小时前
Electron 鸿蒙 PC 上点关闭就真退出了?我花了两天才把“最小化到托盘“做稳
electron·harmonyos
●VON7 小时前
HarmonyKit | 鸿蒙新特性:HdsTabs 沉浸光感底部导航完全指南
华为·harmonyos·鸿蒙·新特性
在书中成长7 小时前
HarmonyOS 小游戏《对战五子棋》开发第2篇-AppScope与entry模块
华为·harmonyos
2501_943782357 小时前
【共创季稿事节】密码生成器:如何构建一个安全的随机密码生成工具
android·数据库·安全·鸿蒙·鸿蒙系统