一、开场白:代码多了,咱们咋办?

干过大型项目的兄弟都懂,一个 App 搞到后面,代码量能把你压死。几十个功能模块,五六个团队同时开发,今天你改个接口,明天他动个组件,后天整个应用就崩了。

我们一开始做 HarmonyOS 项目也踩过这个坑。entry 目录里堆了几百个文件,找个功能得翻半天。编译一次十几分钟,改个小按钮得等半天。最要命的是,不同团队代码搅在一起,谁也不敢随便动,怕把别人的功能搞挂了。

后来琢磨明白了,这玩意儿得模块化。但 HarmonyOS 的模块化跟你想的不太一样,它有三个包类型:HAP、HAR、HSP。刚开始我们也懵,这仨玩意儿到底啥区别?啥时候用哪个?

今天咱们就把这事儿整明白。从官方文档出发,结合实际场景,聊聊模块化设计到底咋搞。


二、为啥要模块化

2.1 HarmonyOS 的包结构是咋回事

在 HarmonyOS 里,应用包结构分三个阶段:开发态、编译态、发布态。简单说就是:

  • 开发态:你们写代码时的工程结构
  • 编译态:编译后生成的中间产物
  • 发布态:最终上架到应用市场的.app 文件

核心就三个玩意儿:

HAP(HarmonyOS Ability Package):应用的主体,分两种:

  • Entry 类型:应用的主入口,一个应用只能有一个
  • Feature 类型:独立功能模块,可以有多个

HAR(HarmonyOS Archive):静态共享库,编译时会被打包进 HAP 里

HSP(HarmonyOS Shared Package):动态共享库,运行时按需加载

这仨东西的关系,看下图就明白了:

<img src="https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/de/v3/W2xizBZhRAu-QDkYN2UnzQ/zh-cn_image_0000002462347089.png?HW-CC-KV=V1&HW-CC-Date=20260404T064520Z&HW-CC-Expire=86400&HW-CC-Sign=4686029E6DF0209AA5CB6C3090520DD0B85F83E4CB0AD0559DF251488D0395AA" title="null" crop="0,0,1,1" id="O9F8G" class="ne-image">

2.2 模块化设计的核心诉求

为啥要模块化?说白了就三个原因:

第一,多团队协作。大项目不可能让一个团队从头干到尾,得拆成多个模块,不同团队负责不同模块,各干各的,互不干扰。

第二,代码复用。有些功能是通用的,比如网络请求、日志打印、工具类,这些玩意儿写一遍就行了,别每个模块都抄一份。

第三,按需加载。不是所有功能用户都会用,有些低频功能可以做成按需加载,用户需要的时候再下载安装,省空间省流量。

2.3 UIAbility 组件设计影响模块化

这儿有个关键点很多人忽略了:你的应用是单任务单窗口,还是多任务多窗口?这直接影响模块化选型。

单任务单窗口:传统手机应用,一次只能干一件事。比如游戏应用,建议用单 HAP 承载 UIAbility。

多任务多窗口:大屏设备上,应用可以同时开多个窗口。比如:

  • 笔记应用:用户可以同时打开多页笔记,互相复制内容
  • 文档编辑:同时编辑多个文档,内容互相拖拽
  • 导航应用:导航后台运行,前台可以查其他位置
  • 视频应用:边看视频边浏览推荐列表

这种多窗口场景,每个窗口对应一个 UIAbility 实例,每个 UIAbility 可以单独显示一个窗口。这时候就得用多 HAP 了,每个 Feature 类型的 HAP 承载一个 UIAbility。


三、解决方案

3.1 共享模块咋搞

先说第一种场景:某个功能模块需要在多个应用之间共享。

比如你们公司有个网络库,十个 App 都要用。这时候咋办?总不能把代码抄十份吧?

正确做法是把这个网络库做成 HAR 包,发布到公司内部的 OHPM 仓。其他应用通过 ohpm install 安装,编译时自动打包进去。

