HarmonyOS应用实战-启示散页-09-HAP、HAR、HSP 不要混着放:用模块边界守住答案之书

HarmonyOS应用实战-启示散页-09-HAP、HAR、HSP 不要混着放:用模块边界守住答案之书

答案之书虽然是轻量应用,但工程结构里已经出现主入口、公共模型、功能页面、资源和发布配置。如果 HAP、HAR、HSP 混着放,短期不一定编译失败,长期一定会出现资源找不到、依赖反向、公共层引用页面层这类问题。模块边界不是大项目专属,小应用越早守住边界,后续迭代越轻。

这篇文章会把问题拆成四个可落地的点:

  1. 区分 entry HAP、公共 HAR、功能 HSP 的责任。
  2. 资源跟随模块归属,不让页面跨模块偷资源。
  3. 公共层只放模型、常量和无 UI 业务工具。
  4. 用依赖方向和发布构建验证模块拆分是否健康。


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/EntryAbilitypromptActionNavPathStack,在 feature 里搜索 entry 私有资源名。搜索结果不一定全是错误,但每一处都值得解释清楚。

解释不清的依赖,通常就是下一次维护的故障点。

验证清单

  • 清应用数据后从冷启动进入,确认默认数据、页面状态和日志分支符合预期。
  • 对本文涉及的写路径准备正常、空值、重复、越界四类输入,确认错误停在 Service 或 Repository。
  • 页面返回、重新进入、切换题库、收藏、历史或删除后,确认对应刷新信号触发重新读取。
  • 修改资源或模块归属后重新构建,确认 HAP、HAR、HSP 的依赖方向没有反转。
  • 涉及真机体验、备份恢复、发布素材的内容,单独记录是否已经在设备或平台侧验证。

常见问题与处理

现象先看哪里处理方式
拆包后图片不显示资源是否放在页面所属模块把局部资源迁到 feature,公共资源放公共资源模块
common 编译依赖 entry公共层是否引用页面 APIcommon 只保留模型和纯工具
点击入口无响应RouteName 是否散落或拼错集中路由协议并逐入口验证
包体突然变大资源是否重复放多模块按模块统计 media/rawfile 大小

小结

HAP、HAR、HSP 的边界不是形式,而是依赖方向、资源归属和发布可控性。小应用先把边界守住,后面加动画、导入、收藏和发布清单才不会越写越乱。