
做鸿蒙应用拆得越细越会撞上一个问题好几个HAP长得不一样但内部都在重复同一套网络请求、登录态、埋点上报。一开始图省事每个HAP各写一份后来公共逻辑一处改了、另一处忘了同步版本乱得自己都记不住。这个系列做到第五篇正好把多HAP集成同一个HSP的事从头到尾理一遍——架构上怎么拆分版本上怎么收敛以及哪些坑实测下来最值得提前躲开。这篇内容主要面向两种人一种是工程里已经有多个HAP、正准备用HSP收敛公共代码的开发者另一种是刚接触鸿蒙模块化开发想搞清楚HAP、HSP、HAR之间到底什么关系的人。我会把概念、决策思路、配置细节和真实踩坑记录放在一起讲尽量让不同基础的读者都能直接落地上手。1. HAP、HSP、HAR三者的边界为什么共享这件事会变成一个架构问题1.1 三种包各自怎么理解先说结论HAP是能被系统安装和运行的最小单元HSP是运行期共享包HAR是编译期静态共享包。三者共享代码的时机完全不同理解这一点是后面所有架构决策的前提。HAPHarmonyOS Ability Package承载着一个应用里可被安装的代码、资源和Ability清单。用户从应用市场下载一个应用实际落地的就是若干个HAP的组合。每个HAP是独立上版本、独立签名的系统把它当成一个可运行的整体来对待。HSPHarmonyOS Shared Package不参与安装入口它是一份运行期共享的代码和资源。听起来像库但它比普通库多了一层工程属性HSP有自己的module配置、自己的资源目录、自己的导出入口通常是Index.ets可以单独构建、单独发版本但只有被某个HAP依赖时才会真正运行。HARHarmonyOS Archive是最容易和HSP混淆的。HAR在编译期就被完整地拷贝进依赖它的HAP里。也就是说多个HAP各自依赖同一个HAR实际上每个HAP内部都有一份拷贝。HSP则不同多个HAP运行时读的是同一份代码和资源。有一个类比很贴切HAR像你把一张图复印了三份三个文件夹各放一份谁改谁的互不影响HSP像三个人看同一块白板任何改动所有人立刻可见。也正是这个立刻可见的特性让版本管理变得格外重要。下面这张表可以帮你快速对比维度HAPHSPHAR是否可独立安装是否否代码共享时机不共享运行时共享编译期拷贝是否有独立模块配置有有有是否参与App Pack组成是是随依赖它的HAP否编译期已内嵌对包体积的影响每个HAP独立全局只保留一份每个依赖的HAP各带一份版本升级的影响范围仅自身所有依赖它的HAP仅当前HAP1.2 什么场景下真的需要多HAP 公共HSP我见过有人为了技术先进硬拆HSP结果公共模块没有几个倒是多了一整套依赖管理和构建配置的负担。一个单入口、单团队的小应用完全没有必要引入HSP。真正需要多HAP集成同一个HSP的场景我归类为四种第一种是主应用拆成多个HAP。比如主入口是一个HAP账号、支付、扫码等功能各自成HAP它们都需要调用同一套登录态、网络层、配置中心和通用组件。没有HSP的话每个HAP只能各写一份或者用HAR重复打三份进包体体积和维护成本都很难受。第二种是超级应用式的工程结构。壳工程做宿主功能模块以独立HAP的方式挂在壳下面由不同的小组独立开发、独立发布。这种结构下公共基础能力必须有一个统一的运行期出口HSP是比HAR更合理的选择。第三种是按需下载和动态加载场景。有些体积大、更新频率低的能力比如扫码引擎、客服SDK很适合放进HSP配合按需分发机制不让用户一次性下载全量包。这个场景下HSP的运行时共享特性几乎是唯一解。第四种是团队协作边界的划分。公共模块由专门的基础设施团队维护时HSP的独立版本号、独立发布链路能让公共团队和业务团队各管各的版本节奏比所有人改同一个HAR要干净得多。1.3 引入HSP后工程结构的变化没有HSP时一个App工程的结构通常是entryHAP 若干个HAR所有HAR编译进对应HAPApp Pack里基本就是几个互不相干的HAP。引入HSP之后工程结构会发生两个明显变化第一App Pack的组成从多个HAP各自完整变成多个HAP 一个或多个HSP的组合。HSP会跟着引用它的HAP一起进入App Pack上架前做整体签名。运行期系统会根据HAP的依赖声明加载对应的HSP相当于在HAP之间搭了一座共享的桥。第二模块的组织方式从谁需要谁带一份变成公共底座统一维护。你需要额外管理HSP模块的构建顺序、版本号、导出接口和混淆规则。这些东西在HAR时代几乎不用操心现在都变成了架构的一部分。2. 架构设计把共享能力沉到HSP把业务边界划到HAP2.1 拆分的第一原则按复用频率而不是按团队感觉架构设计的第一步不是画图而是决定哪些代码进HSP、哪些留在HAP、哪些用HAR。我用的标准非常朴素多个HAP真正会复用的才进HSP只在一个HAP里用的就留在原地被HSP依赖但不跨越HAP共享的底层实现保持HAR。最容易犯的错是把看起来挺通用的全部下沉到HSP。HSP真的不是越厚越好。公共HSP过厚会带来两个实际后果一是版本升级时牵动所有HAP一起回归改动风险面急剧放大二是构建产物和初始化阶段单包体积和加载时间都会明显上升尤其对按需分发场景很不友好。拿我手上的工程举例最终沉淀到HSP的只有四类网络请求封装、统一登录会话、埋点上报、通用UI组件。订单、支付、个人中心这类有明确业务边界的模块全部留在各自的HAP里。边界划清楚之后这个接口该放哪基本不用讨论。2.2 依赖方向HAP可以依赖HSPHSP不要反向依赖HAP多HAP工程里依赖方向是架构的底牌。这里有一条铁律HAP依赖HSP没问题HSP绝对不要反向依赖某个HAP。理由不复杂。HSP一旦反向依赖HAP它就不再是公共共享层而是携带了特定业务上下文。别的HAP引用这个HSP时轻则编译报错重则运行期行为分裂。具体落到代码层面我会强制约束三点HSP的导出入口只放稳定能力不放跟具体业务HAP强相关的逻辑。判断标准很简单这个函数如果离开某个业务HAP就讲不清楚它就不该出现在HSP里。HSP之间允许互相依赖但层级要控制。打破这个约束会出现循环依赖尤其在HSP和HAR混合的时候构建系统和IDE的告警不够明显直到运行期才暴雷。HSP内部的代码可以依赖HAR和ohpm生态依赖包但绝不能出现import某个HAP模块的写法。工程规范里直接禁止这种引用关系。2.3 一个典型的多HAP 公共HSP分层结构下面是我们现有工程抽出来的分层结构看起来简单但它是经过三轮调整后才稳定的App Pack (xxx.app) ├── entry.hap // 主入口桌面图标、导航框架、页面路由壳 ├── pay.hap // 支付业务支付页、收银台、支付结果处理 ├── scan.hap // 扫码业务相机扫码、码解析、结果跳转 ├── past.hsp // 行为共享层网络、登录会话、埋点、错误码 └── uikit.hsp // UI共享层通用组件、主题、样式资源拆成common和uikit两个HSP而不是塞成一个是按变更频率和职责不同来考虑的。行为共享层里的网络、登录、埋点变更频率高几乎每个迭代都会动UI组件层的样式和组件相对稳定变更频率低很多。拆开之后需要升级UI组件时不必连累行为共享层的版本反之亦然。如果你把所有能力都塞进一个HSP每次小改动都要连带整包回归那才叫苦不堪言。2.4 设计阶段的决策清单在我这里每次新增一个模块或者要往HSP里加能力之前必须过一遍这组问题。你也可以直接把这个清单当成评审项这个能力会有几个HAP用到如果当前只有1个先留在HAP里等出现第二个使用者时再考虑下沉到HSP。过早下沉和过晚下沉的代价不同宁可晚一点。它会不会引用某个HAP的私有路由、私有数据模型或私有持久化文件会那它就不能进公共HSP。这一条违反之后短时间内看不出问题等第二个HAP引入时就是灾难。它的变更频率高吗高给它单独一个HSP模块别跟低频模块挤在一起低可以和类似稳定度的能力合并减少模块数量。它的改动会带来破坏性API变更吗会就走下一节说的版本升级流程这一步不是技术问题是发布流程问题。3. 版本管理HSP不是HAR升级条件比你想的更严格3.1 语义化版本HSP版本号的分量和HAR完全不同HSP的module配置里有versionName和versionCode发布到仓库后依赖声明里还会涉及版本范围。看起来和普通依赖差不多但影响面完全不同。HAR的版本升级影响是局部的。每个HAP编译时把自己那份HAR打进去其他HAP升级HAR版本已上线的旧HAP完全不受影响。你可以说HAR的版本是各管各的。HSP是运行期共享升级HSP就像改了一台公共服务器上的配置文件所有连上来的HAP行为一起变。这也是为什么HSP的版本策略必须比HAR严格得多。还有一点容易忽略HSP的版本号不是想升就升的。升了次版本号1.1.0变1.2.0所有依赖它的HAP只要语义兼容理论上可以不动但这种可以不动恰恰是风险因为运行期代码已经变了只是API签名没变行为差异仍然可能影响某些HAP。HSP版本升级后哪怕是小版本我也建议做一次全量回归。3.2 dependencies怎么写版本约束多个HAP必须收敛到同一个版本在HAP的oh-package.json5里声明对HSP的依赖时有一个容易忽略的细节。如果多个HAP都用ohos/common: file:../common这种本地模块方式引入那它们引用的其实就是同一个源码目录构建时会一起打包。这种写法在开发阶段很方便但本地HSP一旦直接改动影响面是全局的版本约束基本形同虚设。正式发布到ohpm仓库之后依赖声明就要用版本范围了{ name: entry, version: 1.0.0, dependencies: { ohos/common: ^1.2.0, ohos/uikit: ~1.0.1 } }这里的^和~语义和npm一致^1.2.0允许1.x范围内所有版本升级~1.0.1只允许补丁版本升级。这个机制在单HAP里没什么问题但在多HAP场景下有一个暗坑不同HAP声明的版本范围最终解析到运行时必须是同一个HSP版本。举个例子entry把ohos/common声明为^1.2.0pay把ohos/common声明为^1.0.0。假设仓库里最新HSP是1.2.0entry解析到1.2.0pay解析到1.0.0或1.1.x而这两个HAP装在一起后系统只能在一份HSP里做选择。一旦它们需要的接口版本不一致就会出现一个HAP正常、另一个HAP调用失败的诡异现象。所以多HAP工程的版本约束必须在设计阶段就约定统一所有HAP对同一HSP的依赖范围写一样或者干脆统一升级到最新兼容版本消除解析分歧。3.3 破坏性变更的发布顺序先兼容、再升级、后下线公共HSP做破坏性变更时绝对不能像内部HAR一样直接改、直接发。正确顺序是在现有HSP上保留旧接口、新增新接口发一个次版本比如1.2.0升到1.3.0。这个版本叫兼容版本旧HAP不用改代码也能跑。所有HAP适配新接口并把依赖声明统一指向这个兼容版本做一轮完整回归。等确认所有使用方都迁移完毕再发主版本升级比如1.3.0升到2.0.0并在2.0里清理掉废弃接口。如果违反顺序直接发2.0并把旧接口删掉最典型的事故是已在用户手机上的旧HAP还没更新但HSP已经通过某种渠道升级于是旧HAP调用HSP时运行期找不到方法。这种问题本地很难复现因为本地开发时所有HAP和HSP往往已经同步升上去了。3.4 版本冲突与锁文件多HAP工程里oh-package-lock.json5这个锁文件的分量比单模块工程里大得多。它锁定了每个HAP解析出来的具体依赖版本。遇到诡异问题第一步永远先查锁文件确认是不是某个HAP被锁在了不期望的旧版本。我恢复现场时十次里有七八次都是本地能跑、打包出错最后定位到的都是lock文件里的HSP版本和最新依赖不一致。改完依赖版本约束之后要记得重新生成lock文件不要手动删一行了事。删锁文件再重新解析通常没问题但如果有多个团队并行开发锁文件的变化会造成大量无谓的diff和冲突建议纳入代码评审范围。4. 工程配置与构建产物从模块创建到App Pack打包的关键细节4.1 创建HSP模块时module.json5的配置项在DevEco Studio里新建Shared Module之后生成的module.json5核心内容大概是这样{ module: { name: common, type: shared, srcEntry: ./Index.ets, description: $string:module_desc, mainElement: , deviceTypes: [phone, tablet, 2in1], deliveryWithInstall: true, installationFree: false, pages: $profile:main_pages, versionName: 1.2.0, versionCode: 102000 } }这里有几个点值得特意说明。type字段必须写成shared写成har或feature都会让模块类型错乱界面和构建行为完全不一样。这是我见过最多的低级错误。deliveryWithInstall表示HSP是否随应用安装分发。true是随包安装false是按需下载。如果做按需分发场景这个字段要和分发配置联动不能只在这里改一个值。versionName和versionCode是HSP自己的版本号。注意它和应用级别的app.json5版本号是独立的两者不必一致但要有映射关系。我习惯把versionCode设为应用版本号的10倍加上HSP内部序号这样调试时一眼能看出当前HSP对应哪个应用版本阶段。4.2 依赖声明与构建产物核对HAP依赖HSP之后构建产物会发生明显变化。一个正常构建的输出大致是这样build/default/outputs/default/ ├── AppPack/xxx-signed.app ├── default/entry-default.hap ├── default/pay-default.hap ├── default/scan-default.hap ├── default/common-default.hsp └── default/uikit-default.hsp打包阶段会把HSP和HAP一起装进App Pack。如果构建产出的App Pack里没有HSP十有八九是HAP的依赖声明没配好或者HSP没有被任何HAP引用被构建系统当成孤立模块跳过了。第一次引入HSP我强烈建议手工解包App Pack看一眼里面的文件结构确认HSP确实在包内、版本号正确。工具类的问题用工具排查永远是最高效的不要凭猜想改配置。4.3 调试模式下多HAP与HSP的联调方法DevEco Studio里Run一个多HAP工程时默认运行配置里指定的entry模块。HSP代码修改后Run某个入口HAP会把HSP跟着带上去这个流程本身是顺畅的。真正需要注意的是非入口HAP的调试。如果某次改动只涉及一个非入口HAP单独Run那个HAP时HSP可能没有重新打包用的是旧缓存。尤其你刚改过公共HSP的代码再去Run一个非入口HAP很容易踩中代码改了但行为没变的假象。我的做法是改公共HSP之后不要只Run某个HAP先Build整个App Pack或者干脆Clean Project之后再Run。虽然多花一点时间但能省掉大量排查是不是缓存的重复工作。5. 实测踩坑多HAP依赖HSP时最容易翻车的几个场景5.1 接口新增了但某个HAP运行时找不到符号这是多HAP HSP最经典的现场两个HAP依赖同一个HSPA运行一切正常B一调用新接口就报错Cannot find function或者Property not exist。我的排查链路是这样的你可以直接复用先确认HSP本身是否正常构建拿构建产物目录里的.hsp文件核对版本和导出接口。查报错模块的oh-package.json5看依赖声明里写的版本范围是否包含新增接口的版本。这里经常出问题开发时改了代码但依赖范围写的是旧版本起点。打开oh-package-lock.json5看这个模块实际解析到了哪个版本。很多时候oh-package.json5写的是^1.3.0但lock文件里还锁在1.2.0构建就跑在旧版本上。重新生成lock文件并提交问题通常就消失了。这一步的关键是不要先怀疑运行逻辑先确认打包进去的HSP到底是哪个版本。版本不对代码写得再对也没用。5.2 改了HSP构建却还是旧逻辑HSP的增量编译缓存是个老顽固。共享模块被多个HAP引用时增量编译如果依赖关系没有被完整识别可能命中旧的产物。具体表现就是HSP代码明明改了也触发重建了但打出来的包还是老逻辑。解法很简单粗暴Clean Project之后重新Build。如果你不想频繁全量构建可以检查工程里模块间依赖顺序的配置把HSP放在依赖链的前端位置。实测下来Clean远比手动删build目录可靠手动删目录偶尔会因为IDE缓存没刷新导致更奇怪的问题。5.3 Release包混淆后HSP导出类被优化Release构建默认开启混淆和裁剪这是常见坑。HSP暴露给外部使用的导出入口Index.ets里的类和方法如果没加混淆keep规则运行期调用就会出现NoSuchMethodError或method not found。处理方式是在混淆配置里给HSP的公开导出加keep规则。另一个重要经验是HSP的公开API必须全部在导出入口统一声明不要分散在深层类里。集中导出有两个好处一是调用方依赖关系清晰二是混淆规则可以集中配置不用逐个类去加keep降低漏配概率。如果你发现release包能跑但一调用某个HSP方法就崩优先怀疑混淆。这个问题的迷惑性在于debug包永远正常release包才暴露。5.4 签名不匹配导致安装失败多HAP和HSP打包进同一个App Pack时所有包共用同一个签名。开发阶段如果用了自动签名偶尔会因本地证书或SDK版本不同导致某个HAP的签名不一致安装时报错INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES。排查方法用SDK自带的工具检查每个包的签名信息确认多个包确实使用的是同一个证书。这也顺便说明为什么按需分发的HSP要配置好依赖HAP的分发规则——签名验证是整体性的任何一环不一致整个App Pack都可能装不上。如果现在让我重新设计一遍这个工程我会把版本策略的讨论放在架构设计之前而不是等模块拆完了才发现HSP版本联动比想象中复杂得多。这一篇讲到的架构拆分、版本约束、构建产物核对和几个踩坑场景都是这个系列里最值得反复看的部分。多HAP集成HSP方案本身不难难的是把依赖关系、发布顺序和版本边界管住。你把这些规则提前定好后面团队协作能少掉一半的沟通成本。