熟悉我们之前项目的读者一定还记得,在做AI旅行助手时,随着功能不断扩展,项目逐渐变得庞大复杂。最初只有一个entry模块负责所有功能,但随着需求增加,我们逐渐加入了地图模块、社交分享模块、PDF处理模块等。很快,代码量从最初的几千行增长到几万行,编译时间越来越长,团队协作也出现了频繁的代码冲突。

这时候,模块化开发就变得尤为重要。但在HarmonyOS开发中,我们面临三个选择:HAR、HSP、HAP。它们看起来相似,但实际应用场景和效果却有天壤之别。今天我们就来彻底搞懂这三者的区别,以及在实际项目中如何做出正确选择。

一、 从实际问题出发:为什么要模块化?

在开始技术细节之前,先看一个真实场景。我们的AI旅行助手最初架构是这样的:

AI旅行助手(单HAP)
├── 核心AI功能
├── 地图模块
├── 社交分享模块
├── PDF处理模块
├── 用户管理模块
└── 数据缓存模块

随着功能增加,出现了几个明显问题:

  1. 编译速度慢:任何小改动都要重新编译整个应用

  2. 代码耦合度高:模块间直接引用,改一处可能影响多处

  3. 包体积过大:即使用户只用到部分功能,也要下载完整应用

  4. 团队协作冲突:多人同时修改同一模块,合并代码困难

这时候,模块化重构就提上了日程。但问题来了:该用HAR、HSP还是HAP?

二、 核心概念详解:HAR、HSP、HAP到底是什么?

2.1 HAP:应用安装和运行的基本单元

HAP(Harmony Ability Package)​ 是应用安装和运行的基本单元,可以理解为Android中的APK模块。每个HAP包含代码、资源、第三方库、配置文件等。

// 典型的HAP结构
entry/
├── src/
│   ├── main/
│   │   ├── ets/        // ArkTS代码
│   │   ├── resources/  // 资源文件
│   │   └── module.json5  // 模块配置文件
│   └── resources/
├── oh-package.json5    // 依赖配置
└── build-profile.json5 // 构建配置

HAP分为两种类型:

  • entry:入口模块,必须存在,包含应用的主入口

  • feature:功能模块,可选,实现特定功能

// module.json5 - entry类型配置
{
  "module": {
    "name": "entry",
    "type": "entry",  // entry类型
    "description": "$string:module_entry_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone",
      "tablet"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages"
  }
}
// module.json5 - feature类型配置
{
  "module": {
    "name": "map_feature",
    "type": "feature",  // feature类型
    "description": "$string:module_map_desc",
    "deviceTypes": [
      "phone"
    ],
    "deliveryWithInstall": false,  // 可按需安装
    "installationFree": true,
    "pages": "$profile:map_pages"
  }
}

2.2 HAR:静态共享包

HAR(Harmony Archive)​ 是静态共享包,主要用于代码和资源的复用。可以把它理解为HarmonyOS版的npm包或Maven依赖。

// 创建和使用HAR的完整流程
@Entry
@Component
struct MainPage {
  // 引入HAR中的组件
  @Builder
  BuildComponentFromHar() {
    // 这里引入HAR中导出的组件
    Column() {
      // 假设CommonButton是从common_ui.har导入的组件
      CommonButton({ text: '从HAR导入的按钮' })
        .onClick(() => {
          // 调用HAR中的工具函数
          const utils = new CommonUtils();
          utils.showToast('来自HAR的功能');
        })
    }
  }
  
  build() {
    Column() {
      this.BuildComponentFromHar()
    }
  }
}

HAR的核心特点:

  • 编译时复制:HAR中的代码在编译时会被复制到每个引用它的模块中

  • 可跨应用共享:可以发布到OHPM(OpenHarmony包管理器)中心仓

  • 版本独立:每个模块引用的是独立的HAR副本

2.3 HSP:动态共享包

HSP(Harmony Shared Package)​ 是动态共享包,这是HarmonyOS特有的模块化方案,解决了HAR的资源重复问题。

