鸿蒙跨模块交互实战:从HAR到HSP的深度构建与高效引用

在鸿蒙应用开发的中后期,随着功能复杂度的提升,代码复用和模块解耦的需求会变得异常迫切。你是否遇到过这样的场景:几个不同的功能模块都需要用到同一套网络请求工具、同一组自定义UI组件,或者同一套数据模型?如果将这些代码在每个模块里都复制粘贴一遍,不仅维护成本会指数级上升,一旦底层逻辑需要调整,那将是一场灾难。这时,共享包(Harmony Archive 和 Harmony Shared Package)就成了你的“救命稻草”。

但现实往往是,文档看懂了,动手一操作就掉坑里。HAR和HSP到底该选哪个?明明按照步骤配置了,编译却报错“模块找不到”?共享包里的页面怎么跳转?这些问题不解决,共享包的优势就无从谈起。本文将从一线开发者的实战视角出发,抛开那些官方的概念复述,直接带你手把手创建、引用、调试HAR和HSP,并重点剖析那些官方文档里可能一笔带过、但实际开发中一定会踩到的“坑”。我们的目标很明确:让你在5分钟内理解核心差异,在30分钟内完成一次成功的跨模块交互实践。

1. 理解鸿蒙模块体系的基石:HAP、HAR与HSP

在动手之前,我们必须先厘清鸿蒙项目中的几种核心模块类型,这是后续所有操作的基础。很多开发者混淆概念,往往是因为没从编译和运行机制的根本上去理解它们。

一个典型的鸿蒙应用工程,其模块结构可以这样划分:

  • HAP (Harmony Ability Package):这是应用的部署包,最终会被安装到设备上。它又细分为两种:
    • Entry HAP:应用的入口模块,一个应用有且仅有一个。它包含了应用的启动图标、名称等全局配置。
    • Feature HAP:功能模块,其内部结构与Entry HAP完全一致,可以独立承载UIAbility和页面。你可以把它想象成一个可插拔的功能插件,用于实现应用的动态特性分发。
  • 共享包 (Shared Package):这类模块不能独立运行,也无法包含UIAbility。它们存在的唯一目的,就是被其他HAP模块引用,提供公共代码和资源。共享包分为两种,它们的区别是本文的核心:
特性维度HAR (Harmony Archive)HSP (Harmony Shared Package)
本质静态共享包动态共享包
编译方式跟随引用它的HAP模块一起编译。可以独立编译,生成独立的 .hsp 文件。
产物存在形式在编译时,其代码和资源会被复制到每个引用它的HAP包中。在编译时,会生成一个独立的共享包文件。所有依赖它的HAP在运行时共享同一份该文件。
包体积影响会导致最终应用包体积增大(因为代码被多次复制)。能有效减少应用包体积(代码只有一份)。
更新灵活性更新HAR需要重新编译并发布所有引用它的HAP。理论上可以独立更新HSP(需遵循鸿蒙的共享包管理规范)。
适用场景工具类库、小型UI组件、稳定的基础模型。大型UI组件库、频繁更新的业务逻辑库、多个应用间需要共享的公共库。

注意:一个常见的误解是认为HAR不能包含UI组件。实际上,HAR和HSP都可以包含ArkUI组件(即.ets文件)。它们不能包含的是UIAbility、ServiceAbility等“Ability”组件,这些是应用的“页面入口”或“后台服务”,必须由HAP承载。

如何快速区分你当前正在操作的模块类型?最准确的方法是查看模块目录下的 src/main/module.json5 配置文件,找到其中的 type 字段。这是模块身份的“身份证”。

// 对于一个Entry或Feature HAP,其type值为:
"type": "entry" // 或 "feature"

// 对于一个HAR,其type值为:
"type": "har"

// 对于一个HSP,其type值为:
"type": "hsp"

理解了这些,你就知道为什么有时候配置看起来没错,但编译行为却和预期不符了。接下来,我们进入实战环节。

2. 实战第一步:创建与配置一个HAR共享包

让我们从最常用的HAR开始。假设我们需要创建一个名为 CommonUtils 的共享包,里面包含一个日志工具类和一个自定义的按钮组件。

2.1 创建HAR模块

在DevEco Studio中,右键点击项目根目录 -> New -> Module。在弹出的窗口中,选择 Static Library 模板,并命名为 CommonUtils。创建完成后,你会发现工程中多了一个 CommonUtils 目录,其 module.json5 中的 type 自动被设置为 "har"。

2.2 编写可共享的代码

在 CommonUtils 模块中,我们创建两个文件:

  • src/main/ets/utils/Logger.ets: 一个简单的日志工具类。
  • src/main/ets/components/CustomButton.ets: 一个自定义按钮组件。

