Administrator
发布于 2026-09-03 / 5,595 阅读
0
0

鸿蒙大型应用分层架构:应用层、能力层、服务层与数据层

鸿蒙百篇系列第 71 篇 · 专家

第 10 篇讲过架构设计的模块化思路,第 34 篇讲过组件化拆分。到第 70 篇为止,你已经掌握了从渲染管线到系统服务的底层知识。从这篇开始,我们进入"专家架构"阶段——把这些底层能力用工程化的方式组织起来,解决大型鸿蒙应用的分层问题。

一个鸿蒙应用从 Demo 到正式产品,最大的转折点不是"功能做完了",而是"代码管不住了"。三五十个模块互相依赖,改一个接口牵连十处编译错误,新来的同事打开工程看半小时才找到入口在哪。这时候你需要的不是某个 API 技巧,而是一套分层架构。

一、为什么要分层

不分层的应用长什么样?一个典型反例:

src/main/ets/
├── pages/
│   ├── HomePage.ets       // 首页里直接写了 HTTP 请求
│   ├── DetailPage.ets     // 详情页里直接操作数据库
│   └── MinePage.ets       // 个人中心里直接写 SharedPreferences
├── model/
│   └── User.ets           // 数据模型和 UI 组件混在一起
└── common/
    └── utils.ets          // 所有工具函数堆在一个文件

这种结构在功能数到 10 个页面以内还能跑,超过就开始出问题:

  • 首页想换网络库,发现 HTTP 调用散落在 8 个页面里

  • 详情页改了数据结构,个人中心的 UI 也崩了

  • 测试网络请求要启动整个应用

分层的本质是把"关注点"隔离开。UI 层不关心数据从哪来,数据层不关心 UI 怎么显示。鸿蒙的官方推荐三层架构:产品定制层、基础特性层、公共能力层。但在大型应用实践中,需要更细的划分。

二、四层架构设计

在华为官方三层架构基础上,结合实际工程经验,大型鸿蒙应用建议采用四层架构:

┌─────────────────────────────────────────┐
│           产品定制层 (products)            │  ← entry 模块
│  应用入口、设备适配、页面路由编排          │
├─────────────────────────────────────────┤
│           基础特性层 (features)           │  ← feature HAP / HSP
│  登录、支付、消息、搜索等业务模块          │
├─────────────────────────────────────────┤
│           能力层 (abilities)              │  ← HSP 动态共享包
│  网络通信、数据存储、日志埋点、安全        │
├─────────────────────────────────────────┤
│           公共层 (common)                 │  ← HAR 静态共享包
│  UI 组件库、工具类、类型定义、常量         │
└─────────────────────────────────────────┘

各层之间只能向下依赖,不能反向引用。

层级职责

层级

模块形态

依赖方向

编译产物

变更频率

产品定制层

entry HAP

→ 基础特性层

.hap

高

基础特性层

feature HAP / HSP

→ 能力层

.hap / .hsp

中

能力层

HSP

→ 公共层

.hsp

低

公共层

HAR

无依赖

.har

极低

变更频率从上到下递减。公共层的接口定义一旦稳定,几乎不会改动。产品定制层每个版本都在变。这个设计让底层变更的影响范围可控。

三、模块形态选型

鸿蒙提供三种模块形态:HAP、HAR、HSP。选型直接决定架构能不能跑通。

形态

加载机制

体积影响

适用场景

HAP

随安装包加载

独立编译产物

应用入口、核心功能模块

HAR

编译时拷贝到引用方

多包引用会膨胀

工具类、UI 组件、类型定义

HSP

运行时共享加载

多包引用不膨胀

业务模块、能力封装

关键决策点:

  • 如果一个 HAR 被 3 个以上 HAP 引用,改成 HSP。否则同一份代码拷贝 3 份,包体积白白增大。

  • 如果一个 HSP 只被 1 个 HAP 引用,改成 HAR。HSP 运行时加载有额外开销,单引用场景不划算。

  • 公共层用 HAR。类型定义和工具函数没有运行时状态,编译时集成效率最高。

