Administrator
发布于 2026-08-30 / 62 阅读
0
0

鸿蒙大型应用架构设计:模块化、工程化与状态管理分层实践

系列文章《鸿蒙开发从入门到专家》第 10 篇(专家篇)

前言

前面九篇文章,我们从环境搭建走到了分布式开发,掌握了鸿蒙应用开发的完整技术栈。但掌握技术点和搭建一个能支撑百万用户、几十人团队协作的大型应用,中间还隔着一道架构设计的鸿沟。

很多团队在鸿蒙项目上踩过这样的坑:一开始把所有代码塞进 entry 模块,三个月后代码量破十万行,编译一次要五分钟,改一行代码牵一发而动全身,新成员入职两周还不敢提交代码。本文从工程化角度,给出一套经过验证的鸿蒙大型应用架构方案。

一、模块化拆分:HAP、HSP、HAR 的分工

1.1 三种模块类型

鸿蒙提供了三种模块形态,理解它们的区别是架构设计的基础:

类型

全称

特点

适用场景

HAP

HarmonyOS Ability Package

可独立安装运行,有入口 Ability

主入口模块、独立功能模块

HSP

HarmonyOS Shared Package

不可独立运行,可被多个 HAP 共享

业务模块、动态功能模块

HAR

HarmonyOS Archive

静态共享包,编译时打包进引用方

基础组件库、工具库

1.2 推荐的模块架构

project/
├── entry/                          # HAP:应用主入口
│   ├── src/main/ets/
│   │   ├── entryability/           # 入口 Ability
│   │   └── pages/
│   │       └── SplashPage.ets      # 启动页
│   └── module.json5
│
├── feature_home/                   # HSP:首页模块
│   ├── src/main/ets/pages/         # 首页、推荐页、搜索页
│   └── Index.ets                   # 模块对外导出
│
├── feature_mine/                   # HSP:我的模块
│   ├── src/main/ets/pages/         # 个人中心、设置、订单
│   └── Index.ets
│
├── feature_video/                  # HSP:视频模块(可按需加载)
│   ├── src/main/ets/pages/
│   └── Index.ets
│
├── library_common/                 # HAR:公共基础库
│   ├── network/                    # HttpClient 封装
│   ├── storage/                    # Preferences/RDB 封装
│   ├── utils/                      # 工具函数
│   └── components/                 # 基础 UI 组件
│
├── library_ui/                     # HAR:UI 组件库
│   ├── buttons/
│   ├── dialogs/
│   ├── skeletons/
│   └── themes/
│
└── library_account/                # HAR:账号体系
    ├── auth/
    ├── user/
    └── token/

拆分的核心原则:

  • 按业务域拆分:首页、视频、我的各自独立

  • 基础能力下沉:网络、存储、组件库做成 HAR 共享

  • 入口保持轻薄:entry 只负责启动路由,业务逻辑下沉到 HSP

1.3 HSP 动态加载

HSP 支持运行时动态加载,适合大功能模块的按需加载:

// 动态导入视频模块
async function enterVideoModule() {
  // 显示加载中
  this.isLoading = true
  
  try {
    // 动态导入 HSP
    let videoModule = await import('feature_video')
    
    // 调用模块暴露的入口函数
    videoModule.enterVideoPage(this.navStack)
  } catch (err) {
    console.error('加载视频模块失败:', err)
    promptAction.showToast({ message: '模块加载失败' })
  } finally {
    this.isLoading = false
  }
}

动态加载的好处:

  • 首次安装包体积更小

  • 用户只用到才下载对应模块

  • 模块可以独立更新,无需全量升级

1.4 跨模块路由解耦

模块之间不能直接引用彼此的页面,否则耦合度爆表。API 26 引入了 router_map.json 路由表实现解耦:

// feature_home/src/main/resources/rawfile/router_map.json
{
  "routerMap": [
    {
      "name": "HomePage",
      "pageSourceFile": "src/main/ets/pages/HomePage.ets",
      "buildFunction": "HomePageBuilder"
    },
    {
      "name": "SearchPage",
      "pageSourceFile": "src/main/ets/pages/SearchPage.ets",
      "buildFunction": "SearchPageBuilder"
    }
  ]
}
// feature_home/Index.ets
@Builder
export function HomePageBuilder() {
  HomePage()
}

@Builder
export function SearchPageBuilder() {
  SearchPage()
}

主模块在 module.json5 中引入路由表:

{
  "module": {
    "routerMap": [
      "feature_home",
      "feature_mine",
      "feature_video"
    ]
  }
}

然后就可以通过名称跳转,无需硬编码路径:

this.navStack.pushPathByName('SearchPage', { keyword: '鸿蒙' })

二、状态管理分层:从混乱到清晰

2.1 状态分层模型

大型应用最头疼的是状态管理。我们采用三层状态模型:

UI 状态层(@State / @Prop / @Link)
    ↑↓
页面状态层(Page ViewModel)
    ↑↓
全局状态层(AppStorage / 全局 ViewModel)
    ↑↓
持久化层(Preferences / RDB / 网络)

2.2 全局状态管理器

// global/AppStateManager.ets
class AppStateManager {
  // 用户登录状态
  private _isLoggedIn: boolean = false
  get isLoggedIn(): boolean { return this._isLoggedIn }
  set isLoggedIn(value: boolean) {
    this._isLoggedIn = value
    AppStorage.setOrCreate('isLoggedIn', value)
  }

  // 用户信息
  private _userInfo: UserInfo | null = null
  get userInfo(): UserInfo | null { return this._userInfo }
  set userInfo(value: UserInfo | null) {
    this._userInfo = value
    AppStorage.setOrCreate('userInfo', value)
  }

  // 主题配置
  private _themeMode: 'light' | 'dark' = 'light'
  get themeMode(): 'light' | 'dark' { return this._themeMode }
  set themeMode(value: 'light' | 'dark') {
    this._themeMode = value
    AppStorage.setOrCreate('themeMode', value)
  }

  // 初始化:从持久化加载
  async init(): Promise<void> {
    let prefs = await AppPreferences.getInstance()
    this._isLoggedIn = await prefs.getBoolean('is_logged_in', false)
    this._themeMode = await prefs.getString('theme_mode', 'light') as 'light' | 'dark'
    
    // 同步到 AppStorage
    AppStorage.setOrCreate('isLoggedIn', this._isLoggedIn)
    AppStorage.setOrCreate('themeMode', this._themeMode)
  }
}

export const appState = new AppStateManager()

2.3 页面级 ViewModel

// pages/home/HomeViewModel.ets
class HomeViewModel {
  @State newsList: NewsItem[] = []
  @State banners: BannerItem[] = []
  @State loading: boolean = false
  @State error: string = ''
  @State hasMore: boolean = true
  
  private currentPage: number = 1
  private readonly pageSize: number = 20

  async loadData(isRefresh: boolean = true): Promise<void> {
    if (this.loading) return
    
    this.loading = true
    this.error = ''
    
    if (isRefresh) {
      this.currentPage = 1
      // 并行加载 banner 和列表
      await Promise.all([
        this.loadBanners(),
        this.loadNewsList()
      ])
    } else {
      await this.loadNewsList()
    }
    
    this.loading = false
  }

  private async loadBanners(): Promise<void> {
    try {
      let data = await httpClient.get<BannerItem[]>('/home/banners')
      this.banners = data
    } catch (err) {
      console.error('加载 banner 失败:', err)
    }
  }

  private async loadNewsList(): Promise<void> {
    try {
      let data = await httpClient.get<NewsItem[]>('/news/list', {
        page: this.currentPage.toString(),
        size: this.pageSize.toString()
      })
      
      if (this.currentPage === 1) {
        this.newsList = data
      } else {
        this.newsList = [...this.newsList, ...data]
      }
      
      this.hasMore = data.length === this.pageSize
      this.currentPage++
    } catch (err) {
      this.error = err instanceof Error ? err.message : '加载失败'
    }
  }
}

export const homeViewModel = new HomeViewModel()

2.4 组件与 ViewModel 的绑定

// pages/home/HomePage.ets
@Entry
@Component
struct HomePage {
  @State newsList: NewsItem[] = homeViewModel.newsList
  @State banners: BannerItem[] = homeViewModel.banners
  @State loading: boolean = homeViewModel.loading
  @State error: string = homeViewModel.error

  aboutToAppear() {
    homeViewModel.loadData()
    
    // 监听 ViewModel 状态变化(简化示例,实际可用更优雅的响应式绑定)
    setInterval(() => {
      this.newsList = homeViewModel.newsList
      this.banners = homeViewModel.banners
      this.loading = homeViewModel.loading
      this.error = homeViewModel.error
    }, 100)
  }

  build() {
    Column() {
      // Banner 轮播
      Swiper() {
        ForEach(this.banners, (banner) => {
          Image(banner.imageUrl)
            .width('100%')
            .aspectRatio(2.5)
            .objectFit(ImageFit.Cover)
        })
      }
      .autoPlay(true)
      .indicator(true)

      // 新闻列表
      if (this.loading && this.newsList.length === 0) {
        SkeletonList({ itemCount: 5 })
      } else if (this.error) {
        ErrorState({ message: this.error, onRetry: () => homeViewModel.loadData() })
      } else {
        NewsList({ items: this.newsList })
      }
    }
  }
}

三、MVVM 模式在鸿蒙中的落地

3.1 架构分层

View 层(ArkUI 组件)
    ↓ 绑定
ViewModel 层(业务逻辑 + 状态管理)
    ↓ 调用