要让这些类能被其他模块使用,关键有两步:

  1. 使用 export 关键字导出:在类、方法或变量的声明前加上 export。
  2. 在入口文件 index.ets 中声明:HAR模块根目录下的 index.ets 文件是其对外的“总出口”,所有需要暴露的接口都必须在这里统一导出。
// 文件:CommonUtils/src/main/ets/utils/Logger.ets
export class Logger {
  static debug(tag: string, message: string): void {
    console.debug(`[${tag}] ${message}`);
  }
  static error(tag: string, message: string): void {
    console.error(`[${tag}] ${message}`);
  }
}

// 文件:CommonUtils/src/main/ets/components/CustomButton.ets
@Component
export struct CustomButton {
  // ... 组件内部实现
}

// 文件:CommonUtils/index.ets
// 这是关键!将所有要共享的内容从这里导出
export { Logger } from './src/main/ets/utils/Logger'
export { CustomButton } from './src/main/ets/components/CustomButton'

2.3 在HAP中引用HAR

现在,我们想在主入口模块 Entry 中使用这个 CommonUtils HAR。

  1. 添加依赖:打开 Entry 模块下的 oh-package.json5 文件,在 dependencies 字段中添加对 CommonUtils 的依赖。路径以 file: 开头,指向HAR模块的根目录。

    // 文件:Entry/oh-package.json5
    {
      "dependencies": {
        "CommonUtils": "file:../CommonUtils"
      }
    }
    
  2. 同步依赖:点击编辑器右上角的 Sync Now 按钮,或是在终端中进入项目根目录执行 ohpm install。这一步会解析依赖关系,并将HAR模块链接到当前HAP。

  3. 在代码中使用:同步成功后,就可以像使用本地模块一样使用HAR中的内容了。

    // 文件:Entry/src/main/ets/pages/Index.ets
    import { Logger, CustomButton } from 'CommonUtils'
    
    @Entry
    @Component
    struct Index {
      build() {
        Column() {
          // 使用共享包中的自定义组件
          CustomButton()
        }
        .onClick(() => {
          // 使用共享包中的工具类
          Logger.debug('IndexPage', 'Button clicked!')
        })
      }
    }
    

提示:如果你在代码中导入 CommonUtils 时出现红色波浪线提示找不到模块,但 oh-package.json5 配置无误,可以尝试执行 File -> Invalidate Caches and Restart... 来清理IDE缓存,这常常能解决这类索引问题。

3. 进阶之选:HSP动态共享包的创建与部署

当你的共享代码体积较大,或者被多个Feature HAP频繁引用时,HSP的优势就显现出来了。创建HSP的步骤与HAR类似,但有几个关键配置点不同。

3.1 创建HSP模块

在 New Module 时,选择 Shared Library 模板,命名为 NetworkLibrary。创建后,其 module.json5 中的 type 为 "hsp"。

3.2 HSP的特殊配置

HSP需要在其 module.json5 中声明一个唯一的 name,这个 name 将在跨HAP引用时作为标识。同时,引用HSP的HAP也需要进行额外配置。

// 文件:NetworkLibrary/src/main/module.json5
{
  "module": {
    "name": "com.yourcompany.networklibrary", // HSP的唯一标识名
    "type": "hsp",
    // ... 其他配置
  }
}

3.3 在HAP中引用HSP

引用HSP的步骤与HAR类似,但需要在HAP的 module.json5 中显式声明依赖关系。

  1. 在 oh-package.json5 中添加依赖(同HAR):
    // 文件:Entry/oh-package.json5
    {
      "dependencies": {
        "NetworkLibrary": "file:../NetworkLibrary"
      }
    }
    
  2. 在 module.json5 中声明依赖(HSP特有步骤):
    // 文件:Entry/src/main/module.json5
    {
      "module": {
        // ... 其他配置
        "dependencies": [
          {
            "bundleName": "com.yourcompany.networklibrary", // 与HSP中定义的name一致
            "moduleName": "NetworkLibrary" // HSP的模块目录名
          }
        ]
      }
    }
    
  3. 配置多HAP编译:由于HSP是独立编译的,要调试一个引用了HSP的HAP,必须开启多HAP部署。在DevEco Studio顶部菜单栏,点击 Run -> Edit Configurations...,在运行配置中勾选 Deploy Multi Hap,并确保你的Entry和它所依赖的HSP模块都被选中。

3.4 编译与运行

完成以上配置后,点击运行。DevEco Studio会先编译HSP,再编译依赖它的HAP,最后将多个HAP包一起安装到设备或模拟器上。你可以在Log窗口中看到类似 [HSP] Installing HSP: com.yourcompany.networklibrary 的日志,这表明HSP正在被独立安装和加载。

4. 核心难题破解:共享包中的页面路由与常见“坑点”

共享包不能有UIAbility,但可以包含普通的ArkUI页面(@Component)。如何从其他模块跳转到共享包里的页面,是另一个高频问题。

