【三国志 App 实战系列 14】HarmonyOS 多模块工程拆分实战:entry、library1、library2 的边界设计
该应用已上架鸿蒙应用商店,欢迎各位下载尝鲜、吐槽!拜谢!
系列第 14 篇。上一篇解决的是 Stage 生命周期如何把前后台信号传给听书页面;这一篇换到工程组织层面,讨论一个更长期的问题:当三国志 App 的人物、事件、地图、听书、收藏、备份都堆到一起后,哪些代码应该留在 entry,哪些应该下沉到共享库,哪些适合成为业务模块?

一、真实问题背景:页面能跑不等于工程边界清晰
《耳畔三国·将星落》最早可以只靠一个入口页面完成原型:首页、人物、事件、地图、收藏、听书都写在一起,调试速度很快。但当功能进入第二轮维护后,问题开始变得明显:
| 问题 | 单体写法的表现 | 长期风险 |
|---|---|---|
| 页面入口和业务 UI 混在一起 | entry 既负责启动又承载复杂页面 |
后续接入备份、评论、生命周期时入口层越来越重 |
| 模型和主题散落在页面里 | Person、AudioRecord、主题 token 被多处引用 |
新功能扩展时容易复制类型 |
| mock 内容和页面渲染耦合 | 数据数组跟 UI 状态写在同一层 | 后续替换本地内容源成本高 |
| 依赖方向不清楚 | 公共类型反过来引用业务页面 | 多模块构建容易出现循环依赖 |
所以第 14 篇不讲单个功能,而是复盘当前项目为什么拆成 entry、library1、library2,以及这个拆分对后续维护到底有没有价值。
本文基于当前 HarmonyOS NEXT / ArkTS 工程实测,源码对象集中在:
build-profile.json5
entry/oh-package.json5
library1/Index.ets
library2/Index.ets
entry/src/main/ets/pages/Index.ets
library2/src/main/ets/pages/MainFrame.ets
先用 rg 定位真实依赖关系:
rg -n "from 'library1'|from 'library2'|export \{|MainFrame|MockRecords" entry library1 library2 -g "*.ets"
本项目的关键命中结果是:
entry/src/main/ets/pages/Index.ets:1:import { MainFrame } from 'library2';
library2/Index.ets:1:export { MainFrame } from './src/main/ets/pages/MainFrame';
library2/Index.ets:2:export { MockRecords } from './src/main/ets/data/MockRecords';
library2/src/main/ets/pages/MainFrame.ets:1:import { AppColors, AppFontSize, AppRadius, AppSpacing, AppTheme, AudioRecord, Faction, FavoriteRecord, HistoryEvent, MapMarker, NoteRecord, Person } from 'library1';
library2/src/main/ets/data/MockRecords.ets:1:import { AudioRecord, Faction, FavoriteRecord, HistoryEvent, MapMarker, NoteRecord, Person } from 'library1';
library1/Index.ets:1:export { AppColors, AppFontSize, AppRadius, AppSpacing, AppTheme } from './src/main/ets/theme/AppTheme';
library1/Index.ets:2:export { Person, HistoryEvent, Faction, FavoriteRecord, NoteRecord, AudioRecord, MapMarker, ArticleRecord } from './src/main/ets/models/RecordsModels';
这说明当前依赖方向是:
entry -> library2 -> library1
只要这条方向保持单向,入口、业务页面、公共模型就不会互相拖拽。
二、模块清单:build-profile 只公开 modules,不公开签名材料
项目根目录的 build-profile.json5 同时包含签名配置和模块列表。公开文章里只适合展示模块声明,不应该粘贴签名证书路径、密码、profile 等敏感字段。
可公开的模块声明是这一段:
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
},
{
"name": "library1",
"srcPath": "./library1"
},
{
"name": "library2",
"srcPath": "./library2",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
}
]
我把三个模块拆成下面的职责:
| 模块 | 当前职责 | 不应该放什么 |
|---|---|---|
entry |
应用入口、Ability、备份扩展、最终页面挂载 | 大量业务 UI、mock 内容、主题模型 |
library1 |
主题 token、数据模型、路由常量等基础能力 | 依赖页面、调用系统 UI 能力、引用 library2 |
library2 |
三国志业务页面、mock 数据、听书/收藏/地图交互 | 应用签名配置、Ability 生命周期入口 |
这不是为了“模块多就高级”,而是为了让每层变更的理由不同。
三、entry:只做入口挂载,不承载业务页面细节
entry/src/main/ets/pages/Index.ets 现在非常薄:
import { MainFrame } from 'library2';
@Entry
@Component
struct Index {
build() {
Column() {
MainFrame();
}
.width('100%')
.height('100%')
}
}
这一层的价值不是代码多,而是边界清楚。entry 负责让应用启动起来,并把真实主界面挂进去。备份扩展、生命周期回调、模块元信息也属于入口模块;但人物列表怎么筛选、听书如何分段、收藏怎么持久化,不应该反向塞回 entry。
如果以后要加启动页、隐私弹窗、账号态或应用级路由,entry 可以继续承接入口级逻辑;如果只是业务 Tab 里的页面状态,仍然应该留在 library2。
四、library2:业务功能模块承载页面和本地内容
entry 依赖 library2 的方式写在 entry/oh-package.json5:
{
"name": "entry",
"version": "1.0.0",
"description": "Records of the Three Kingdoms entry module.",
"dependencies": {
"library2": "file:../library2"
}
}
library2 自己再依赖 library1:
{
"name": "library2",
"version": "1.0.0",
"description": "Records of the Three Kingdoms feature module.",
"main": "Index.ets",
"dependencies": {
"library1": "file:../library1"
}
}
library2/Index.ets 对外只暴露业务入口:
export { MainFrame } from './src/main/ets/pages/MainFrame';
export { MockRecords } from './src/main/ets/data/MockRecords';
在 MainFrame.ets 里,业务页面使用 library1 的模型、主题和数据类型:
import {
AppColors,
AppFontSize,
AppRadius,
AppSpacing,
AppTheme,
AudioRecord,
Faction,
FavoriteRecord,
HistoryEvent,
MapMarker,
NoteRecord,
Person
} from 'library1';
import { ArticleRecord } from 'library1';
import { MockRecords } from '../data/MockRecords';
这说明 library2 的职责是“把公共模型变成可交互页面”。例如首页快捷入口、人物详情、事件索引、地图、听书、收藏和设置入口都属于这一层。它可以引用 AppGalleryKit、CoreSpeechKit、AVSessionKit 这类业务能力,但不应该让 library1 反过来知道这些页面实现。
五、library1:公共模型和主题的稳定层
library1/Index.ets 的导出非常关键:
export { AppColors, AppFontSize, AppRadius, AppSpacing, AppTheme } from './src/main/ets/theme/AppTheme';
export {
Person,
HistoryEvent,
Faction,
FavoriteRecord,
NoteRecord,
AudioRecord,
MapMarker,
ArticleRecord
} from './src/main/ets/models/RecordsModels';
export { Routes, RouteParams } from './src/main/ets/routes/Routes';
这里导出的都是“业务页面会用,但不依赖具体页面”的对象。比如 AudioRecord 只是描述听书条目,不关心 TTS 如何播放;AppTheme 只是提供色彩、间距、字号,不关心首页卡片如何排版;Routes 只是路由常量,不关心页面跳转按钮放在哪里。
以 AudioRecord 为例:
export class AudioRecord {
id: string = '';
targetId: string = '';
title: string = '';
durationText: string = '';
durationSeconds: number = 0;
listenedSeconds: number = 0;
constructor(id: string, targetId: string, title: string, durationText: string,
listenedSeconds: number, durationSeconds: number = 0) {
this.id = id;
this.targetId = targetId;
this.title = title;
this.durationText = durationText;
this.durationSeconds = durationSeconds;
this.listenedSeconds = listenedSeconds;
}
}
这个类型可以被听书页、收藏页、备份边界和后续内容扩展共同引用。它放在 library1,比放在 MainFrame.ets 里更稳定。
六、模块依赖图:单向依赖比模块数量更重要
下面这张图是本篇的核心,不是为了展示目录很多,而是确认依赖不会绕回来。

