Administrator
发布于 2026-09-02 / 3,513 阅读
0
0

鸿蒙组件化与插件化:模块拆分与运行时加载

系列文章《鸿蒙开发从入门到专家》第 34 篇 · 高级

几乎每个写了两三年的鸿蒙应用都会经历这一段:项目里几十个 Ability、上百个组件,互相引用成藤蔓状,改一行崩一片,新人来了两个月都不敢提交代码。

这是单体(monolithic)结构的宿命:模块间没有边界,依赖单向不可控。这类问题的标准答案是"组件化",本质是把"代码上的可见性"变成"架构上的边界"。

这一篇把鸿蒙应用从单体走向组件化、插件化的过程讲清楚,配上代码模板和路由设计。

一、为什么一定要组件化

我们从反面看一下不组件化的代价:

  • 编译时间失控:上千个文件改动一次,全量编译经常 5 分钟起,调试等待时间远超写代码时间。

  • 小组并行困难:交易组改 Payment,账号组改 Profile,但代码层都引着公共层的 UserService,互相踩。

  • 单元测试难写:组件间依赖复杂,没法独立测试某个模块。

组件化要解决的就是这三件事:隔离、并行、可测。它和"模块化"不一样——模块化只是从文件层面拆分了,组件化是在运行时层面拆开了。

二、鸿蒙的模块机制:HAP/HSP/HAR

鸿蒙提供了三种打包单元,先从概念上厘清:

类型

形态

适用场景

HAP

Ability Package,可独立安装

应用主包、动态加载的特性包

HSP

Harmony Shared Package,动态共享包

多个 HAP 共享的代码与资源

HAR

Harmony Archive,静态包

内部静态依赖,打包进主 HAP

讲组件化最常用的是 HSP——它支持多 HAP 共享,运行时按需加载,并且可以独立版本管理。HAR 适合"复用但不独立部署"的场景,比如一个工具库。

工程结构上的典型样子:

myapp/
  features/
    feature-home/        # 首页特性包,HSP
    feature-product/     # 商品特性包,HSP
    feature-cart/        # 购物车特性包,HSP
    feature-user/        # 用户中心特性包,HSP
  modules/
    module-net/          # 网络层,HSP
    module-router/       # 路由层,HSP
    module-ui-kit/       # 公共 UI 库,HAR
  entry/                 # 主入口 HAP

每个特性包对外只暴露一个 Index 文件和一组路由:

features/feature-product/src/main/ets/
  index.ets              # 路由出口
  pages/
    ProductListPage.ets
    ProductDetailPage.ets
  viewmodel/
    ProductVM.ets
  model/
    Product.ets

index.ets 只 forward 必要的接口:

// features/feature-product/src/main/ets/index.ets
export { ProductListPage, ProductDetailPage } from './pages/Index';
export { ProductVM } from './viewmodel/ProductVM';
export type { Product } from './model/Product';

调用方只能看到这三个符号,看不到 ProductService、ProductRepository 这些内部实现。这种"只暴露入口"是组件隔离的第一道门。

三、组件路由:从"硬编码跳转"到"路由表"

组件化最大的设计问题之一是"入口怎么跳"。如果还在用 router.pushUrl({ url: 'pages/ProductListPage' }),组件化等于白做——因为入口地址把组件之间的依赖又绑死了。

引入路由表是必备动作。

1. 路由中心

module-router 定义一个路由器:

// module-router/src/main/ets/Router.ets
import { Router } from '@kit.ArkUI';

const ROUTES = new Map<string, (params?: object) => void>([
  ['product/list', () => { Router.pushUrl({ url: 'pages/ProductListPage' }); }],
  ['product/detail', (params?: object) => {
    Router.pushUrl({ url: 'pages/ProductDetailPage', params: params as object });
  }],
]);

export function jump(name: string, params?: object): void {
  const handler = ROUTES.get(name);
  if (!handler) {
    console.warn(`未注册路由: ${name}`);
    return;
  }
  handler(params);
}

export function registerRoute(name: string, handler: (params?: object) => void): void {
  ROUTES.set(name, handler);
}

每个特性包初始化时注册自己的路由:

// features/feature-product/src/main/ets/Init.ets
import { registerRoute } from 'module-router';

export function initProductFeature(): void {
  registerRoute('product/list', (params) => {
    Router.pushUrl({ url: 'pages/ProductListPage', params });
  });
  registerRoute('product/detail', (params) => {
    Router.pushUrl({ url: 'pages/ProductDetailPage', params });
  });
}

调用方从原来的 Router.pushUrl(...) 改成 Router.jump('product/detail', { id: 123 })。

2. 路由懒加载

支持跳转后再异步加载对应模块的 HAP:

import { bundleManager } from '@kit.AppKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const handler = ROUTES.get(name);
if (handler) {
  handler(params);
} else {
  // 路由不存在,尝试加载模块
  const packageName = routeToPackage.get(name);
  if (packageName) {
    await ensureFeatureInstalled(packageName);
    registerFeature(packageName);
    ROUTES.get(name)?.(params);
  }
}