4.1 标准路由跳转方式

鸿蒙的路由机制 router 支持通过一个特定的URL格式来跳转到任意模块的页面,前提是你知道目标的 bundleName、moduleName 和页面路径。

// 假设在 NetworkLibrary HSP 中有一个页面 src/main/ets/pages/DetailPage.ets
// 它被导出在 NetworkLibrary/index.ets 中:export { DetailPage } from './src/main/ets/pages/DetailPage'

// 在 Entry HAP 中跳转到该页面
import router from '@ohos.router';

// 跳转URL格式:'@bundle:包名(bundleName)/模块名(moduleName)/路径/页面名(不含.ets后缀)'
let targetUrl = '@bundle:com.yourcompany.yourapp/NetworkLibrary/pages/DetailPage';
router.pushUrl({ url: targetUrl });

这里有几个参数需要你准确获取:

  • bundleName:应用的包名,在 AppScope/app.json5 文件的 bundleName 字段下。
  • moduleName:不是你在oh-package.json5里写的依赖别名,而是目标模块在文件系统中的目录名(例如 NetworkLibrary)。
  • 路径和页面名:相对于该模块 ets 目录的路径。

4.2 避坑指南:高频问题与解决方案

在实际操作中,你几乎一定会遇到下面这几个问题:

  • 坑点一:Previewer(预览器)无法显示共享包内容

    • 现象:代码没有报错,但预览器一片空白,或者提示组件未定义。
    • 原因:预览器目前对跨模块,尤其是HSP的支持有限。
    • 解决方案:必须使用模拟器或真机进行 Build 和 Run 才能看到正确效果。不要依赖预览器调试跨模块功能。
  • 坑点二:编译失败,提示“Module not found”或“Cannot resolve dependency”

    • 检查1:oh-package.json5 中的文件路径是否正确。file:../ 后面的路径是相对于当前 oh-package.json5 文件所在目录的。
    • 检查2:执行 ohpm install 后,查看项目根目录下的 oh_modules 文件夹里,是否生成了对应共享包的软链接。
    • 检查3:对于HSP,务必检查HAP的 module.json5 中的 dependencies 配置,bundleName 和 moduleName 是否与HSP的配置完全匹配(大小写敏感)。
  • 坑点三:运行时崩溃,提示找不到HSP

    • 现象:应用安装成功,但一点击跳转或使用HSP功能就崩溃。
    • 检查:运行配置中 Deploy Multi Hap 是否勾选,并且是否包含了所有相关的HAP和HSP模块。漏掉任何一个,该模块就不会被安装到设备上。
  • 坑点四:HAR中的资源(如图片、字符串)引用失败

    • 注意:在HAR中,引用资源应使用相对路径,并且确保资源文件被正确放置在 src/main/resources 目录下。在其他模块中引用该资源时,需要通过HAR模块名进行访问,例如 $r('app.media.icon') 需要改为 $r('CommonUtils.media.icon')(假设HAR模块名为CommonUtils)。

4.3 一种备选的迂回方案

如果标准的路由跳转因为某些复杂配置问题始终无法成功,这里有一个虽然不够优雅但能救急的“土办法”:在共享包中,将目标页面 export 出来。然后,在需要跳转的HAP模块中,创建一个极其简单的、空的中间页面(ModuleIndex),这个页面的唯一作用就是导入并展示共享包里的目标页面。

// 1. 在共享包 NetworkLibrary 中,确保 DetailPage 被导出。
// 2. 在 Entry HAP 中创建一个新页面 ModuleIndex.ets
import { DetailPage } from 'NetworkLibrary';

@Entry
@Component
struct ModuleIndex {
  build() {
    Column() {
      DetailPage() // 直接使用共享包的页面组件
    }
  }
}
// 3. 在Entry中,用普通的路由跳转到 ModuleIndex 页面。
// router.pushUrl({ url: 'pages/ModuleIndex' });

这样,打开 ModuleIndex 就相当于打开了 DetailPage。这个方法绕开了复杂的跨模块路由URL,但增加了中间层,仅建议在调试或临时解决问题时使用。

跨模块交互是构建大型、可维护鸿蒙应用的必备技能。从HAR到HSP的选择,本质上是在包体积、编译速度和更新灵活性之间做权衡。对于团队内的公共工具库,稳定的用HAR,常变的、体积大的考虑HSP。最关键的是,理解其背后的机制——HAR是复制,HSP是共享——这能帮你从根本上理解和解决大部分配置问题。多动手构建几个Demo,亲自踩一遍上面提到的坑,比读十篇教程都管用。在实际项目里,我通常会先以HAR形式快速实现共享,当模块稳定且被广泛引用后,再评估是否要重构为HSP以优化包体积。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