项目演进路线:从单页面到多模块的架构升级策略

一、引言
任何一个成功的应用都不是一蹴而就的,而是从简单的单模块结构逐步演进到复杂的分层架构。本文以一个当前的 11 模块分层架构案例为蓝本,回溯其可能的演进路线------从最初的一个 entry 模块,逐步拆分为 features → commons → components 四层架构。
理解这个演进过程,能帮助开发者在自己的项目中做出恰当的架构决策:什么时候拆分?先拆什么?接口如何先行?依赖如何治理?
二、阶段一:单模块起步
2.1 初始结构
项目初始阶段,所有代码都在 product/entry 一个模块中:
product/entry/
src/main/ets/
pages/ # 所有页面
components/ # 所有组件
models/ # 所有数据模型
utils/ # 所有工具类
viewmodels/ # 所有 ViewModel
resources/
2.2 单模块的优势与痛点
优势:
- 开发效率高,无需考虑模块间通信
- 调试简单,不存在跨模块断点问题
- 构建速度快,无需处理多模块依赖
痛点:
- 编译速度随代码量增加而线性下降
- 代码耦合严重,难以进行独立测试
- 无法实现组件级别的复用
- 团队协作时冲突频繁
2.3 何时需要拆分?
根据实践经验,出现以下信号时应该考虑拆分:
- 代码量信号 :
src/目录超过 50 个文件,构建时间超过 3 分钟 - 团队信号:2 人以上同时修改同一模块,频繁产生合并冲突
- 复用信号:发现相同或类似的 UI 组件在多个页面中重复实现
- 业务信号:不同的业务功能(首页/我的/答题)边界逐渐清晰
三、阶段二:横向拆分 --- 三层架构
3.1 识别业务边界
首先分析应用的业务功能,识别出清晰的业务模块:
- 首页(homePage):课程展示、搜索、分类浏览
- 我的(minePage):个人中心、设置、学习记录
- 答题(topicPage):答题引擎、试卷、错题本
这三个业务模块之间的交互很少,天然适合横向拆分。
3.2 建立三层架构
第一次拆分形成三层架构:
product/entry/ ← 应用入口,承载路由表和 Tab 导航
└── 依赖三个 feature
features/
├── homePage/ ← 首页业务
├── minePage/ ← 我的业务
└── topicPage/ ← 答题业务
每个 feature 模块拥有自己的页面、ViewModel、模型和资源,通过 Index.ets 导出页面 Builder:
typescript
// features/homePage/Index.ets
export { HomePageBuilder } from './src/main/ets/pages/MainPage';
export { FeaturedCoursesBuilder } from './src/main/ets/pages/FeaturedCourses';
export { SearchPageBuilder } from './src/main/ets/pages/SearchIndexPage';
// ...
3.3 路由解耦:RouterTable + RouterModule
拆分后最大的挑战是路由管理。项目采用 RouterTable 统一注册 + RouterModule 统一跳转的模式:
typescript
// product/entry/src/main/ets/model/RouterTable.ets
import { ChapterPracticeBuilder } from 'homepage';
import { AnswerQuestionsBuilder } from 'topicpage';
import { SetUpPageBuilder } from 'minepage';
export class RouterTable {
static builderMap: Map<string, WrappedBuilder<[object]>> = new Map();
public static routerInit() {
RouterTable.builderMap.set(RouterMap.MAIN_PAGE, wrapBuilder(MainEntryBuilder));
RouterTable.builderMap.set(RouterMap.LOGIN_PAGE, wrapBuilder(LoginPageBuilder));
RouterTable.builderMap.set(RouterMap.ANSWER_QUESTIONS_PAGE, wrapBuilder(AnswerQuestionsBuilder));
RouterTable.builderMap.set(RouterMap.SET_UP_PAGE, wrapBuilder(SetUpPageBuilder));
// ... 30+ 路由注册
}
}
通过 RouterModule 封装导航操作:
typescript
// commons/commonLib/src/main/ets/utils/RouterModule.ets
class RouterModule {
public static stack: NavPathStack = new NavPathStack();
static builderMap: Map<string, WrappedBuilder<[object]>> = new Map();
public static push(info: NavRouterInfo) {
RouterModule.stack.pushPathByName(info.url, info.param, info.onPop);
}
public static pop() {
RouterModule.stack.pop();
}
public static getNavParam<T>(info: NavRouterInfo): T | undefined {
const paramsArr = RouterModule.stack.getParamByName(info.url) as T[];
return paramsArr.pop();
}
}
这种模式的精妙之处在于:feature 模块不直接引用 RouterTable,而是通过 RouterMap 枚举引用路由名,由 entry 模块负责实际的路由注册。
四、阶段三:纵向拆分 --- 提取 commons 层
4.1 发现重复代码
随着三个 feature 模块的开发,出现了一些跨模块共享的代码:
BreakpointModel+BreakpointType:所有模块都需要响应式布局RouterModule:路由工具需要被所有模块使用CommonHeader/TopBar/Banner:通用 UI 组件Logger/PreferenceUtil/AudioPlayer:工具类- 各种 Manager 和 Model
4.2 抽取 commonLib
将这些共享代码提取为 commons/commonLib 模块:
commons/commonLib/
src/main/ets/
components/ # 通用 UI 组件
Banner.ets
CommonHeader.ets
TopBar.ets
constants/ # 路由枚举
RouterMap.ets
managers/ # 业务管理器
DashboardManager.ets
ReminderManager.ets
StatisticsManager.ets
models/ # 通用数据模型
BreakpointModel.ets
SampleModel.ets
WordCardModel.ets
utils/ # 工具类
AudioPlayer.ets
BreakpointUtils.ets
Logger.ets
PreferenceUtil.ets
RouterModule.ets
viewModel/ # 通用 ViewModel
OrderInfo.ets
通过 Index.ets 统一导出:
typescript
// commons/commonLib/Index.ets
export { Logger } from './src/main/ets/utils/Logger';
export { BreakpointType, BreakpointModel } from './src/main/ets/models/BreakpointModel';
export { RouterModule, RouterMap } from './src/main/ets/utils/RouterModule';
export { Banner } from './src/main/ets/components/Banner';
export { BaseViewModel } from './src/main/ets/models/BaseViewModel';
export { PreferenceUtil } from './src/main/ets/utils/PreferenceUtil';
// ...
4.3 commonLib 的依赖方向
commonLib 是纯底层模块,不依赖任何 feature 或 component:
homePage ──┐
minePage ──┼──→ commonLib
topicPage ─┘
entry ──────┘
所有依赖箭头指向 commonLib,commonLib 不反向依赖任何人。
五、阶段四:精细拆分 --- 组件层
5.1 识别可复用的业务组件
在 feature 模块的开发中,识别出可独立成组件的业务能力:
| 组件 | 识别信号 | 来源模块 |
|---|---|---|
| base_select | 多个页面需要选择器 | homePage |
| select_category | 分类选择逻辑可复用 | homePage |
| answer_questions | 答题引擎独立性强 | topicPage |
| login_info | 登录逻辑在各处复用 | entry |
| aggregated_payment | 支付收银台独立 | minePage |
| feedback | 反馈功能可复用 | minePage |
| search | 搜索 UI 独立 | homePage |
| search_question | 搜索题目逻辑独立 | homePage |
5.2 组件提取原则
提取组件时遵循"接口先行"原则:
第一步:定义接口
typescript
// 组件提取前,先在原位置定义接口
interface SelectCategoryProps {
categories: CategoryItem[];
onSelect: (item: CategoryItem) => void;
selectedId?: string;
}
第二步:实现组件
typescript
// 创建组件模块,实现接口
@ComponentV2
export struct SelectCategory {
@Param categories: CategoryItem[] = [];
@Event onSelect: (item: CategoryItem) => void = () => {};
@Param selectedId: string = '';
// 实现细节
}
第三步:导出并在原处替换
typescript
// 原模块中移除实现代码,改为导入
import { SelectCategory } from 'select_category';
5.3 组件的依赖策略
提取后的组件遵循"最小依赖"原则:
- 7 个组件零外部依赖
- 仅
feedback组件依赖 axios 等网络库 - 组件不依赖 commonLib(避免与应用业务耦合)
这种设计使得组件可以脱离本项目,直接复用到其他 HarmonyOS 应用中。
六、最终架构全景
经过四个阶段的演进,项目形成了完整的四层架构:
┌─────────────────────────────────────────────────────────┐
│ product/entry │
│ 路由表(RouterTable)、Tab 导航、Ability 生命周期 │
│ 依赖:所有 features + components │
├─────────────────────────────────────────────────────────┤
│ features 层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ homePage │ │ minePage │ │topicPage │ │
│ │ 首页业务 │ │ 我的业务 │ │ 答题业务 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
├───────┼──────────────┼──────────────┼───────────────────┤
│ └──────┬───────┘ │ │
│ ▼ ▼ │
│ commons/commonLib │
│ 通用组件、模型、工具类、路由、资源 │
│ 被所有 features、components、entry 依赖 │
├─────────────────────────────────────────────────────────┤
│ components 层 │
│ base_select select_category answer_questions │
│ login_info aggregated_payment feedback │
│ search search_question │
│ 独立 HAR 包,零依赖或极少依赖,可跨项目复用 │
└─────────────────────────────────────────────────────────┘
模块间的依赖关系通过 build-profile.json5 清晰体现:
json5
{
"modules": [
{ "name": "entry", "srcPath": "./product/entry" },
{ "name": "homePage", "srcPath": "./features/homePage" },
{ "name": "minePage", "srcPath": "./features/minePage" },
{ "name": "topicPage", "srcPath": "./features/topicPage" },
{ "name": "commonLib", "srcPath": "./commons/commonLib" },
{ "name": "base_select", "srcPath": "./components/base_select" },
// ... 其余 7 个组件
]
}
七、渐进式重构策略
7.1 重构原则
- 每次只改一层:先做横向拆分(feature),稳定后再做纵向拆分(commons)
- 接口先行:先定义接口和导出,再移动实现
- 持续构建可工作的版本:每一步拆分后都要确保应用可以正常构建和运行
- 小步提交:每次重构提交一个独立的模块,而不是一次性提交所有变更
7.2 重构优先级建议
第一阶段(1-2 天):提取 commonLib
├── 通用工具类(Logger、PreferenceUtil)
├── 通用模型(BreakpointModel)
└── 通用组件(Banner、TopBar)
第二阶段(2-3 天):拆分为三个 feature
├── homePage(首页相关页面)
├── minePage(我的相关页面)
└── topicPage(答题相关页面)
第三阶段(3-5 天):提取 8 个组件
├── 先拆分零依赖组件(base_select、search 等)
└── 再拆分有依赖组件(feedback)
7.3 依赖治理的检查清单
拆分后,定期检查以下依赖规则是否被违反:
- entry 依赖所有模块,但没有任何模块依赖 entry
- feature 模块依赖 commonLib,但不依赖其他 feature
- component 模块不依赖任何 feature
- commonLib 不依赖任何模块(纯底层)
- 无循环依赖链(A → B → A)
八、总结:架构演进的关键决策
| 阶段 | 决策 | 收益 |
|---|---|---|
| 单模块 | 快速验证业务 | 开发效率最大化 |
| 横向拆分 | 按业务边界拆为三个 feature | 团队并行开发,构建速度提升 |
| 纵向拆分 | 提取 commonLib | 消除重复代码,统一基础能力 |
| 组件化 | 提取 8 个独立组件 | 支持跨项目复用,极致解耦 |
架构演进不是一蹴而就的工程,而是随着业务发展和团队规模变化持续调整的过程。理解何时拆分、拆到什么粒度、用什么策略拆,比理解最终架构本身更重要。
对于正在规划 HarmonyOS 应用架构的团队,建议从单模块起步,在出现明确的代码复用需求和团队协作痛点时,按照本文的四个阶段逐步演进,最终构建出适合自己业务的分层架构。
九、参考源码路径
| 文件 | 路径 |
|---|---|
| 根模块配置 | d:\HarmonyOS\WorkSpace\Exam1.0.4\build-profile.json5 |
| RouterTable | d:\HarmonyOS\WorkSpace\Exam1.0.4\product\entry\src\main\ets\model\RouterTable.ets |
| RouterMap | d:\HarmonyOS\WorkSpace\Exam1.0.4\commons\commonLib\src\main\ets\constants\RouterMap.ets |
| RouterModule | d:\HarmonyOS\WorkSpace\Exam1.0.4\commons\commonLib\src\main\ets\utils\RouterModule.ets |
| commonLib Index | d:\HarmonyOS\WorkSpace\Exam1.0.4\commons\commonLib\Index.ets |
| homePage Index | d:\HarmonyOS\WorkSpace\Exam1.0.4\features\homePage\Index.ets |
| minePage Index | d:\HarmonyOS\WorkSpace\Exam1.0.4\features\minePage\Index.ets |
| topicPage Index | d:\HarmonyOS\WorkSpace\Exam1.0.4\features\topicPage\Index.ets |
| homePage oh-package | d:\HarmonyOS\WorkSpace\Exam1.0.4\features\homePage\oh-package.json5 |
| entry oh-package | d:\HarmonyOS\WorkSpace\Exam1.0.4\product\entry\oh-package.json5 |