该应用已上架鸿蒙应用商店欢迎各位下载尝鲜、吐槽拜谢系列第 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 能力、引用library2library2三国志业务页面、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 只知道 library2library2 只知道 library1library1 不知道上层模块。这种单向依赖带来三个好处好处具体体现入口层稳定EntryAbility、备份扩展、最终挂载不会被页面细节污染公共层可复用模型、主题、路由可以被业务模块和后续测试复用业务层可扩展MainFrame可以继续增长功能但不影响entry的启动职责反过来如果library1开始 importlibrary2或者公共模型里开始调用页面方法就说明拆分边界已经失效。七、调试命令用源码和构建一起验边界检查多模块拆分时我不会只看目录而是用命令确认三件事。第一确认模块注册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 productdefault --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做发布素材。