四、目录结构落地

以一个电商应用为例,四层架构的工程目录如下:

myapp/
├── entry/                          # 产品定制层 (HAP)
│   └── src/main/ets/
│       ├── entryability/
│       │   └── EntryAbility.ets     # 应用入口
│       └── pages/
│           └── MainPage.ets         # 首页(编排各 feature 的页面)
│
├── features/                        # 基础特性层
│   ├── feature_home/               # 首页模块 (HSP)
│   │   └── src/main/ets/
│   │       └── pages/HomePage.ets
│   ├── feature_cart/               # 购物车模块 (HSP)
│   │   └── src/main/ets/
│   │       └── pages/CartPage.ets
│   └── feature_mine/               # 个人中心模块 (HSP)
│       └── src/main/ets/
│           └── pages/MinePage.ets
│
├── abilities/                       # 能力层
│   ├── ability_network/            # 网络通信 (HSP)
│   │   └── src/main/ets/
│   │       ├── HttpClient.ets      # HTTP 封装
│   │       └── ApiInterceptor.ets  # 拦截器
│   ├── ability_storage/            # 数据存储 (HSP)
│   │   └── src/main/ets/
│   │       ├── RdbHelper.ets       # RDB 封装
│   │       └── PreferencesHelper.ets
│   └── ability_security/           # 安全能力 (HSP)
│       └── src/main/ets/
│           └── CryptoUtil.ets
│
├── common/                         # 公共层 (HAR)
│   └── src/main/ets/
│       ├── components/             # UI 组件
│       │   ├── LoadingView.ets
│       │   └── ErrorView.ets
│       ├── model/                  # 数据模型
│       │   ├── Product.ets
│       │   └── User.ets
│       ├── constants/              # 常量
│       │   └── AppConstants.ets
│       └── utils/                  # 工具类
│           └── DateUtils.ets
│
├── build-profile.json5             # 工程构建配置
└── oh-package.json5                # 依赖管理

五、模块间通信

分层之后,模块之间不能直接调用对方内部实现。需要通过标准化接口通信。

方式一:接口定义在公共层

在 common 层定义接口,各层实现:

// common/src/main/ets/interfaces/INetworkService.ets
export interface INetworkService {
  get<T>(url: string): Promise<T>;
  post<T>(url: string, data: object): Promise<T>;
}

能力层实现接口:

// abilities/ability_network/src/main/ets/NetworkService.ets
import { INetworkService } from '@app/common';

export class NetworkService implements INetworkService {
  async get<T>(url: string): Promise<T> {
    const http = await import('@ohos.net.http');
    const client = http.createHttp();
    const res = await client.request(url, { method: http.RequestMethod.GET });
    return JSON.parse(res.result as string) as T;
  }

  async post<T>(url: string, data: object): Promise<T> {
    const http = await import('@ohos.net.http');
    const client = http.createHttp();
    const res = await client.request(url, {
      method: http.RequestMethod.POST,
      extraData: JSON.stringify(data),
      header: { 'Content-Type': 'application/json' }
    });
    return JSON.parse(res.result as string) as T;
  }
}

特性层通过依赖注入使用:

// features/feature_home/src/main/ets/HomePage.ets
import { INetworkService } from '@app/common';

@Entry
@Component
struct HomePage {
  @State products: Product[] = [];
  private networkService: INetworkService;

  aboutToAppear() {
    this.loadData();
  }

  async loadData() {
    try {
      this.products = await this.networkService.get<Product[]>('/api/products');
    } catch (err) {
      console.error('加载失败: ' + (err as Error).message);
    }
  }

  build() {
    // ...渲染列表
  }
}

方式二:事件总线

跨模块事件通知用 Emitter(参考第 47 篇调试技巧中提到的事件机制):

// common 层定义事件常量
export const EVENT_CART_UPDATED = 'cart_updated';
export const EVENT_LOGIN_SUCCESS = 'login_success';