这种能发布到 OHPM 仓的模块,只能是 HAR 包。HSP 不行,因为 HSP 跟应用的 bundleName 绑定,只能在一个应用内用,没法跨应用共享。

3.2 按需加载模块咋搞

第二种场景:某个功能用户不常用,想做成按需加载。

比如一个购物 App,有个"AR 试穿"功能,一个月没几个人用。这玩意儿要是打包在主包里,几百万用户下载时就得多花流量,多占空间。

按需加载有两个好处:

  • 用户首次安装包体积小,下载快
  • 安装后占用空间少,低频功能不占地方

实现按需加载,可以用 Feature 类型的 HAP 或者 HSP:

单 HAP 场景:如果应用只有一个 UIAbility,不需要 ExtensionAbility,优先用单 HAP(Entry 类型)。按需加载的模块用 HSP。

多 HAP 场景:如果要实现多任务多窗口,或者有 ExtensionAbility(比如卡片、分享),用多 HAP(一个 Entry + 多个 Feature)。每个 HAP 包含一个 UIAbility 或 ExtensionAbility。

3.3 多 HAP/HSP引用相同HAR包的坑

这儿有个大坑,我们踩过,你们得注意。

当多个 HAP 或 HSP 引用同一个 HAR 包时,HAR 里的单例会失效,影响启动性能。

看个例子:

HAR_COMMON 里有个 func 方法,执行一次要算很久:

// har_common/src/main/ets/utils/Utils.ets
const LARGE_NUMBER = 100000000;

function func(): number {
  let count = 0;
  while (count < LARGE_NUMBER) {
    count++;
  }
  return count;
}

export let funcResult = func();

问题在于,当 HAP 和 HSP 同时引用 HAR_COMMON 时,func 方法会执行两次!因为 HAR 是静态库,会被分别打包进 HAP 和 HSP,各有一份副本。

咋解决?把 HSP 改成 HAR:

官方做了个性能对比:

方案 阶段时长 (毫秒)
优化前(使用 HSP 包) 3125
优化后(使用 HAR 代替 HSP) 853.9

看到没?启动时间从 3 秒多降到 800 多毫秒,差了快 4 倍!


四、选型建议

4.1 单 HAP 工程咋选

如果你的应用是单窗口,只有一个 Entry 类型的 HAP,那选型就简单了。