// HSP的典型使用场景
@Entry
@Component
struct TravelApp {
  // 动态加载HSP模块
  async loadMapModule() {
    try {
      // 检查HSP是否已安装
      const isInstalled = await this.checkHspInstalled('com.example.mapmodule');
      
      if (!isInstalled) {
        // 动态下载并安装HSP
        await this.downloadAndInstallHsp('https://example.com/map.hsp');
      }
      
      // 加载HSP中的Ability
      const context = getContext(this) as common.UIAbilityContext;
      let want: Want = {
        deviceId: '', // 空字符串表示本设备
        bundleName: 'com.example.travel',
        abilityName: 'MapAbility',
        moduleName: 'map_hsp'  // 指定HSP模块名
      };
      
      context.startAbility(want);
      
    } catch (error) {
      console.error('加载地图模块失败:', error);
    }
  }
  
  build() {
    Column() {
      Button('查看地图')
        .onClick(() => this.loadMapModule())
    }
  }
}

HSP的核心特点:

  • 运行时共享:多个模块共享同一份HSP代码

  • 节省空间:不会重复打包,有效控制应用体积

  • 同进程运行:与宿主应用共享进程和生命周期

三、 详细对比:如何选择?

3.1 核心差异对比

特性

HAP

HAR

HSP

定义

应用安装运行单元

静态共享包

动态共享包

跨应用共享

不可共享

✅ 支持

❌ 仅限应用内

资源复用方式

不涉及

编译时复制

运行时共享

多模块引用影响

不涉及

多份拷贝,体积增大

单份共享,节省空间

发布方式

打包为应用

OHPM中心仓/私仓

跟随宿主应用发布

生命周期

独立/可配置

编译时确定

与宿主应用相同

3.2 实际选择指南

基于我们的AI旅行助手项目,这里提供具体的选择建议:

场景1:通用UI组件库

// 情况:多个模块都需要使用相同的按钮、卡片、弹窗等UI组件
// ❌ 错误做法:每个模块都复制一份相同的组件代码
// ✅ 正确选择:使用HSP

// 理由:HSP确保多个模块共享同一份UI组件代码
// 优势:
// 1. 一处修改,多处生效
// 2. 不会增加包体积
// 3. 样式和交互保持一致

场景2:工具类和工具函数

// 情况:网络请求、图片处理、数据转换等工具函数
// 选择依据:
// - 如果只在当前应用内使用:HSP ✅
// - 如果要给其他应用使用:HAR ✅

// 网络请求工具 - 使用HSP
class NetworkUtils {
  // 这个工具只在旅行助手应用内使用
  static async requestTravelData(url: string) {
    // 实现...
  }
}

// 图片处理工具 - 使用HAR
class ImageProcessor {
  // 这个工具可能被多个不同应用使用
  static compressImage(imageData: Uint8Array): Uint8Array {
    // 实现...
  }
}

场景3:业务功能模块

// 情况:地图模块、社交分享模块、PDF处理模块
// 选择:将这些模块拆分为独立的feature HAP

// 优势:
// 1. 可独立开发、测试
// 2. 可按需安装,减少初始包体积
// 3. 独立团队并行开发

四、 实战案例:AI旅行助手的模块化重构

4.1 重构前:单一HAP架构

// 重构前 - 所有代码在一个HAP中
AI旅行助手(entry HAP,50MB)
├── src/main/ets/
│   ├── MainAbility.ets        // 主入口
│   ├── pages/
│   │   ├── HomePage.ets       // 首页
│   │   ├── AIChatPage.ets     // AI对话
│   │   ├── MapPage.ets        // 地图功能 - 3000行代码
│   │   ├── SocialPage.ets     // 社交分享 - 2000行代码
│   │   └── PdfPage.ets        // PDF处理 - 2500行代码
│   ├── common/
│   │   ├── components/        // 公共组件 - 重复定义
│   │   ├── utils/             // 工具函数 - 各处拷贝
│   │   └── constants/         // 常量定义
│   └── ...

存在的问题

  • 地图、社交、PDF模块代码混杂,耦合严重

  • 公共组件在多个页面重复定义

  • 编译一次需要3分钟

  • 安装包体积50MB,用户下载慢

4.2 重构后:模块化架构

// 重构后 - 模块化架构
AI旅行助手(主包 15MB + 动态模块)
├── entry HAP(5MB)- 主入口
├── map_feature HAP(8MB)- 地图模块
├── social_feature HAP(3MB)- 社交分享
├── pdf_feature HAP(4MB)- PDF处理
├── common_ui HSP(2MB)- 公共UI组件
├── network_utils HSP(1MB)- 网络工具
└── data_manager HSP(3MB)- 数据管理