Repository 层(数据获取:网络/本地/缓存)
    ↓ 操作
Data Source 层(HTTP / RDB / Preferences / 分布式)

3.2 Repository 模式

// repository/NewsRepository.ets
class NewsRepository {
  private cacheManager = new CacheManager()
  private cacheKey = 'news_list'
  private cacheTTL = 3 * 60 * 1000  // 3 分钟

  async getNewsList(page: number, size: number): Promise<NewsItem[]> {
    let cacheKey = `${this.cacheKey}_${page}`
    
    // 1. 读内存缓存
    let cached = this.cacheManager.getMemory<NewsItem[]>(cacheKey)
    if (cached) return cached

    // 2. 网络请求
    let data = await httpClient.get<NewsItem[]>('/news/list', {
      page: page.toString(),
      size: size.toString()
    })

    // 3. 写入缓存
    this.cacheManager.setMemory(cacheKey, data as object, this.cacheTTL)
    
    return data
  }

  async getNewsDetail(id: string): Promise<NewsDetail> {
    return httpClient.get<NewsDetail>(`/news/detail/${id}`)
  }
}

export const newsRepository = new NewsRepository()

3.3 依赖注入简化

大型应用中手动传递依赖很繁琐,可以封装一个简单的依赖注入容器:

// di/ServiceLocator.ets
class ServiceLocator {
  private services: Map<string, object> = new Map()

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

  get<T>(key: string): T {
    let service = this.services.get(key)
    if (!service) {
      throw new Error(`Service ${key} not registered`)
    }
    return service as T
  }
}

export const serviceLocator = new ServiceLocator()

// 初始化注册
serviceLocator.register('newsRepository', newsRepository)
serviceLocator.register('userRepository', userRepository)
serviceLocator.register('appState', appState)

四、工程化最佳实践

4.1 代码规范

  • 命名规范:组件名 PascalCase(LoadingButton),文件名小写短横线(loading-button.ets),常量 UPPER_SNAKE_CASE

  • 目录组织:按功能分层,不按类型分(如 pages/home/ 下包含页面、ViewModel、样式,而非所有 ViewModel 放一起)

  • 注释规范:公共 API 必须写 JSDoc,复杂业务逻辑加行内注释

4.2 构建优化

// build-profile.json5
{
  "app": {
    "signingConfigs": [],
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compileSdkVersion": "5.0.0(12)",
        "compatibleSdkVersion": "5.0.0(12)",
        "runtimeOS": "HarmonyOS",
        "buildOption": {
          "strictMode": {
            "caseSensitiveCheck": true,
            "useNormalizedOHMUrl": true
          }
        }
      }
    ]
  }
}

4.3 测试策略

// 单元测试示例
import { describe, it, expect } from '@ohos/hypium'

describe('HomeViewModel Test', () => {
  it('loadData should set loading to true', async () => {
    let viewModel = new HomeViewModel()
    let loadPromise = viewModel.loadData()
    
    expect(viewModel.loading).assertTrue()
    
    await loadPromise
    expect(viewModel.loading).assertFalse()
  })
})

鸿蒙官方提供 @ohos/hypium 测试框架,支持单元测试和 UI 测试。

五、完整项目骨架示例

// entry/src/main/ets/entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'

export default class EntryAbility extends UIAbility {
  async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
    // 初始化全局状态
    await appState.init()
    
    // 初始化网络层
    // ...
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // 根据登录状态决定跳转页面
    let targetPage = appState.isLoggedIn ? 'pages/MainPage' : 'pages/LoginPage'
    
    windowStage.loadContent(targetPage, (err) => {
      if (err) {
        console.error('加载页面失败:', err.message)
      }
    })
  }
}

小结与系列回顾

本文作为系列的最后一篇,从架构层面总结了大型鸿蒙应用的设计要点:

  1. 模块化:entry 轻薄,业务下沉到 HSP,基础能力封装成 HAR,用 router_map 解耦路由

  2. 状态分层:UI 状态 → 页面 ViewModel → 全局状态 → 持久化,每层职责清晰

  3. MVVM 落地:Repository 处理数据,ViewModel 处理业务,View 只负责展示

  4. 工程化:代码规范、构建优化、测试覆盖,保障团队协作效率

至此,《鸿蒙开发从入门到专家》系列第 1 批(1-10 篇)全部完成。回顾一下这个系列的学习路径:

  • 初级(1-3 篇):环境搭建 → ArkTS 语法 → ArkUI 组件

  • 中级(4-7 篇):页面路由 → 数据持久化 → 网络请求 → 自定义组件

  • 高级(8-9 篇):性能优化 → 分布式能力

  • 专家(10 篇):架构设计与工程化

后续批次我们将继续深入:元服务开发、AI 能力集成(HiAI Foundation)、ArkUI-X 跨平台、以及更多实战项目案例。

参考阅读


评论