HarmonyOS 项目越写越散:用 products、features、common 守住依赖方向

摘要:目录分得再多,只要首页能直接 import 详情页、公共模块反向依赖业务模型,工程仍会越改越难。本文用 HarmonyOS 一多场景常见的三层结构说明边界:products 负责入口装配与设备差异,features 负责独立业务闭环,common 提供稳定公共能力;通过依赖方向、公开接口和反向搜索把规则落到代码里。

最危险的信号不是目录多,而是“为了方便先 import 一下”。一次横向 feature 调用、一次把业务对象放进 common,看似省时,后续就会让每个改动跨半个工程。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

三层各自回答不同的问题

application/
  products/default/       # Entry HAP:启动、装配、设备差异
  features/learning/      # 课程业务闭环
  features/profile/       # 资料业务闭环
  common/base/            # 稳定模型、工具、存储抽象
  common/ui/              # 通用组件与设计 token
可以依赖 不该依赖
products features、common 其他 products
features common sibling features、products
common 标准库/稳定公共模块 features、products

目录只是表达方式,真正的约束是依赖只能从上向下。

products 只装配,不吞业务规则

产品层适合放 EntryAbility、路由注册、设备级资源和入口组合。它负责把功能带到用户面前,不负责计算课程进度或校验资料字段。

export function registerDefaultRoutes(stack: NavPathStack): void {
  stack.pushPathByName('LearningHome', undefined)
}

如果产品层开始复制 feature 内部服务,手机与平板入口很快会得到两套不同规则;feature 只应暴露稳定页面或接口给它装配。

feature 必须能讲清自己的业务闭环

一个 feature 的页面、服务、模型和资源围绕同一领域组织。其他 feature 不直接 import 其内部页面;跨领域协作由产品层编排,或经 common 中稳定的接口传递。

features/learning/
  pages/LearningHomePage.ets
  services/LearningProgressService.ets
  models/LearningSummary.ets
  resources/base/

“能独立解释自己的输入、输出、数据 owner 和路由入口”是 feature 的最低标准。做不到时,先收窄领域,而不是继续加公共目录。

common 只放跨领域仍稳定的能力

// common/base/result/AppResult.ets
export type AppResult<T> =
  | { kind: 'success'; value: T }
  | { kind: 'failure'; message: string }

通用结果类型、格式化工具、设计 token、存储抽象适合 common;LearningCourseCardProfileAvatarService 这类带业务名的代码不应放进去。common 一旦知道具体 feature,反向依赖就已经发生。

用公开接口替代横向 import

错误方式是资料模块直接 import 学习模块的内部服务。更稳的方式是产品层取得两个 feature 暴露的接口,编排一次用户动作。

// products/default/services/HomeActionCoordinator.ets
async openLearningAndRefreshProfile(): Promise<void> {
  await this.learningEntry.openTodayTask()
  await this.profileEntry.refreshBadge()
}

协作发生在宿主层,feature 保持独立。若两边共享的是长期稳定模型,再考虑把模型下沉到 common,而非直接互相引用。

HAR、HSP 与 HAP 由边界决定

需要 合适形态
产品入口与 Ability Entry HAP
可编译期复用的稳定库 HAR
运行期共享的大型能力 HSP,按项目需求评估
独立业务功能 feature 模块,按部署策略选择

不要因为“想拆包”就把每个文件变成模块。先定业务边界和依赖方向,再决定包形态;包是实现手段,不是架构目标。

迁移时从最小可验证切口开始

  1. 选一个频繁被跨层引用的 feature,列出其入口、服务和模型。
  2. 把可稳定复用的类型下沉到 common,其余留在 feature。
  3. 让 products 替换对内部页面的直接依赖。
  4. 跑构建和主路径,确认路由与资源没有丢失。
  5. 再处理下一个 feature,避免一次大搬家无法定位回归。

每次迁移都应删除因拆分产生的旧 import 和旧别名;保留两条平行依赖路径只会让边界继续模糊。

用搜索把反向依赖暴露出来

