
HarmonyOS应用实战-启示散页-09-HAP、HAR、HSP 不要混着放用模块边界守住答案之书答案之书虽然是轻量应用但工程结构里已经出现主入口、公共模型、功能页面、资源和发布配置。如果 HAP、HAR、HSP 混着放短期不一定编译失败长期一定会出现资源找不到、依赖反向、公共层引用页面层这类问题。模块边界不是大项目专属小应用越早守住边界后续迭代越轻。这篇文章会把问题拆成四个可落地的点区分 entry HAP、公共 HAR、功能 HSP 的责任。资源跟随模块归属不让页面跨模块偷资源。公共层只放模型、常量和无 UI 业务工具。用依赖方向和发布构建验证模块拆分是否健康。1. 先定义模块责任HAP 是应用入口负责 Ability、窗口、路由挂载和发布配置HAR 更适合放模型、常量、工具和纯逻辑HSP 可以承载可独立复用的功能页面或资源。答案之书不需要为了拆而拆但已经拆开的部分必须有清晰归属。entry HAPEntryAbility、pages/Index、module.json5、启动图标 common HARmodels、constants、id、formatter、validation feature HSPDrawingPage、题库功能页、局部资源边界的目标是让依赖方向稳定而不是追求目录数量。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):PromiseDeck{constdeck:Deckthis.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:stringDrawingPage;staticreadonlyDeckEditor:stringDeckEditorPage;staticreadonlyFavorites:stringFavoritesPage;}路由名属于工程协议。集中后模块迁移或页面改名时才有确定修改点。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.batassembleHap--no-daemon hdc shell hilog|Select-StringRouteName|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公共层是否引用页面 APIcommon 只保留模型和纯工具点击入口无响应RouteName 是否散落或拼错集中路由协议并逐入口验证包体突然变大资源是否重复放多模块按模块统计 media/rawfile 大小小结HAP、HAR、HSP 的边界不是形式而是依赖方向、资源归属和发布可控性。小应用先把边界守住后面加动画、导入、收藏和发布清单才不会越写越乱。