鸿蒙开发实战:5分钟搞定HAR与HSP共享包的创建与引用(附避坑指南)
鸿蒙跨模块交互实战:从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: 一个自定义按钮组件。
要让这些类能被其他模块使用,关键有两步:
- 使用
export关键字导出:在类、方法或变量的声明前加上export。 - 在入口文件
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。
-
添加依赖:打开
Entry模块下的oh-package.json5文件,在dependencies字段中添加对CommonUtils的依赖。路径以file:开头,指向HAR模块的根目录。// 文件:Entry/oh-package.json5 { "dependencies": { "CommonUtils": "file:../CommonUtils" } } -
同步依赖:点击编辑器右上角的
Sync Now按钮,或是在终端中进入项目根目录执行ohpm install。这一步会解析依赖关系,并将HAR模块链接到当前HAP。 -
在代码中使用:同步成功后,就可以像使用本地模块一样使用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 中显式声明依赖关系。
- 在
oh-package.json5中添加依赖(同HAR):// 文件:Entry/oh-package.json5 { "dependencies": { "NetworkLibrary": "file:../NetworkLibrary" } } - 在
module.json5中声明依赖(HSP特有步骤):// 文件:Entry/src/main/module.json5 { "module": { // ... 其他配置 "dependencies": [ { "bundleName": "com.yourcompany.networklibrary", // 与HSP中定义的name一致 "moduleName": "NetworkLibrary" // HSP的模块目录名 } ] } } - 配置多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的配置完全匹配(大小写敏感)。
- 检查1:
-
坑点三:运行时崩溃,提示找不到HSP
- 现象:应用安装成功,但一点击跳转或使用HSP功能就崩溃。
- 检查:运行配置中
Deploy Multi Hap是否勾选,并且是否包含了所有相关的HAP和HSP模块。漏掉任何一个,该模块就不会被安装到设备上。
-
坑点四:HAR中的资源(如图片、字符串)引用失败
- 注意:在HAR中,引用资源应使用相对路径,并且确保资源文件被正确放置在
src/main/resources目录下。在其他模块中引用该资源时,需要通过HAR模块名进行访问,例如$r('app.media.icon')需要改为$r('CommonUtils.media.icon')(假设HAR模块名为CommonUtils)。
- 注意:在HAR中,引用资源应使用相对路径,并且确保资源文件被正确放置在
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以优化包体积。
更多推荐
所有评论(0)