系列文章《鸿蒙开发从入门到专家》第 10 篇(专家篇)
前言
前面九篇文章,我们从环境搭建走到了分布式开发,掌握了鸿蒙应用开发的完整技术栈。但掌握技术点和搭建一个能支撑百万用户、几十人团队协作的大型应用,中间还隔着一道架构设计的鸿沟。
很多团队在鸿蒙项目上踩过这样的坑:一开始把所有代码塞进 entry 模块,三个月后代码量破十万行,编译一次要五分钟,改一行代码牵一发而动全身,新成员入职两周还不敢提交代码。本文从工程化角度,给出一套经过验证的鸿蒙大型应用架构方案。
一、模块化拆分:HAP、HSP、HAR 的分工
1.1 三种模块类型
鸿蒙提供了三种模块形态,理解它们的区别是架构设计的基础:
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)
}
})
}
}
小结与系列回顾
本文作为系列的最后一篇,从架构层面总结了大型鸿蒙应用的设计要点:
模块化:entry 轻薄,业务下沉到 HSP,基础能力封装成 HAR,用
router_map解耦路由状态分层:UI 状态 → 页面 ViewModel → 全局状态 → 持久化,每层职责清晰
MVVM 落地:Repository 处理数据,ViewModel 处理业务,View 只负责展示
工程化:代码规范、构建优化、测试覆盖,保障团队协作效率
至此,《鸿蒙开发从入门到专家》系列第 1 批(1-10 篇)全部完成。回顾一下这个系列的学习路径:
初级(1-3 篇):环境搭建 → ArkTS 语法 → ArkUI 组件
中级(4-7 篇):页面路由 → 数据持久化 → 网络请求 → 自定义组件
高级(8-9 篇):性能优化 → 分布式能力
专家(10 篇):架构设计与工程化
后续批次我们将继续深入:元服务开发、AI 能力集成(HiAI Foundation)、ArkUI-X 跨平台、以及更多实战项目案例。