不包含按需加载模块:全部用 HAR 就行。

](https://i-blog.csdnimg.cn/direct/c2fe88c108da41c6969c3efe439e7f59.png)

这样搞有三个好处:

  1. 全部编译进 HAP,没有额外的 HSP,节省安装和加载成本
  2. HAR 编译进 HAP 时,可以利用 ArkTS 的类型推断和编译优化
  3. 工程架构简单,后续演进灵活

包含按需加载模块:这时候得在 App Size 和启动性能之间做平衡。

App Size 优先:把公共依赖的模块封装在一个 HSP 模块壳里。

比如 hap_A 依赖于 har_A、har_C、har_D,hsp_B 依赖于 har_B、har_C、har_D。har_C 和 har_D 是公共的,为了节省空间,把它们封装到 common_hsp 里。

注意啊,这个 common_hsp 是个"模块壳",它本身没实际功能,就是为了把公共 HAR 包打包到一起,避免重复。

性能优先:直接依赖公共 HAR 包,不用 HSP 模块壳。

这样搞的话,har_C 和 har_D 会在 hap_A 和 hsp_B 里各有一份,App Size 会大一些,但启动性能更好,因为没有 HSP 的安装和加载损耗。

4.2 多 HAP 工程咋选

多 HAP 工程就是应用里有多个 HAP 包(一个 Entry + 多个 Feature)。这时候也得考虑有没有公共能力模块。

包含公共能力模块:同样得在 App Size 和启动性能之间做平衡。

性能优先:除了产品组件里的 HAP 包,其余全是 HAR 包。

这样搞的话,多个 HAP 之间会有相同的 HAR 包(比如 har_2、har_3、har_C 等)。App Size 会大一些,但如果 HAR 包本身不大,或者 App Size 不是瓶颈,可以用这个方案,减少动态加载的性能损耗。

App Size 优先:把公共的 HAR 包封装到 HSP 工程里。

比如有 3 个 HAP 包(1 个 entry 和 2 个 feature),把公共的 HAR 包封装到 common_wrap_hsp 和 feature_wrap_hsp 里。这两个 HSP 也是"模块壳",没实际功能,就是为了合理放置模块在编译产物中的位置。

这样搞的话,每个 HAR 包在 App 编译产物里只出现一次,App Size 最小。但模块壳数量别太多,否则影响安装速度和启动性能。

注意一个坑:在应用间共享的 HAR 包,原则上不允许依赖 HSP 包。因为 HSP 跟 bundleName 绑定,一旦 HAR 依赖了应用内 HSP,这个 HAR 就没法给其他应用共享了。

4.3 不包含公共能力模块

这种情况比较少,一般是一些小应用。可以参考单 HAP 的场景,全部用 HAR 就行。


五、避坑指南

坑 1:HSP 和 HAR 混用导致单例失效

前面说了,多 HAP/HSP 引用相同 HAR 包时,HAR 里的单例会失效。这个坑我们踩过,启动慢得离谱,后来用 Launch 模板一分析,才发现是这个问题。

咋避免?要么全用 HAR,要么把 HSP 改成 HAR。如果必须用 HSP,确保每个 HAR 只被一个模块引用。

坑 2:模块壳太多影响启动性能

模块壳是为了节省 App Size 搞出来的,但这玩意儿不能多用。每个 HSP 都要安装和加载,多了会影响启动性能。

建议:模块壳数量控制在 2-3 个以内,只封装真正公共的、体积大的 HAR 包。小的 HAR 包直接重复打包就行,影响不大。

坑 3:HAR 依赖 HSP 导致无法共享

这个坑更隐蔽。你搞了个 HAR 包想给多个应用用,结果这个 HAR 依赖了某个应用内的 HSP。编译没问题,但发布的时候发现,这个 HAR 没法给其他应用用了。

记住:要共享的 HAR 包,只能依赖其他 HAR 包,不能依赖 HSP。

坑 4:按需加载模块设计不合理

按需加载不是把随便哪个模块改成 HSP 就行。得考虑:

  • 这个功能用户是不是真的不常用
  • 按需加载后,用户体验会不会受影响
  • 模块之间的依赖关系清不清晰

建议:把真正低频的、独立的功能做成按需加载,比如 AR 试穿、高级滤镜、离线地图这种。核心功能别搞按需加载,用户一打开 App 就要用的东西,必须打包在主包里。

坑 5:多窗口应用用单 HAP

如果你的应用要支持多任务多窗口(比如大屏设备上的分屏操作),别用单 HAP。每个窗口对应一个 UIAbility,每个 UIAbility 应该用独立的 Feature 类型 HAP 承载。

不然的话,用户开个分屏,两个窗口互相干扰,体验差得一批。


六、总结一下

模块化设计这事儿,说复杂也复杂,说简单也简单。核心就三点:

第一,搞清楚你的应用是啥形态。单窗口还是多窗口?要不要按需加载?有没有跨应用共享的需求?这决定了你们用 HAP、HAR、HSP 的哪个组合。

第二,在 App Size 和启动性能之间做平衡。想要包小,就用 HSP 模块壳;想要启动快,就全用 HAR。没有绝对的对错,看你的应用更在意哪个。

第三,避开那些坑。单例失效、模块壳太多、HAR 依赖 HSP、按需加载设计不合理、多窗口用单 HAP,这些坑我们都给你们标出来了,别踩。

最后送大家一句话:模块化不是一蹴而就的,得随着业务发展不断演进。一开始别搞太复杂,等真遇到瓶颈了再优化也不迟。

好了,今天叨了这么多,希望能帮你们把模块化这事儿整明白。有啥问题,评论区见。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