从单文件 MVP 到分层架构:ArkUI 应用如何安全拆分而不破坏功能

单文件不是原罪

“心晴手记”HarmonyOS 第一版的主页面承担:

  • 隐私门禁;
  • 三步引导;
  • 今日、历史、统计、习惯、设置五个 Tab;
  • 习惯编辑和删除确认;
  • 历史日期编辑;
  • 应用锁遮罩;
  • 页面状态和统计派生函数。

对于需要快速完成平台迁移和功能对齐的阶段,这种结构有现实优势:

  • 状态都在一个组件中,联调路径短;
  • 不需要过早设计跨组件状态同步;
  • 容易与 iOS 功能表逐项对照;
  • 小团队可以迅速跑通完整体验。

问题不在于“曾经写在一个文件”,而在于功能稳定后是否继续无限增长。

一、出现哪些信号时应该开始拆分

1. 一个修改需要在文件中跳转很远

改习惯模型,却要来回寻找编辑弹窗、今日行、统计和删除确认。

2. Builder 数量持续增长

页面、卡片、弹窗和小组件混在同一命名空间。

3. 状态变量无法按功能归类

selectedMoodselectedMonthMillishabitNameDraftlocked 全部并列。

4. 测试只能通过完整页面进行

日期、统计和 CSV 逻辑无法独立验证。

5. 多人修改冲突频繁

不同功能都编辑同一个大文件。

满足两三项时,就可以规划渐进拆分。

二、不要从“把文件切成五份”开始

最危险的重构是机械移动:

TodayPage.ets
HistoryPage.ets
InsightsPage.ets
HabitsPage.ets
SettingsPage.ets

然后才发现每个页面都需要:

  • habits;
  • completions;
  • settings;
  • persist();
  • named();
  • uiRevision;
  • 多个回调。

结果可能只是把一个大文件变成大量参数传递和双向状态混乱。

正确顺序应先分离“纯逻辑”和“系统边界”,最后才移动有状态 UI。

三、第一步:先抽离纯函数

适合最先拆出的逻辑:

  • dayIdentifier()
  • CSV 转义;
  • 心情和标签定义;
  • 图标 Key 归一化;
  • 习惯统计分母计算;
  • 格式版本迁移函数。

纯函数特点:

输入明确
→ 输出明确
→ 不直接访问 ArkUI 状态
→ 不调用系统能力

例如:

export function completionRate(
  completed: number,
  eligible: number
): number {
  if (eligible === 0) {
    return 0;
  }

  return Math.min(
    100,
    Math.round(completed / eligible * 100)
  );
}

抽离后可以直接写单元测试,不必渲染整个统计页。

四、第二步:系统能力保持服务化

当前项目已经把这些能力分开:

AppRepository
AppLockService
ExternalService

页面只调用:

await authenticate(title)
await exportTextFile(context, fileName, content)
await appRepository.save(snapshot)

服务层不应该反过来修改 ArkUI 页面状态。它返回结果,页面决定显示 Toast、锁定层或错误信息。

这层边界在继续拆页面时非常重要,因为多个 Feature 都可以复用服务,而不需要彼此引用。

五、第三步:按领域组织,不按控件类型组织

一个可演进目录示意:

entry/src/main/ets/
├─ model/
│  ├─ MoodModels.ets
│  ├─ HabitModels.ets
│  └─ SettingsModels.ets
├─ data/
│  ├─ AppRepository.ets
│  └─ StateMigration.ets
├─ services/
│  ├─ AppLockService.ets
│  └─ ExternalService.ets
├─ features/
│  ├─ today/
│  ├─ history/
│  ├─ insights/
│  ├─ habits/
│  └─ settings/
└─ components/
   ├─ AppIcon.ets
   └─ SectionHeading.ets

features/insights 中应同时包含统计页 UI 和它专属的派生逻辑,而不是建立一个装着所有函数的 utils/ 垃圾桶。

六、第四步:定义唯一状态所有者

拆分前必须回答:

谁拥有 moodEntries?
谁拥有 habits?
谁负责 persist?
子页面如何发起修改?

对于当前规模,可以保留根页面为单一状态所有者:

App Root
├─ 状态数组
├─ Repository
└─ 把只读数据 + 业务回调传给 Feature

例如习惯页接收:

@Prop habits: Habit[];
onSaveHabit: (draft: HabitDraft) => void;
onArchiveHabit: (id: string) => void;
onDeleteHabit: (id: string) => void;

子页面不直接拿 Repository,也不自己维护另一份 habits 副本。

如果应用继续变大,再引入更系统的状态管理。不要因为拆文件就立即增加复杂框架。

七、区分“编辑草稿”和“持久化实体”

当前页面有:

habitNameDraft
habitIconDraft
habitColorDraft

拆分后这些草稿应该属于习惯编辑 Feature,而不是全局 AppState。

持久化状态:

habits[]

编辑状态:

当前弹窗是否打开
正在编辑哪个 ID
输入框临时值

把两者分离能减少根状态数量,也避免用户取消弹窗时污染真实数据。

八、统计逻辑应该接收快照,不直接读 UI

可以建立:

interface InsightInput {
  days: number;
  nowMillis: number;
  moodEntries: MoodEntry[];
  habits: Habit[];
  completions: HabitCompletion[];
}

然后:

function buildInsights(
  input: InsightInput
): InsightResult {
  // 计算记录天数、分布、完成率和总结数据
}

好处:

  • 测试可以固定 nowMillis
  • 不依赖系统当前日期;
  • 7 天和 30 天只是不同输入;
  • UI 只负责展示 InsightResult;
  • 将来更换页面结构不影响算法。

九、组件拆分后重点防范状态失联

原来同一组件内直接赋值:

this.habits = nextHabits;
this.refreshUi();

拆成子组件后,容易出现:

  • 子组件修改自己的副本,根状态没变;
  • 回调改变根状态,但子组件 Key 没更新;
  • @Prop@Link 或状态管理方式选择不当;
  • Builder 捕获旧闭包;
  • 列表复用导致局部仍显示旧值。

因此每拆一个 Feature,都要回归:

新增 → 编辑 → 归档 → 恢复 → 删除

不要一次移动全部页面后再统一排查。

十、推荐的渐进式拆分顺序

阶段 1:无风险抽离

  • 模型常量;
  • 纯函数;
  • 图标映射;
  • 格式化工具。

阶段 2:视觉组件

  • AppIcon;
  • IconBadge;
  • SectionHeading;
  • 通用空状态。

阶段 3:无复杂编辑的 Feature

  • 设置展示;
  • 隐私支持区;
  • 统计展示卡片。

阶段 4:带草稿和弹窗的 Feature

  • 习惯编辑;
  • 历史日期编辑;
  • 今日记录。

阶段 5:重构根状态

只有前面稳定后,再评估状态容器、页面路由和模块化。

十一、每一步都需要可回滚验证

重构前记录:

  • QA 清单;
  • 当前模拟器截图;
  • 关键统计样本;
  • 导出 JSON 示例;
  • 多语言资源 Key;
  • 构建命令。

每次只拆一个边界,并运行:

静态自检
→ ArkTS 编译
→ HAP 构建
→ 相关功能回归

重构成功的标准不是文件变短,而是行为保持一致、职责更清楚、测试更容易。

十二、什么时候不应该继续拆

如果抽出一个组件后:

  • 参数超过十几个;
  • 所有状态仍从根透传;
  • 组件只使用一次且没有独立语义;
  • 调试必须跨五个文件;
  • 测试并没有变容易;

说明拆分边界可能选错了。

架构目标不是文件数量最多,而是变化能够局部发生。

总结

从单文件 MVP 演进到分层架构,推荐遵循:

  1. 承认单文件在早期的效率价值;
  2. 先抽纯函数,再抽系统服务;
  3. 按领域组织 Feature;
  4. 明确唯一状态所有者;
  5. 草稿状态与持久化实体分离;
  6. 统计逻辑接收确定快照;
  7. 每次只拆一个边界并完整回归;
  8. 不为了“看起来架构化”制造参数地狱。

好的重构不是推倒重来,而是让已经验证过的产品行为在新结构中继续成立。

本文案例来自“心晴手记(MoodMemoir)”HarmonyOS 版当前架构及后续模块化规划。

参考资料


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值