鸿蒙组件化设计横空出世

基于 hmsmoduledemo 示例工程(登录 / 注册 / 修改密码 / 游戏强更 / 支付), 讲清「包类型隔离 + 接口契约 + 单向依赖」的组件化落地方式。

效果图

一、为什么要组件化

单体工程里所有代码揉在一个模块,随着功能变多会出现:

  • 代码耦合,改支付可能碰坏登录;
  • 编译慢,任何一行改动全量重编;
  • 无法多人并行开发,冲突不断。

组件化的核心思路:按业务边界拆包,模块之间只通过"契约"通信,谁也不认识谁的实现

本工程的三条铁律:

  1. 包类型隔离 ------ 不同职责用不同包类型(HAP / HAR / HSP);
  2. 接口契约 ------ 跨模块调用只依赖 common_api 里的 interface,不依赖实现类;
  3. 单向依赖 ------ 上层可依赖下层,下层绝不依赖上层,业务模块之间互不 import。

二、三种包类型:HAP / HAR / HSP

包类型 全称 特点 本工程中的角色
HAP HarmonyOS Ability Package 应用安装/运行单元,可含 Ability,最终打进 app 包 entry:壳工程
HAR HarmonyOS Archive 静态共享包,每个引用它的 HAP 各自拷贝一份,代码随包复制 common / common_api / user_module / pay_module / update_module
HSP HarmonyOS Shared Package 动态共享包,同设备同应用内只存在一份,运行时共享 common_service:全局登录态

关键区别记忆点:

  • 多个模块都要用、但要求全局只有一份状态 (如登录态、配置中心)→ 必须放 HSP,否则每个 HAP 里的 HAR 副本各有自己的静态变量,状态会"分裂"。
  • 纯工具、纯契约(无状态)→ 放 HAR 即可。
  • 包类型在各自 src/main/module.json5 里声明:"type": "entry" / "har" / "shared"

三、整体架构:单向依赖分层

scss 复制代码
entry (Entry HAP)                       ← 壳工程:Navigation 组装 + 模块初始化
├── user_module   (HAR)  登录 / 注册 / 修改密码
├── pay_module    (HAR)  支付中心(商品 → 下单 → 支付)
├── update_module (HAR)  强更(版本检查 + 强制更新弹窗)
├── common_service(HSP)  SessionManager:登录态 AppStorage 封装(全局单例)
├── common_api    (HAR)  契约层:IUserService / IPayService / IUpdateService
│                        ServiceRegistry / RouteCenter / 模型 / 事件常量
└── common        (HAR)  基础能力:Logger / Validators / MockHttp
  • entry 只负责两件事:初始化各模块EntryAbility.onCreate)和 组装 Navigation 路由壳pages/Index)。
  • 业务模块(user / pay / update)之间没有任何相互 import ;它们只向下依赖 common_api(契约)、common_service(状态)、common(工具)。
  • 想调用别人的功能?不 import 实现类,而是问 ServiceRegistry 要接口。

依赖关系体现在各模块 oh-package.json5file: 本地依赖里,例如 entry/oh-package.json5

json5 复制代码
"dependencies": {
  "@demo/common": "file:../common",
  "@demo/common_api": "file:../common_api",
  "@demo/common_service": "file:../common_service",
  "@demo/user_module": "file:../user_module",
  "@demo/update_module": "file:../update_module",
  "@demo/pay_module": "file:../pay_module"
}

四、四种模块间通信机制(重点)

ArkTS 不支持反射(没有 Class.forName),所以"接口找实现"必须显式完成。本工程给出四种互补机制:

4.1 请求-响应:ServiceRegistry(接口注册表)

位置:common_api/src/main/ets/registry/ServiceRegistry.ets

ets 复制代码
export class ServiceRegistry {
  private static services: Map<string, Object> = new Map<string, Object>();

  static register<T extends Object>(key: string, impl: T): void { ... }
  static get<T extends Object>(key: string): T | undefined { ... }
}

工作流程(以支付为例):

  1. 契约定义在 common_apiIPayService 接口声明 queryProducts / createOrder / pay
  2. 实现方注册PayModule.init()ServiceRegistry.register<IPayService>(ServiceKeys.PAY, new PayServiceImpl())
  3. 使用方获取 :任意页面 ServiceRegistry.get<IPayService>(ServiceKeys.PAY)?.createOrder(...)

要点:

  • 使用方拿到的类型是接口 ,编译期完全不知道 PayServiceImpl 的存在 ------ 这就是"面向接口编程";
  • key 统一收敛在 ServiceKeys 常量类,防止字符串散落;
  • 适合"问一句、答一句"的调用(登录、下单、查版本)。

位置:common_api/src/main/ets/registry/RouteCenter.ets

鸿蒙页面路由是 Navigation + NavDestination 体系,但路由表集中在 entry,业务页面却在各个 HAR 里,entry 不该直接 import 业务页面(否则壳和业务又耦合了)。解法:

  1. 各业务模块用 @Builder 声明页面构建函数,模块初始化时通过全局函数 wrapBuilder 包成 WrappedBuilder 注册进 RouteCenter:
ets 复制代码
// user_module/UserModule.ets
@Builder
function LoginPageBuilder(params: RouteParams): void {
  LoginView({ params });
}

RouteCenter.register({ name: RouteNames.LOGIN, builder: wrapBuilder(LoginPageBuilder) });
  1. entrynavDestination 统一从 RouteCenter 按名字取 builder:
ets 复制代码
// entry/pages/Index.ets
@Builder
pageMap(name: string, param: ESObject): void {
  NavDestination() {
    if (RouteCenter.has(name)) {
      RouteCenter.get(name)?.builder.builder(param as RouteParams)
    } else {
      Text(`页面未注册:${name}`)
    }
  }
  ...
}
  1. 跳转变成纯数据驱动:RouteCenter.push(pathStack, RouteNames.PAY_CENTER)

要点:

  • 路由名集中在 RouteNames 常量类;
  • 路由参数统一封装为 RouteParams(含 pathStackargs),页面间传参不依赖具体类型;
  • WrappedBuilder / wrapBuilder / NavPathStack全局符号 ,不需要也不能从 @kit.ArkUI import(本工程踩过这个坑)。

4.3 全局状态:AppStorage + HSP 单例

位置:common_service/src/main/ets/session/SessionManager.ets

ets 复制代码
export class SessionManager {
  static onLogin(user: UserInfo): void {
    AppStorage.setOrCreate('isLogin', true);
    AppStorage.setOrCreate('KEY_USER_INFO', user);
    ...
  }
}
  • 写入方(UserServiceImpl.login 成功后)调 SessionManager.onLogin()
  • 读取方(首页、支付页)用 @StorageLink('isLogin') 响应式绑定,登录态一变 UI 自动刷新,不需要手动发通知:
ets 复制代码
@StorageLink('isLogin') isLogin: boolean = false;
  • 为什么放 HSP:AppStorage 是全应用一份,但 SessionManager 这个类如果放 HAR,会被每个 HAP/HSP 各自打包一份,静态方法和常量虽然不冲突,但类的"身份"会分裂 (A 模块存的 UserInfo 类型和 B 模块读的不是同一个类)。放 HSP 保证全应用只有一份实现。

4.4 通知型事件:emitter 事件总线

位置:common_api/src/main/ets/constants/Events.ets

ets 复制代码
// 发送(UserServiceImpl 登录成功后)
emitter.emit({ eventId: EVENT_USER_LOGIN }, { data: { userId } });

使用原则(工程注释里写明的纪律):

  • 事件只用于"通知型"通信(我登录成功了,谁关心谁听),不期待返回值;
  • 请求-响应一律走 ServiceRegistry,不要用 emitter 模拟调用(否则时序和错误处理会失控);
  • eventId 是 number,集中定义常量:EVENT_USER_LOGIN = 1001EVENT_PAY_SUCCESS = 2001EVENT_FORCE_UPDATE = 3001 等。

四种机制怎么选

需求 用哪个
调别人的功能拿结果 ServiceRegistry
跳页面 RouteCenter
多个 UI 共享一个状态且要自动刷新 AppStorage(HSP 封装)
做完一件事广播一声 emitter

五、关键业务流程走读

5.1 启动初始化

EntryAbility.onCreateentry/src/main/ets/entryability/EntryAbility.ets):

ets 复制代码
UserModule.init();
UpdateModule.init();
PayModule.init();

每个 init() 做两件事:向 ServiceRegistry 注册服务实现、向 RouteCenter 注册页面 builder。显式注册替代反射,编译期可追踪,哪个模块没初始化一跑就知道(服务 get 到 undefined、页面显示"未注册")。

5.2 登录链路

LoginViewIUserService.login()UserServiceImplMockHttp 校验账号 → 成功后:

  1. SessionManager.onLogin(user) 写 AppStorage;
  2. emitter.emit(EVENT_USER_LOGIN) 广播;
  3. 首页 @StorageLink('isLogin') 自动刷新出"你好,测试玩家"。

测试账号:13800000000 / abc123

5.3 强更链路

首页 aboutToAppearbundleManager.getBundleInfoForSelf 读本机 versionCode(AppScope 配置 100)→ IUpdateService.checkUpdate() 对比 Mock 服务端 minSupportCode=150100 < 150 触发强制弹窗 ForceUpdateDialogautoCancel: false,只能"立即更新"或"退出游戏")。

注意强更弹窗不走路由 :它是组件不是页面,由 update_module 直接导出给 entry 用 ------ 组件级复用可以直接导出,页面级跳转才需要 RouteCenter。

5.4 支付链路

PayCenterView 加载商品列表 → 选渠道(华为/微信/支付宝占位)→ createOrderpay

  • 页面层先查 @StorageLink('isLogin'),未登录跳登录页(第一道闸);
  • 服务层 createOrder 再查 SessionManager.isLogin(),未登录返回 401(第二道闸,防止绕过 UI 直接调接口)------ 状态校验要在服务层兜底
  • 支付成功 emitter.emit(EVENT_PAY_SUCCESS)

六、Mock 网络层的设计意图

common/src/main/ets/network/MockHttp.etssetTimeout(600ms) + 内存数据模拟网络:

ets 复制代码
static request(api: string, params: ESObject, handler: (p: ESObject) => MockResponse): Promise<MockResponse>

所有业务代码都写成"异步请求 → 拿响应 → 处理 code"的形式。真实项目只需把 MockHttp 的实现换成 @kit.NetworkKithttp.createHttp().request(),业务模块一行不用改 ------ 这就是契约/分层带来的替换自由度。


七、踩坑记录(本工程实际修过的错)

这些错误在组件化鸿蒙工程里非常典型,值得记住:

  1. HAR 模块缺 src/main/module.json5 → 构建报 module.json5 file not found。每个模块都必须有,typehar / shared / entry
  2. AppScope/app.json5icon / label → schema 校验失败。且 icon 资源必须放在 AppScope/resources/ 下,不能引用 entry 的资源。
  3. main_pages.json 的键名 :API 12 要求 "src": [...],不是 "pages": [...]
  4. oh-package.json5 不要写 @ohos/hvigor 依赖 :hvigor 插件由 hvigor/hvigor-config.json5 管理,写进 oh-package 会让 ohpm install 去 ohpm 仓库拉取报 404。
  5. 0xDEMO 不是合法十六进制(M 不是 hex 字符)→ 编译报奇怪的二元表达式错误。
  6. WrappedBuilder / NavPathStack / wrapBuilder 是全局符号 ,从 @kit.ArkUI import 反而报 "has no exported member"。
  7. ArkTS 不允许对象字面量当类型{ key: PayChannel; label: string }[] 要抽成 interface
  8. UI 构建作用域里不能写普通语句@Builder / build() 里不允许 const route = ... 这类声明,要改成 if/else + 组件调用。
  9. 系统色资源名要和 SDK 对齐 :如 sys.color.ohos_id_color_sub_emphasize 在当前 SDK 不存在,可查 SDK 目录 toolchains/id_defined.json 确认可用名。
  10. 修复顺序:先修包结构/资源(PreBuild),再装依赖(ohpm install),最后才是 ArkTS 编译错误 ------ 前置错误会掩盖后面的真实报错。

八、动手练习建议

  1. 加一个"设置模块"(setting_module HAR) :提供 ISettingService 契约(读取/保存音量开关),在 entry 首页加入口。走完"定义契约 → 实现注册 → 页面自注册 → 壳组装"全流程。
  2. 把商品列表改成 emitter 通知刷新:模拟"支付成功后商品库存变化",体会事件总线与 ServiceRegistry 的分工。
  3. 把 MockHttp 换成真实 http 请求:验证业务模块是否真的零改动。
  4. 故意让 user_module import pay_module 的实现类,观察依赖规则被破坏后编译/架构上会发生什么,再改回契约调用。

九、参考文件速查

内容 路径
服务注册表 common_api/src/main/ets/registry/ServiceRegistry.ets
路由中心 common_api/src/main/ets/registry/RouteCenter.ets
登录态封装(HSP) common_service/src/main/ets/session/SessionManager.ets
事件常量 common_api/src/main/ets/constants/Events.ets
路由名常量 common_api/src/main/ets/constants/RouteNames.ets
共享数据模型 common_api/src/main/ets/model/Models.ets
Mock 网络层 common/src/main/ets/network/MockHttp.ets
模块初始化入口 entry/src/main/ets/entryability/EntryAbility.ets
Navigation 组装 entry/src/main/ets/pages/Index.ets

项目地址

gitee.com/qiuyu123/hm...

相关推荐
特立独行的猫A1 小时前
Tauri v2 桌面应用m3u8dl-tauri移植到 HarmonyOS(鸿蒙 PC)完整实战指南
harmonyos
HarmonyOS_SDK1 小时前
从“一屏一态”到“一屏多能”:WPS通过HarmonyOS多窗口能力重塑移动办公体验
harmonyos
世人万千丶2 小时前
收纳格卡片风:ArkUI 让鸿蒙物品清单像收纳盒贴标
学习·华为·harmonyos·鸿蒙
yuhulkjv3352 小时前
Grok鸿蒙版导出word格式的终极解法:AI导出鸭如何重构AI内容到文档的最后一公里
人工智能·ai·word·harmonyos·ai导出鸭
大锅盖13 小时前
HarmonyOS 6.1.1 ArkWeb:交付门户下载资料前-为什么必须先登记下载代理与来源字段
android·华为·harmonyos
特立独行的猫a3 小时前
Tauri v2的Rust应用 → HarmonyOS(鸿蒙 PC)移植30分钟速成指南
开发语言·rust·harmonyos·tauri·移植·鸿蒙pc
YM52e15 小时前
分页查询的基石:ArkTS 为鸿蒙商品列表设计 LIMIT/OFFSET 的表
android·学习·华为·harmonyos
math_hongfan15 小时前
主从嵌套层次分明:ArkUI 订单卡片内嵌明细的鸿蒙界面
学习·华为·harmonyos
woshihuanglaoshi18 小时前
事务加持防超卖:ArkTS 在鸿蒙里玩转 TRANSACTION 出入库
学习·华为·harmonyos