// 购物车模块发送事件
import { emitter } from '@kit.BasicServicesKit';
emitter.emit({ eventId: EVENT_CART_UPDATED }, { data: { count: 3 } });

// 首页模块监听
emitter.on({ eventId: EVENT_CART_UPDATED }, (eventData) => {
  console.info('购物车数量更新: ' + eventData.data.count);
});

方式三:AppStorage 全局状态

跨模块共享简单状态用 AppStorage:

// 登录模块写入
AppStorage.setOrCreate('userInfo', { id: '123', name: '张三' });

// 任意模块读取
const userInfo = AppStorage.get('userInfo');

AppStorage 只适合轻量数据。数据量大或结构复杂时用 RDB(第 23 篇)或分布式 KV(第 62 篇)。

六、依赖注入容器

大型应用需要统一的依赖注入容器,避免每个模块手动创建实例:

// common/src/main/ets/DIContainer.ets
export class DIContainer {
  private static instances: Map<string, object> = new Map();

  static register<T>(key: string, instance: T): void {
    this.instances.set(key, instance as object);
  }

  static resolve<T>(key: string): T {
    return this.instances.get(key) as T;
  }

  static unregister(key: string): void {
    this.instances.delete(key);
  }
}

// 在 EntryAbility 中初始化
import { DIContainer } from '@app/common';
import { NetworkService } from '@app/ability_network';
import { RdbHelper } from '@app/ability_storage';

export default class EntryAbility extends UIAbility {
  onCreate() {
    // 注册全局依赖
    DIContainer.register('INetworkService', new NetworkService());
    DIContainer.register('IRdbHelper', new RdbHelper());
  }
}

各模块通过容器获取依赖:

const networkService = DIContainer.resolve<INetworkService>('INetworkService');

七、多设备适配

产品定制层负责设备差异化。通过条件编译和设备类型判断,实现多设备适配:

// entry/src/main/ets/pages/MainPage.ets
import { deviceInfo } from '@kit.BasicServicesKit';

@Entry
@Component
struct MainPage {
  @State deviceType: string = '';

  aboutToAppear() {
    this.deviceType = deviceInfo.deviceType;
  }

  build() {
    if (this.deviceType === 'phone') {
      // 手机竖向布局
      Column() {
        this.contentArea()
      }
    } else if (this.deviceType === 'tablet') {
      // 平板左右分栏
      Row() {
        this.sideBar()
        this.contentArea()
      }
    }
  }

  @Builder contentArea() { /* ... */ }
  @Builder sideBar() { /* ... */ }
}

八、常见误区

误区

后果

正确做法

所有模块都做成 HSP

启动变慢,模块加载开销大

公共层用 HAR,只对多引用的业务模块用 HSP

公共层依赖能力层

循环依赖,编译失败

公共层不依赖任何上层,只定义接口

特性层之间直接互相引用

模块耦合,无法独立编译

通过事件总线或 DIContainer 通信

产品层直接操作数据库

UI 代码膨胀,测试困难

数据操作走能力层,UI 只管渲染

九、从单模块迁移到分层架构

如果你的应用已经是一个大 entry 模块,迁移步骤:

  1. 先拆公共层。把工具类、常量、数据模型抽到 HAR,entry 模块引用它。这一步风险最低。

  2. 再拆能力层。把网络、存储、日志等通用能力封装成 HSP,通过接口暴露给上层。

  3. 然后拆特性层。按业务域划分 feature 模块,每个 feature 包含自己的页面和业务逻辑。

  4. 最后整理产品层。entry 只保留入口、路由编排和设备适配。

每一步迁移完做一轮回归测试,确保功能没退化。

十、小结

分层架构不是"看起来整齐",而是"改起来安全"。四层架构的核心原则就一条:依赖只能向下,不能向上。公共层不知道业务,能力层不知道 UI,特性层不知道彼此存在。违反这条原则,架构就会退回到面条代码。

下一篇我们讲错误体系标准化。分层之后,错误如何在层间传递、如何统一采集、如何不丢失上下文——这是分层架构能不能用的关键。


评论