这种"路由即模块查找"的设计,让未安装的子模块按需下载——这就是"插件化"的运行模型。

四、组件间通信

组件化最忌讳"组件 A 直接调用组件 B 的方法"。常见的几种通信方式:

1. 共享 ViewModel(适合同进程)

用 @ObservedV2 或 EventBus 来跨组件传递数据:

// module-router/src/main/ets/EventBus.ets
type Listener = (data: any) => void;

class _EventBus {
  private listeners = new Map<string, Set<Listener>>();

  on(event: string, cb: Listener) {
    if (!this.listeners.has(event)) this.listeners.set(event, new Set());
    this.listeners.get(event)!.add(cb);
  }

  off(event: string, cb: Listener) {
    this.listeners.get(event)?.delete(cb);
  }

  emit(event: string, data: any) {
    this.listeners.get(event)?.forEach(cb => cb(data));
  }
}

export const EventBus = new _EventBus();

调用:

// 订单完成后通知购物车刷新
EventBus.emit('cart:refresh', { reason: 'order_paid' });

// 购物车订阅
EventBus.on('cart:refresh', () => {
  cartVM.reload();
});

2. 应用级 SharedPreferences / 用户态存储

适合不那么强调实时性的数据。

3. 跨进程:分布式通信

如果组件在另一台设备(见第 29 篇分布式开发),用分布式消息总线。

设计组件通信的几个原则:

  • 接口只传数据,不传行为。

  • 单向数据流:上游 → 下游,事件反向流动。

  • 避免"组件 A 改组件 B 的内部状态"。实在要做,加一层中介。

五、组件化的工程化要点

写代码只是组件化的 30%。剩下 70% 是工程化:

1. 命名规范

feature-{业务}     业务特性包
module-{类型}      通用模块(net/data/ui-kit)
entry              主入口

2. 依赖方向

entry
  ↓
feature-* + module-*
  ↓(只能引用 module-*)
module-* 互不依赖

箭头方向是单向的。feature-a 不能依赖 feature-b,否则就是变相耦合。

3. 版本管理

HSP 之间可能存在版本冲突问题,建议:

  • 主版本统一管理,使用 ohpm-lock。

  • 公共模块更新要走标准化流程。

  • 引入自动化依赖分析工具,发现循环依赖立即报警。

4. 构建脚本

build-profile.json5 里把每个 feature 声明为独立 build target:

{
  "app": {
    "products": [
      {
        "name": "default",
        "bundleName": "com.example.myapp",
        "modules": [
          "entry",
          "feature-home",
          "feature-product",
          "feature-cart",
          "feature-user",
          "module-net",
          "module-router",
          "module-ui-kit"
        ]
      }
    ]
  }
}

CI 每次构建一个 feature 包做单元测试,避免对整体构建的依赖。

六、案例:从单体到组件化的实战

一个电商 App 起初是单 HAP,运行一年后:

  • 模块数:28 个目录,按业务归类。

  • 编译时间:6 分钟。

  • 改动常见问题:支付组改了 UserVM 影响了订单展示。

改造过程分三步:

第一步,按业务拆 HSP。把 home、product、cart、user、order 拆成独立 HSP。module-net、module-ui-kit、module-router 公共化。

第二步,引入路由表。全代码扫一遍 Router.pushUrl(...),替换成 jump('xxx')。这一步会改出不少 bug,正好一并收敛。

第三步,接入特性包灰度。把 feature-product 标为可独立发布的"特性包",不强制更新即可下载。先用 10% 灰度,没问题再放量。

改造后:

指标

改造前

改造后

单模块编译时间

6 分钟

30 秒

特性 PR 平均 review 时间

2 天

0.5 天

主包大小

45MB

22MB

单元测试覆盖率

12%

47%

收益是非常实际的。"写起来累一点,跑起来轻松"是组件化的常态。

七、组件化和插件化的差异

维度

组件化

插件化

部署

跟随主包一起编译

独立安装包,运行时加载

适用

团队内复用、并行

第三方接入、按需扩展

版本

锁定一致

动态升级

通信

直接调用

必须走接口

组件化是插件化的前置。鸿蒙里,HSP 提供的"按需加载"已经把组件化和插件化打通了——一个模块可以按业务需要随时切换部署形态。

小结

组件化不是技术问题,是工程问题。要点:

  • 架构先行:先画依赖图,再写代码。

  • 路由为核:所有跨模块调用都进路由表。

  • 数据单向:共享数据走 ViewModel / EventBus。

  • 构建独立:每个模块可以单独编译、单独测试。

下一篇我们沿着这条路线继续——把"动态化"说透。鸿蒙有它独特的形态:服务卡片、原子化服务、远端下发。这些能力怎么和组件化协同,做到"主包不变给老用户、新能力给新用户",我们下篇展开讲。

参考阅读

  • 鸿蒙开发者文档《应用包结构(HAP/HSP/HAR)》

  • 鸿蒙开发者文档《Stage 模型与组件化》

  • 《架构整洁之道》(Clean Architecture,思路可借鉴)

  • 《组件化架构:项目级模块化开发最佳实践》


评论