【鸿蒙专题与实战】多模块工程:HAP、HAR、HSP 与按需加载
·
📚 鸿蒙专题与实战专栏 · 第 25 篇 / 共 30 篇(专题篇 21-30)
上一篇:音视频开发 | 下一篇:持续集成:自动化构建与流水线
目录
单模块项目超过 5 万行就该考虑拆分了。鸿蒙提供了三种模块形态,理解它们的差异是做大项目的前提。

一、三种模块一句话区分
| 类型 | 能否独立安装 | 编译方式 | 放什么 |
|---|---|---|---|
| HAP | 能(entry 必备) | 独立打包 | 应用入口、页面 |
| HAR | 不能 | 编译进依赖方 | 工具类、UI 组件、常量 |
| HSP | 不能 | 运行时共享一份 | 公共业务、大资源 |
二、创建 HAR 并引用
先看创建与引用的完整流程,再逐个文件落地代码:
# 1. 新建:右键项目 → New → Module → Static Library
# 生成 library/ 目录,含 Index.ets 作为统一出口
# 2. 在 HAR 的 Index.ets 导出
export { HttpUtil } from './src/main/ets/utils/HttpUtil'
export { UserCard } from './src/main/ets/components/UserCard'
# 3. 在 entry 的 oh-package.json5 声明依赖
{
"dependencies": {
"@ohos/library": "file:../library"
}
}
# 4. 使用
import { HttpUtil, UserCard } from '@ohos/library'
2.1 HttpUtil.ets:请求封装与错误处理
把网络请求收敛到一个工具类里,统一处理超时、状态码和异常,业务侧只关心返回数据:
// library/src/main/ets/utils/HttpUtil.ets
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 统一响应结构,业务侧按 code 判断成功与否
export interface ApiResult<T> {
code: number;
message: string;
data: T;
}
export class HttpUtil {
private static readonly TIMEOUT: number = 10000; // 请求超时时间(毫秒)
/**
发起 GET 请求
@param url 完整请求地址
@returns 解析后的业务数据
*/
static async get<T>(url: string): Promise<T> {
const request = http.createHttp();
try {
// 关键行:设置超时,避免弱网下长时间挂起
const resp = await request.request(url, {
method: http.RequestMethod.GET,
connectTimeout: HttpUtil.TIMEOUT,
readTimeout: HttpUtil.TIMEOUT,
header: { 'Content-Type': 'application/json' }
});
// 关键行:非 200 直接抛错,交给上层统一处理
if (resp.responseCode !== http.ResponseCode.OK) {
throw new Error(HTTP ${resp.responseCode});
}
const result = JSON.parse(resp.result as string) as ApiResult<T>;
if (result.code !== 0) {
throw new Error(result.message || '业务异常');
}
return result.data;
} catch (err) {
// 关键行:把系统错误和业务错误统一成可读信息
const e = err as BusinessError;
console.error([HttpUtil] request failed: ${e.code} ${e.message});
throw new Error(e.message ?? '网络请求失败');
} finally {
// 关键行:无论成功失败都要销毁请求对象,防止内存泄漏
request.destroy();
}
}
}
2.2 UserCard.ets:可复用的 ArkTS 组件
组件只依赖入参,不直接发请求,保证在任意页面都能复用:
// library/src/main/ets/components/UserCard.ets
@Component
export struct UserCard {
// 关键行:@Prop 接收父组件传入的数据,单向同步
@Prop userName: string = '';
@Prop avatarUrl: string = '';
@Prop desc: string = '';
build() {
Row({ space: 12 }) {
Image(this.avatarUrl)
.width(48)
.height(48)
.borderRadius(24)
.objectFit(ImageFit.Cover)
.alt($r('app.media.icon')) // 关键行:加载失败时的占位图
Column({ space: 4 }) {
Text(this.userName)
.fontSize(16)
.fontWeight(FontWeight.Medium)
Text(this.desc)
.fontSize(12)
.fontColor('#999999')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
}
}
2.3 entry 中调用两个导出
在 entry 页面里同时使用工具类和组件,验证 HAR 引用是否生效:
// entry/src/main/ets/pages/Index.ets
// 关键行:从 HAR 的统一出口导入,路径与 HAR 内部结构解耦
import { HttpUtil, UserCard } from '@ohos/library';
interface UserInfo {
name: string;
avatar: string;
desc: string;
}
@Entry
@Component
struct Index {
@State user: UserInfo = { name: '', avatar: '', desc: '' };
@State loading: boolean = false;
aboutToAppear(): void {
this.loadUser();
}
async loadUser(): Promise<void> {
this.loading = true;
try {
// 关键行:直接调用 HAR 中的静态方法,异常已在内部统一封装
const data = await HttpUtil.get<UserInfo>('https://api.example.com/user/1');
this.user = data;
} catch (err) {
console.error(load user failed: ${(err as Error).message});
} finally {
this.loading = false;
}
}
build() {
Column({ space: 16 }) {
if (this.loading) {
LoadingProgress().width(40).height(40)
} else {
// 关键行:像普通组件一样使用 HAR 导出的自定义组件
UserCard({
userName: this.user.name,
avatarUrl: this.user.avatar,
desc: this.user.desc
})
}
}
.width('100%')
.padding(16)
}
}
📌 注意:HAR 内的资源(如图片、字符串)需通过
$r('app.media.xxx') 引用,且资源名不能与宿主工程冲突;跨 HAR 引用时建议统一加前缀。
三、HAR 与 HSP 的关键差别
假设 entry 和 feature1 都依赖同一个公共库:
用 HAR:库代码被复制两份,包体翻倍
用 HSP:运行时只加载一份,多包共享
# 创建 HSP:New → Module → Shared Library
# 依赖方式与 HAR 相同,但需在 build-profile.json5 中确认
# 该 HSP 会在运行时被动态加载
⚠️ HSP 并非万能:它的加载有额外开销,且必须保证所有依赖方版本一致。小工具库用 HAR 更省心。
四、feature HAP 按需加载(减小首包)
把「不常用但体积大」的功能(如视频编辑、离线地图)做成独立 HAP,用户首次使用时才下载:
import { bundleManager } from '@kit.AbilityKit'
// 按需触发下载安装
const installer = bundleManager.getBundleInstaller()
// 具体 API 随版本演进,核心是「检测未安装 → 引导下载 → 安装后跳转」
// 详见官方「按需分发」文档,配置在 module.json5 的 distributionFilter
📌 按需分发需要在 AGC 配置分发规则,且审核对「拆包合理性」有要求,不能把核心功能拆出去变相绕过首包限制。
五、拆分时机建议
别为了架构而架构。当出现「同一段代码被 2 个以上模块复制」或「单个模块编译超过 60 秒」时,就是该拆分的信号。
小结
把前面的内容压缩成一张选型清单,落地时对照着看即可:
- 入口与页面放 HAP:entry 必备,能独立安装;不常用的大功能拆成 feature HAP,按需分发减小首包。
- 工具类、UI 组件、常量放 HAR:编译进依赖方,引用简单、无运行时开销,适合小体量公共代码。
- 多包共享的公共业务与大资源放 HSP:运行时只加载一份,避免 HAR 被复制多份导致包体翻倍。
- HSP 不是万能解:有额外加载开销,且要求所有依赖方版本一致,小工具库用 HAR 更省心。
- 拆分看信号而非看心情:同一段代码被 2 个以上模块复制,或单个模块编译超过 60 秒,就该动手拆了。
建议先按这张清单给现有工程做一次「模块体检」,把重复代码收敛成 HAR,再评估是否需要 HSP 或 feature HAP,一步一步来,别一次性重构。
六、下一篇预告
模块拆分完成后,如何保证每次提交都不破坏构建?下一篇:持续集成流水线。
专题篇持续更新,点赞 + 收藏 + 关注不迷路。
想深入哪个方向,评论区留言,点赞高的先写。
更多推荐



所有评论(0)