rg "from .*features|import .*features" common
rg "from .*products|import .*products" features common
rg "from .*features/(learning|profile)" features

命中不一定都是错误:公开入口可作为例外,但要在模块 README 或依赖规则中写清原因。重点是让例外显式、可审查,而非隐蔽存在。

验证与常见误区

现象 根因 处理
common 改动牵动所有业务 common 混入领域模型 把业务类型迁回 feature
两个 feature 互相 import 横向协作无 owner 由 products 编排或抽稳定接口
拆模块后资源找不到 资源仍按旧 owner 放置 与页面一起迁移并验证引用
入口层业务膨胀 products 承担领域规则 下沉到 feature service

最后用 hvigorw assembleHap --no-daemon 验证编译,再从真实入口走一次路由、返回和资源加载。架构重组不做构建验证,往往会把路径和模块配置问题留到最后。

小结

三层工程不是三堆目录:products 是可变的入口装配,features 是独立的业务闭环,common 是向下稳定的公共能力。只要依赖方向持续可见、例外有记录、迁移按小切口验证,项目增长也不会自然变散。

页面、组件、服务也要延续同一条边界

模块拆好以后,模块内部仍可能重新变乱。推荐让 route 级页面处理组合、生命周期和页面局部状态,让组件只接收窄 props,让服务负责读取、写入与业务转换。

features/learning/
  pages/LearningHomePage.ets       # 页面组合与 onPageShow
  components/CourseProgressCard.ets # 纯展示与回调
  services/LearningProgressService.ets
  models/LearningSummary.ets

组件不应为了显示一张卡片而直接请求 Repository;这样同一组件放到另一个页面时会带入不可见的业务依赖。页面调用服务取得读模型,再把必要字段传给组件,测试和复用都会更清楚。

对跨层例外建立显式规则

完全没有例外并不现实,但例外需要被记录。例如产品层可依赖 feature 暴露的路由入口;common 可依赖一个真正稳定的第三方库。每个例外都应说明理由、依赖方向和替代方案,避免后来人复制成常规做法。

例外 可接受条件 不可接受做法
products 调 feature 入口 入口是公开契约 import feature 私有页面
feature 使用 common UI 组件无业务数据依赖 common UI 反查 feature 数据
feature 间共享模型 模型确实跨领域稳定 为方便把整个服务下沉

把规则放进仓库的模块 README 或依赖检查脚本,才能让架构从口头习惯变为可持续约束。

重构完成后检查不止编译

构建通过说明模块引用可解析,不说明用户路径可用。每次调整边界后还应从产品入口验证路由、资源、返回刷新和异常状态;尤其 HSP/HAR 拆分后,资源归属与导出名称最容易出错。先用小切口证明一条 feature 链路,再扩大迁移范围,风险远小于一次全工程搬迁。

验证层面 应证明什么
编译 模块依赖、导出名和资源引用可解析
路由 产品入口可进入 feature 并正确返回
数据 页面从服务读取而非跨层直接访问
资源 拆分后的图片、字符串和主题仍可加载
异常 feature 加载失败时有清晰的回退或提示

把这些验证放入每次小迁移的完成条件,才能避免“目录已经移动、实际页面却打不开”的半成品重构。

依赖规则需要随功能增长持续复查

新增模块时先写它属于哪一层、公开什么、依赖什么,再创建目录。代码审查中将“是否新增反向或横向依赖”作为固定问题;每次发布前运行一次反向搜索。规则不需要复杂工具才能生效,但必须反复执行,才能防止短期便利积累成长期架构债。

对于历史项目,可以先允许少量带注释的例外,再逐步减少。强行一次清零容易打断业务交付;没有目标日期的例外则会永久化。把例外登记为可关闭任务,边界才会随着工程演进真正变清晰。

架构规则最终服务于交付速度:清晰边界让新功能更容易找到归属,也让问题定位范围更小。只要每次新增依赖都先回答“它属于哪一层、为什么必须依赖”,三层结构就能持续带来维护收益。

这也是避免架构只停留在图纸上的关键。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