📚 鸿蒙专题与实战专栏 · 第 25 篇 / 共 30 篇(专题篇 21-30)
上一篇:音视频开发 | 下一篇:持续集成:自动化构建与流水线

目录

一、三种模块一句话区分

二、创建 HAR 并引用

三、HAR 与 HSP 的关键差别

四、feature HAP 按需加载(减小首包)

五、拆分时机建议

六、下一篇预告


单模块项目超过 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,一步一步来,别一次性重构。

六、下一篇预告

模块拆分完成后,如何保证每次提交都不破坏构建?下一篇:持续集成流水线。


专题篇持续更新,点赞 + 收藏 + 关注不迷路。
想深入哪个方向,评论区留言,点赞高的先写。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