25-项目越写越散-用三层工程和模块边界守住长期维护
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;LearningCourseCard、ProfileAvatarService 这类带业务名的代码不应放进去。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 模块,按部署策略选择 |
不要因为“想拆包”就把每个文件变成模块。先定业务边界和依赖方向,再决定包形态;包是实现手段,不是架构目标。
迁移时从最小可验证切口开始
- 选一个频繁被跨层引用的 feature,列出其入口、服务和模型。
- 把可稳定复用的类型下沉到 common,其余留在 feature。
- 让 products 替换对内部页面的直接依赖。
- 跑构建和主路径,确认路由与资源没有丢失。
- 再处理下一个 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 加载失败时有清晰的回退或提示 |
把这些验证放入每次小迁移的完成条件,才能避免“目录已经移动、实际页面却打不开”的半成品重构。
依赖规则需要随功能增长持续复查
新增模块时先写它属于哪一层、公开什么、依赖什么,再创建目录。代码审查中将“是否新增反向或横向依赖”作为固定问题;每次发布前运行一次反向搜索。规则不需要复杂工具才能生效,但必须反复执行,才能防止短期便利积累成长期架构债。
对于历史项目,可以先允许少量带注释的例外,再逐步减少。强行一次清零容易打断业务交付;没有目标日期的例外则会永久化。把例外登记为可关闭任务,边界才会随着工程演进真正变清晰。
架构规则最终服务于交付速度:清晰边界让新功能更容易找到归属,也让问题定位范围更小。只要每次新增依赖都先回答“它属于哪一层、为什么必须依赖”,三层结构就能持续带来维护收益。
这也是避免架构只停留在图纸上的关键。
更多推荐
所有评论(0)