HarmonyOS应用实战-启示散页-09-HAP、HAR、HSP 不要混着放:用模块边界守住答案之书
答案之书虽然是轻量应用,但工程结构里已经出现主入口、公共模型、功能页面、资源和发布配置。如果 HAP、HAR、HSP 混着放,短期不一定编译失败,长期一定会出现资源找不到、依赖反向、公共层引用页面层这类问题。模块边界不是大项目专属,小应用越早守住边界,后续迭代越轻。
这篇文章会把问题拆成四个可落地的点:
- 区分 entry HAP、公共 HAR、功能 HSP 的责任。
- 资源跟随模块归属,不让页面跨模块偷资源。
- 公共层只放模型、常量和无 UI 业务工具。
- 用依赖方向和发布构建验证模块拆分是否健康。
1. 先定义模块责任
HAP 是应用入口,负责 Ability、窗口、路由挂载和发布配置;HAR 更适合放模型、常量、工具和纯逻辑;HSP 可以承载可独立复用的功能页面或资源。答案之书不需要为了拆而拆,但已经拆开的部分必须有清晰归属。
entry HAP:EntryAbility、pages/Index、module.json5、启动图标 common HAR:models、constants、id、formatter、validation feature HSP:DrawingPage、题库功能页、局部资源边界的目标是让依赖方向稳定,而不是追求目录数量。
2. 公共层不能引用页面层
最危险的反向依赖是 common 里为了方便拿 Toast、NavPathStack 或页面资源。公共层一旦引用页面层,所有模块都会被迫知道 UI 细节。答案之书的 common 只放类型和纯函数,页面行为留在 entry 或 feature。
// common/src/main/ets/models/Deck.tsexportinterfaceDeck{id:string;name:string;builtIn:boolean;answers:Answer[];createdAt:number;updatedAt:number;}模型可以被所有层引用,但模型不能知道页面怎么展示、怎么跳转、怎么弹 Toast。
3. 服务层依赖 Repository,不依赖页面
DeckService、FavoriteService、HistoryService 可以依赖 Repository 和模型,但不应该引用具体页面。这样 DrawingPage、DeckEditorPage、HistorySheet 都能复用同一套业务能力。
exportclassDeckService{asyncsave(payload:SaveDeckPayload):Promise<Deck>{constdeck:Deck=this.createDeck(payload);awaitDeckRepository.saveDeck(deck);AppStorage.setOrCreate(AppStorageKey.LastDeckUpdateAt,deck.updatedAt);returndeck;}}Service 可以发刷新信号,但不要知道哪个页面会刷新。页面订阅信号,业务层只表达数据变化。
4. 资源跟着拥有者走
模块拆分后,资源最容易错放。DrawingPage 的局部背景、动画素材如果属于 feature,就放 feature 的 resources;应用图标、启动页图标属于 entry。不要让 HSP 页面长期引用 entry 的私有图片,否则拆包后路径很容易断。
{"module":{"name":"entry","type":"entry","abilities":[{"name":"EntryAbility","icon":"$media:app_icon","startWindowIcon":"$media:startIcon"}]}}资源引用要能回答“这个资源归谁发布”。如果答案不清楚,后面做包体压缩和上架素材核对会很痛苦。
5. 路由表不要散在多个页面
如果使用 Navigation,路由名和页面构建器应该集中管理。首页 push DrawingPage,收藏页 push DeckEditor,都应该使用同一套 RouteName,避免字符串散落导致点击无响应。
exportclassRouteName{staticreadonlyDrawing:string='DrawingPage';staticreadonlyDeckEditor:string='DeckEditorPage';staticreadonlyFavorites:string='FavoritesPage';}路由名属于工程协议。集中后,模块迁移或页面改名时才有确定修改点。
6. HSP 边界要看依赖方向
HSP 可以承载能力,但不应该反过来依赖 entry。比如 DrawingPage 如果放在 HSP,它可以依赖 common 的模型和服务接口,但不能直接读 entry 的EntryAbility或 entry 私有资源。
{"dependencies":{"common":"file:../common","drawingFeature":"file:../drawingFeature"}}依赖应该从入口指向功能,从功能指向公共。反过来就说明模块边界已经被打穿。
7. 包体优化要以模块为单位看
答案之书的资源压缩和包体控制不能只看最终 HAP 大小。要知道哪些图片属于启动页,哪些属于动画,哪些 rawfile 是默认题库。模块边界清楚后,包体变大时才能定位到具体模块。
Get-ChildItem.\entry\build\default\outputs-Recurse-Filter*.hap|Select-ObjectFullName,LengthGet-ChildItem.\libraryHSP\src\main\resources\base\media|Sort-ObjectLength-Descending|Select-Object-First 10 Name,Length包体排查不是最后一步才做的事。模块资源归属越清楚,发布前越容易解释体积变化。
8. 模块拆分后要跑构建闭环
只看源码目录不能证明模块边界正确。拆分、迁移资源、调整依赖后,要跑hvigorw assembleHap --no-daemon,并重点看资源找不到、依赖循环、路由目标缺失这类错误。
&'.\hvigorw.bat'assembleHap--no-daemon hdc shell hilog|Select-String'RouteName|ResourceManager|EntryAbility'构建通过说明依赖和资源基本闭合,但路由点击和资源展示仍需要真机或模拟器走一遍。
9. 验证与排障
模块边界验证要从依赖、资源、路由、发布四个角度看。页面能打开只是其中一项,更要确认公共层没有引用页面、功能资源没有借 entry 私有资源、发布构建没有临时文件。
验证点: 1. common 中不出现 pages、promptAction、NavPathStack 2. feature 页面引用自己的 media 或公共资源 3. RouteName 集中定义,没有散落字符串 4. assembleHap --no-daemon 成功 5. 真机点击首页、题库、收藏、历史入口都能进入目标页如果拆包后资源缺失,优先查资源归属和模块依赖,不要先把图片复制到所有模块。
还有一个实用判断:如果一个文件移动到别的模块后,需要顺手改一堆业务规则,说明它本来就不该被当成公共能力;如果只需要调整 import 和资源引用,说明边界相对健康。答案之书这种轻量应用不需要复杂架构,但要避免页面、服务、资源和发布配置互相绑死。
拆模块时也不要把“复用”理解成所有东西都上移。题库模型、答案模型、时间格式化这类稳定能力可以放公共层;DrawingPage 的动效参数、启动页插图、收藏页布局属于具体体验,留在功能或入口模块更合适。公共层越克制,后面越少出现所有模块都被迫重新构建的情况。
发布前还可以做一次反向搜索:在 common 里搜索pages/、EntryAbility、promptAction、NavPathStack,在 feature 里搜索 entry 私有资源名。搜索结果不一定全是错误,但每一处都值得解释清楚。
解释不清的依赖,通常就是下一次维护的故障点。
验证清单
- 清应用数据后从冷启动进入,确认默认数据、页面状态和日志分支符合预期。
- 对本文涉及的写路径准备正常、空值、重复、越界四类输入,确认错误停在 Service 或 Repository。
- 页面返回、重新进入、切换题库、收藏、历史或删除后,确认对应刷新信号触发重新读取。
- 修改资源或模块归属后重新构建,确认 HAP、HAR、HSP 的依赖方向没有反转。
- 涉及真机体验、备份恢复、发布素材的内容,单独记录是否已经在设备或平台侧验证。
常见问题与处理
| 现象 | 先看哪里 | 处理方式 |
|---|---|---|
| 拆包后图片不显示 | 资源是否放在页面所属模块 | 把局部资源迁到 feature,公共资源放公共资源模块 |
| common 编译依赖 entry | 公共层是否引用页面 API | common 只保留模型和纯工具 |
| 点击入口无响应 | RouteName 是否散落或拼错 | 集中路由协议并逐入口验证 |
| 包体突然变大 | 资源是否重复放多模块 | 按模块统计 media/rawfile 大小 |
小结
HAP、HAR、HSP 的边界不是形式,而是依赖方向、资源归属和发布可控性。小应用先把边界守住,后面加动画、导入、收藏和发布清单才不会越写越乱。