当前结构可以用一句话概括:
entry 只知道 library2,library2 只知道 library1,library1 不知道上层模块。
这种单向依赖带来三个好处:
| 好处 | 具体体现 |
|---|---|
| 入口层稳定 | EntryAbility、备份扩展、最终挂载不会被页面细节污染 |
| 公共层可复用 | 模型、主题、路由可以被业务模块和后续测试复用 |
| 业务层可扩展 | MainFrame 可以继续增长功能,但不影响 entry 的启动职责 |
反过来,如果 library1 开始 import library2,或者公共模型里开始调用页面方法,就说明拆分边界已经失效。
七、调试命令:用源码和构建一起验边界
检查多模块拆分时,我不会只看目录,而是用命令确认三件事。
第一,确认模块注册:
rg -n '"name": "entry"|"name": "library1"|"name": "library2"|srcPath' build-profile.json5
第二,确认包依赖方向:
rg -n '"library1"|"library2"|dependencies' entry/oh-package.json5 library2/oh-package.json5
第三,确认 ArkTS import 没有反向依赖:
rg -n "from 'library1'|from 'library2'" entry library1 library2 -g "*.ets"
如果要做构建验证,可以继续跑 Hvigor:
$env:DEVECO_SDK_HOME = 'D:\HuaweiDevelopFormalStudy\DevEco Studio\sdk'
$env:Path = 'D:\HuaweiDevelopFormalStudy\DevEco Studio\jbr\bin;D:\HuaweiDevelopFormalStudy\DevEco Studio\sdk\default\openharmony\toolchains;D:\HuaweiDevelopFormalStudy\DevEco Studio\tools\node;' + $env:Path
& 'D:\HuaweiDevelopFormalStudy\DevEco Studio\tools\hvigor\bin\hvigorw.bat' assembleHap --mode module -p product=default --no-daemon
本篇只新增文档和发布素材,不改 ArkTS 源码;如果你在项目中移动真实代码,构建验证就必须执行。
八、问题复盘:拆分不是越早越好,也不是越多越好
这次复盘后,我会把模块拆分的判断标准归纳成四条:
| 判断问题 | 适合拆到哪里 |
|---|---|
| 这个对象是否不依赖页面生命周期? | library1 |
| 这个对象是否承载具体业务交互? | library2 |
| 这个对象是否只在应用启动、Ability 或扩展声明里有意义? | entry |
| 这个对象是否包含签名、发布、构建环境信息? | 只留配置,不进入公共文章正文 |
当前项目选择 library1 放模型和主题,是因为这些对象被首页、详情、收藏、听书、地图共同消费。选择 library2 放 MainFrame 和 MockRecords,是因为它们已经包含业务组织方式:哪些 Tab、哪些内容、哪些交互入口,都不是纯模型。
如果在很早期就强行拆十几个模块,反而会让每次改一个字段都跨模块跳转。对这个三国志 App 来说,三层已经够用:入口层、公共基础层、业务功能层。
九、失败模式:这些拆法会让维护更难
| 失败模式 | 表面收益 | 实际问题 |
|---|---|---|
把所有页面都放进 entry |
初期路径短 | Ability、页面、数据、主题全部耦合 |
把 MainFrame 下沉到 library1 |
看起来复用更多 | 公共库反而依赖业务 UI,边界倒置 |
| 每个 Tab 拆一个模块 | 模块名更细 | 当前规模下跨模块改动成本大于收益 |
公开文章粘贴完整 build-profile |
代码看起来完整 | 容易泄露签名路径和密码字段 |
| 公共模型直接调用系统 Kit | 写业务方便 | 后续测试和复用会被系统能力绑定 |
最容易被忽略的是第四点。很多工程文章为了完整,直接贴完整配置文件;但 HarmonyOS 项目的构建配置可能包含证书路径、profile 和签名密码。公开分享时必须裁剪,只讲模块结构,不暴露签名材料。
十、验收清单
| 验收项 | 通过标准 |
|---|---|
| 模块声明 | build-profile.json5 中存在 entry、library1、library2 |
| 依赖方向 | entry -> library2 -> library1,没有反向 import |
| 入口页面 | entry/src/main/ets/pages/Index.ets 只挂载 MainFrame |
| 公共导出 | library1/Index.ets 导出主题、模型和路由 |
| 业务导出 | library2/Index.ets 导出 MainFrame 和业务数据入口 |
| 敏感信息 | 公开文章不粘贴签名密码和证书路径 |
| 构建验证 | 移动源码后 Hvigor 能通过 |
| 后续扩展 | 新功能能判断放入入口层、公共层还是业务层 |
十一、边界与后续演进
当前拆分仍然不是终点。
如果后续把内容数据从 mock 改成更完整的本地 JSON 或数据库,MockRecords 可以继续留在 library2,但数据加载器可能需要单独抽出来。原因是数据加载器会连接内容源、缓存和搜索索引,它不一定属于页面。
如果后续增加可复用组件库,例如统一空状态、列表项、详情页标题栏,可以考虑放到 library1/src/main/ets/components。但前提是组件不依赖 MainFrame 的内部状态,否则仍然应该留在 library2。
如果后续引入更多原子服务或独立入口,entry 的职责会更重要:它要决定不同入口挂载哪个业务能力,而不是让每个业务模块自己操作 Ability。
十二、小结
这次多模块拆分的核心结论是:模块边界要服务真实变更,不要只服务目录美观。
在当前三国志 App 里,合理的边界是:
entry: 应用入口、Ability、扩展声明、最终挂载
library1: 主题 token、数据模型、路由常量等稳定基础层
library2: 业务页面、本地内容、听书/收藏/地图/评论等交互能力
只要依赖方向保持 entry -> library2 -> library1,后续做资源体系、搜索、内容模型扩展、深浅色跟随系统时,代码就有明确落点。
下一篇会继续换到资源体系,讨论应用图标、启动图、功能图和宣发截图如何保持一致:哪些图片应该进 resources/base/media,哪些只适合放在 doc/generated_images 做发布素材。
更多推荐


所有评论(0)