具体实现步骤

步骤1:创建公共UI组件HSP

// common_ui模块的oh-package.json5
{
  "name": "@travel/common-ui",
  "version": "1.0.0",
  "description": "AI旅行助手公共UI组件库",
  "types": "./index.d.ts",
  "main": "./src/main/ets/index.ets",
  "dependencies": {
    // HSP特有的依赖配置
  }
}

// 定义可共享的组件
@Component
export struct TravelButton {
  @Prop text: string = '';
  @Prop type: 'primary' | 'secondary' | 'outline' = 'primary';
  @Prop loading: boolean = false;
  @Emit onClick: () => void = () => {};
  
  build() {
    Button(this.text)
      .width('100%')
      .height(44)
      .fontSize(16)
      .fontWeight(FontWeight.Medium)
      .backgroundColor(this.getBackgroundColor())
      .fontColor(this.getFontColor())
      .border({
        width: this.type === 'outline' ? 1 : 0,
        color: '#1890FF'
      })
      .borderRadius(8)
      .enabled(!this.loading)
      .onClick(() => {
        if (!this.loading) {
          this.onClick();
        }
      })
  }
  
  private getBackgroundColor(): ResourceColor {
    switch (this.type) {
      case 'primary': return '#1890FF';
      case 'secondary': return '#F5F5F5';
      case 'outline': return Color.White;
      default: return '#1890FF';
    }
  }
}

步骤2:创建工具类HSP

// network_utils HSP - 网络请求工具
export class TravelApiClient {
  private static instance: TravelApiClient;
  private baseURL: string = 'https://api.travel-assistant.com';
  private token: string = '';
  
  static getInstance(): TravelApiClient {
    if (!TravelApiClient.instance) {
      TravelApiClient.instance = new TravelApiClient();
    }
    return TravelApiClient.instance;
  }
  
  async request<T>(endpoint: string, options?: RequestOptions): Promise<T> {
    const url = `${this.baseURL}${endpoint}`;
    const headers = {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${this.token}`,
      ...options?.headers
    };
    
    try {
      const response = await fetch(url, {
        method: options?.method || 'GET',
        headers,
        body: options?.body ? JSON.stringify(options.body) : undefined
      });
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      
      return await response.json() as T;
    } catch (error) {
      console.error('API请求失败:', error);
      throw error;
    }
  }
  
  // 设置认证token
  setToken(token: string): void {
    this.token = token;
  }
}

步骤3:创建功能模块feature HAP

// map_feature模块的module.json5
{
  "module": {
    "name": "map_feature",
    "type": "feature",  // 注意这里是feature类型
    "description": "$string:module_map_desc",
    "deviceTypes": [
      "phone",
      "tablet"
    ],
    "deliveryWithInstall": false,  // 不随主包安装
    "installationFree": true,      // 支持免安装
    "pages": "$profile:map_pages",
    "abilities": [
      {
        "name": "MapAbility",
        "srcEntry": "./ets/MapAbility.ets",
        "description": "$string:map_ability_desc",
        "icon": "$media:icon",
        "label": "$string:map_ability_label",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "visible": true,
        "skills": [
          {
            "actions": [
              "action.system.detail"
            ],
            "entities": [
              "entity.system.detail"
            ]
          }
        ]
      }
    ]
  }
}

步骤4:配置模块依赖

// entry模块的oh-package.json5
{
  "dependencies": {
    // 依赖common_ui HSP
    "@travel/common-ui": "file:../common_ui",
    // 依赖network_utils HSP
    "@travel/network-utils": "file:../network_utils"
  }
}

// map_feature模块的oh-package.json5
{
  "dependencies": {
    // 同样依赖这两个HSP
    "@travel/common-ui": "file:../common_ui",
    "@travel/network-utils": "file:../network_utils",
    // 可能还有地图相关的特定依赖
    "@ohos/maps": "^1.0.0"
  }
}

五、 常见问题与解决方案

5.1 HAR转HSP的常见问题

问题:HAR转HSP后编译报错

// 错误示例:HAR中的单例模式在HSP中失效
// HAR中的工具类
export class ConfigManager {
  private static instance: ConfigManager;
  private config: any = {};
  
  private constructor() {}
  
  static getInstance(): ConfigManager {
    if (!ConfigManager.instance) {
      ConfigManager.instance = new ConfigManager();
    }
    return ConfigManager.instance;
  }
  
  setConfig(key: string, value: any): void {
    this.config[key] = value;
  }
  
  getConfig(key: string): any {
    return this.config[key];
  }
}

// 在HSP中使用时的问题
// 模块A中设置值
ConfigManager.getInstance().setConfig('theme', 'dark');

// 模块B中获取值 - 这里获取到的是默认值,不是dark!
// 因为每个模块加载的是不同的HAR副本
const theme = ConfigManager.getInstance().getConfig('theme'); // undefined

解决方案

// 方案1:使用HSP替代HAR
// 将ConfigManager改为HSP,确保单例真正共享

// 方案2:使用持久化存储替代内存共享
import { preferences } from '@kit.PreferencesKit';

export class SharedConfigManager {
  private static prefStore: preferences.Preferences | null = null;
  
  static async initialize(): Promise<void> {
    this.prefStore = await preferences.getPreferences(
      getContext(), 
      'shared_config'
    );
  }
  
  static async setConfig(key: string, value: string): Promise<void> {
    if (!this.prefStore) await this.initialize();
    await this.prefStore.put(this.prefStore, key, value);
    await this.prefStore.flush(this.prefStore);
  }
  
  static async getConfig(key: string): Promise<string> {
    if (!this.prefStore) await this.initialize();
    return await this.prefStore.get(this.prefStore, key, '');
  }
}

5.2 多HSP引用同一个HAR的问题

问题:多HSP引用同一个HAR,在A HSP中初始化的值,在B HSP中获取不到。

// 错误:HAR被多个HSP引用
project/
├── common_har/          // HAR包
├── feature_a_hsp/       // 引用common_har
└── feature_b_hsp/       // 也引用common_har

// 结果:feature_a_hsp和feature_b_hsp各自有一份common_har的副本
// feature_a_hsp中设置的值,feature_b_hsp访问不到

解决方案

  1. 将HAR升级为HSP

  2. 使用持久化存储(Preferences、RDB等)在模块间共享数据

  3. 通过事件总线(EventBus)在模块间通信

5.3 动态加载feature HAP

// 动态加载和安装feature模块
@Entry
@Component
struct MainPage {
  // 检查模块是否已安装
  async checkModuleInstalled(moduleName: string): Promise<boolean> {
    const bundleManager = bundleManager.getBundleManagerForSelf();
    const bundleFlags = 0;
    
    try {
      const bundleInfo = await bundleManager.getBundleInfoForSelf(bundleFlags);
      const moduleExists = bundleInfo.hapModuleInfos?.some(
        module => module.name === moduleName
      );
      return !!moduleExists;
    } catch (error) {
      console.error('检查模块失败:', error);
      return false;
    }
  }
  
  // 动态安装feature模块
  async installFeatureModule(moduleName: string): Promise<void> {
    try {
      const context = getContext(this) as common.UIAbilityContext;
      const want: Want = {
        bundleName: 'com.example.travel',
        abilityName: 'MainAbility',
        parameters: {
          'action': 'installModule',
          'moduleName': moduleName
        }
      };
      
      await context.startAbilityForResult(want);
      
      // 安装成功后,可以启动该模块
      await this.launchFeatureModule(moduleName);
      
    } catch (error) {
      console.error('安装模块失败:', error);
      prompt.showToast({
        message: '模块安装失败,请重试',
        duration: 3000
      });
    }
  }
  
  // 启动feature模块
  async launchFeatureModule(moduleName: string): Promise<void> {
    const context = getContext(this) as common.UIAbilityContext;
    let want: Want = {
      deviceId: '', // 本设备
      bundleName: 'com.example.travel',
      moduleName: moduleName,
      abilityName: `${moduleName}Ability`
    };
    
    await context.startAbility(want);
  }
}

六、 最佳实践总结

6.1 选择建议总结

根据我们的AI旅行助手项目经验,总结出以下选择建议:

使用HAR的场景

  1. 跨应用共享的UI组件库

  2. 通用的工具类库(如日期处理、字符串工具等)

  3. 第三方封装的SDK

  4. 需要发布到OHPM中心仓供其他开发者使用的库

使用HSP的场景

  1. 应用内多个模块共享的UI组件

  2. 应用内共享的业务工具类

  3. 需要单例管理的服务类

  4. 资源文件(图片、字体等)共享

使用feature HAP的场景

  1. 独立的功能模块(如地图、社交、支付等)

  2. 可按需安装的功能

  3. 不同团队负责的独立模块

  4. 需要独立测试和发布的模块

6.2 性能优化建议

// 模块懒加载优化
@Component
struct LazyModuleLoader {
  @State isModuleLoaded: boolean = false;
  @State moduleComponent: any = null;
  
  // 按需加载模块
  async loadModuleIfNeeded() {
    if (this.isModuleLoaded) {
      return;
    }
    
    // 显示加载状态
    this.isModuleLoaded = false;
    
    try {
      // 动态导入模块
      const module = await import('../feature_module/FeatureComponent');
      this.moduleComponent = module.FeatureComponent;
      this.isModuleLoaded = true;
    } catch (error) {
      console.error('加载模块失败:', error);
    }
  }
  
  build() {
    Column() {
      if (this.isModuleLoaded && this.moduleComponent) {
        // 动态渲染加载的组件
        this.moduleComponent()
      } else {
        // 显示加载中状态
        LoadingIndicator()
          .size(40)
          .color('#1890FF')
        
        Button('加载模块')
          .onClick(() => this.loadModuleIfNeeded())
      }
    }
  }
}

6.3 包大小优化对比

优化前(单HAP):

  • 总大小:50MB

  • 首次下载:50MB

  • 包含:所有功能

优化后(模块化):

  • entry HAP:5MB(核心功能)

  • 按需feature HAPs:20MB(可选安装)

  • 公共HSPs:6MB(共享,不重复)

  • 首次下载:11MB(节省78%流量)

七、 常见陷阱与调试技巧

7.1 依赖循环问题

// 错误:循环依赖
// module_a依赖module_b,module_b又依赖module_a
{
  "module_a": {
    "dependencies": {
      "module_b": "file:../module_b"
    }
  },
  "module_b": {
    "dependencies": {
      "module_a": "file:../module_a"
    }
  }
}

解决方案

  1. 提取公共代码到新的HSP

  2. 使用接口抽象代替具体实现依赖

  3. 通过事件通信代替直接调用

7.2 资源ID冲突问题

// HSP中的资源引用
// 正确做法:使用资源别名避免冲突
@Component
struct SharedComponent {
  @Builder
  BuildImage() {
    // ❌ 错误:直接使用资源ID,可能与其他模块冲突
    // Image($r('app.media.logo'))
    
    // ✅ 正确:通过HSP模块名限定
    Image($r('app.media.module_a:logo'))
  }
}

7.3 版本管理建议

// 版本锁定策略
{
  "dependencies": {
    // 主版本号.次版本号.修订号
    "@travel/common-ui": "1.2.3",           // 精确版本
    "@travel/network-utils": "^1.2.0",     // 兼容版本(自动更新次版本和修订号)
    "@travel/data-manager": "~1.2.3"       // 只更新修订号
  }
}

八、 总结

通过模块化重构,我们的AI旅行助手项目获得了显著改进:

重构效果对比

  • 编译时间:从3分钟缩短到30秒(增量编译)

  • 安装包大小:从50MB减少到11MB(首次下载)

  • 团队开发效率:提升60%(并行开发,减少冲突)

  • 代码维护性:大幅提升(模块解耦,职责清晰)

关键收获

  1. HAP是基础:每个应用必须有entry HAP,feature HAP实现模块化

  2. HSP优于HAR:应用内共享优先使用HSP,避免资源重复

  3. 按需加载:非核心功能使用feature HAP,按需安装

  4. 合理拆分:按业务域拆分模块,保持高内聚低耦合

最佳实践

  1. 小即是美:每个模块专注单一职责

  2. 明确依赖:避免循环依赖,合理使用HSP

  3. 版本控制:严格管理模块版本

  4. 持续重构:随着业务发展,不断优化模块划分

HarmonyOS的模块化体系(HAR、HSP、HAP)为大型应用开发提供了强大支持。正确使用这些工具,不仅能提升开发效率,还能显著改善用户体验。希望本文的实践经验能帮助你在HarmonyOS应用开发中,做出更明智的架构选择。

 

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