1. 鸿蒙应用代码结构设计的必要性
作为一名长期从事跨端开发的工程师,我见过太多因为初期结构设计不当而导致后期维护困难的项目。鸿蒙应用开发虽然相对较新,但同样面临着代码组织的问题。很多开发者(包括早期的我自己)在快速实现功能时,往往会忽略代码结构的合理性,等到项目规模扩大后才追悔莫及。
在鸿蒙应用开发中,合理的代码结构至少能带来三个显著优势:
-
可维护性提升:当业务逻辑变更时,你能够快速定位到需要修改的代码位置,而不必在数千行的单体文件中大海捞针。
-
团队协作效率:清晰的模块边界让多个开发者可以同时工作在不同模块上,减少代码冲突和沟通成本。
-
可测试性增强:分离的架构使得单元测试、集成测试更容易实施,特别是业务逻辑与UI分离后,可以单独测试核心算法而不必启动完整应用。
2. 初学者常见的不良结构模式
2.1 典型的"大杂烩"式结构
很多鸿蒙新手项目会呈现这样的目录结构:
code复制entry
├─ pages
│ ├─ Home.ets
│ ├─ Login.ets
│ └─ Profile.ets
│
├─ components
│
└─ utils
这种结构在小demo中尚可接受,但一旦业务复杂度上升,很快就会暴露出严重问题。我曾接手过一个采用类似结构的项目,其中单个页面文件竟然超过了3000行代码,包含了从UI渲染到网络请求、数据处理、业务逻辑等所有内容,维护起来简直是噩梦。
2.2 混合架构的核心问题
让我们看一个典型的混合实现案例:
typescript复制@Entry
@Component
struct HomePage {
@State list: string[] = []
aboutToAppear() {
this.loadData()
}
async loadData() {
const result = await fetch("https://api.example.com/list")
const data = await result.json()
this.list = data
}
build() {
Column() {
ForEach(this.list, (item: string) => {
Text(item)
})
}
}
}
这段代码至少存在三个架构问题:
-
职责混杂:页面组件同时承担了数据获取、数据处理和UI渲染三种职责,违反了单一职责原则。
-
可测试性差:由于网络请求直接嵌入在UI组件中,无法在不渲染UI的情况下测试数据加载逻辑。
-
复用困难:如果其他页面也需要相同的数据获取逻辑,只能复制粘贴代码,导致重复和维护困难。
3. 推荐的分层架构设计
3.1 基础分层结构
经过多个项目的实践验证,我推荐采用以下基础结构:
code复制entry
├─ pages
├─ components
├─ services
├─ models
└─ utils
各层级的职责划分如下:
| 层级 | 职责 | 示例内容 |
|---|---|---|
| pages | 页面入口 | 只包含页面框架和路由 |
| components | 可复用UI组件 | 按钮、列表项、卡片等 |
| services | 业务逻辑 | 用户服务、订单服务等 |
| models | 数据模型 | 接口定义、DTO等 |
| utils | 工具函数 | 日期格式化、字符串处理等 |
3.2 Service层的实现细节
业务逻辑应该封装在Service层中。以用户服务为例:
typescript复制import http from '@ohos.net.http'
export class UserService {
private static instance: UserService
private constructor() {}
public static getInstance(): UserService {
if (!UserService.instance) {
UserService.instance = new UserService()
}
return UserService.instance
}
async getUserList(): Promise<User[]> {
const request = http.createHttp()
try {
const response = await request.request(
"https://api.example.com/users",
{
method: http.RequestMethod.GET,
connectTimeout: 60000,
readTimeout: 60000
}
)
if (response.responseCode === 200) {
return JSON.parse(response.result)
} else {
throw new Error(`HTTP ${response.responseCode}`)
}
} catch (error) {
console.error("获取用户列表失败:", error)
throw error
}
}
}
这个实现有几个值得注意的点:
-
单例模式:使用单例确保全局只有一个服务实例,避免重复创建消耗资源。
-
错误处理:完善的错误处理机制,包括网络错误和业务错误。
-
超时设置:明确设置了连接和读取超时,避免请求长时间挂起。
-
类型安全:返回明确的Promise<User[]>类型,方便调用方处理。
3.3 页面层的精简实现
使用Service层后,页面组件可以大幅简化:
typescript复制import { UserService } from '../services/UserService'
import { User } from '../models/UserModel'
@Entry
@Component
struct UserPage {
@State users: User[] = []
@State loading: boolean = true
@State error: string | null = null
private userService = UserService.getInstance()
aboutToAppear() {
this.loadUsers()
}
async loadUsers() {
try {
this.loading = true
this.users = await this.userService.getUserList()
this.error = null
} catch (err) {
this.error = "加载用户列表失败"
console.error(err)
} finally {
this.loading = false
}
}
build() {
Column() {
if (this.loading) {
LoadingIndicator()
} else if (this.error) {
ErrorView(this.error)
} else {
UserList({ users: this.users })
}
}
}
}
这个页面组件现在只关注三件事:
-
状态管理:管理加载状态、错误状态和数据状态。
-
生命周期:在适当的生命周期钩子中触发数据加载。
-
UI渲染:根据不同状态渲染对应的UI。
4. 数据模型的规范化管理
4.1 模型定义的最佳实践
在models目录中,我们应该为每种业务实体定义清晰的接口:
typescript复制// models/UserModel.ets
export interface User {
id: number
name: string
email: string
avatar: string
createdAt: string
updatedAt: string
}
export interface UserListResponse {
data: User[]
total: number
page: number
pageSize: number
}
好的模型定义应该:
-
完整:包含所有必要的字段,即使当前不用。
-
精确:使用合适的类型(如number而不是any)。
-
分层:区分基础模型和响应DTO。
4.2 模型转换的实用技巧
在实际项目中,API返回的数据格式往往与前端需要的格式不一致。我推荐在Service层进行转换:
typescript复制async getFormattedUserList(): Promise<FormattedUser[]> {
const response = await this.getUserList()
return response.data.map(user => ({
id: user.id,
fullName: `${user.firstName} ${user.lastName}`,
avatarUrl: this.resolveAvatarUrl(user.avatar),
joinDate: new Date(user.createdAt).toLocaleDateString()
}))
}
这种转换的好处是:
-
UI层简化:UI组件直接使用处理好的数据,不需要额外的格式化逻辑。
-
一致性:相同的数据转换逻辑集中管理,避免散落在各处。
-
可维护性:当API变更时,只需修改一处转换逻辑。
5. 组件化设计的进阶实践
5.1 智能组件与木偶组件
在大型项目中,我通常将组件分为两类:
-
智能组件:也称为容器组件,负责数据获取和状态管理。
-
木偶组件:纯UI组件,只通过props接收数据和回调。
例如,一个智能用户列表组件:
typescript复制@Component
export struct SmartUserList {
@State users: User[] = []
private userService = UserService.getInstance()
aboutToAppear() {
this.loadUsers()
}
async loadUsers() {
this.users = await this.userService.getUserList()
}
build() {
Column() {
DumbUserList({ users: this.users })
}
}
}
对应的木偶组件:
typescript复制@Component
export struct DumbUserList {
@Prop users: User[]
build() {
List() {
ForEach(this.users, (user) => {
ListItem() {
UserItem({ user })
}
})
}
}
}
这种分离使得:
-
测试更容易:木偶组件只需验证渲染逻辑。
-
复用性更高:同一木偶组件可以被不同智能组件使用。
-
职责更清晰:每个组件只做一件事。
5.2 组件通信模式
对于组件间通信,鸿蒙提供了多种方式:
-
Props向下传递:父组件向子组件传递数据。
-
自定义事件向上传递:子组件通过事件通知父组件。
-
全局状态管理:对于跨组件通信,可以使用AppStorage或自定义状态管理。
我建议的通信原则是:
尽量使用props和事件进行直接通信,只有在真正需要全局状态时才使用状态管理。
6. 大型项目的模块化拆分
6.1 功能模块化结构
当项目规模扩大时,基础结构需要进一步细化:
code复制entry
├─ modules
│ ├─ user
│ │ ├─ pages
│ │ ├─ components
│ │ └─ services
│ │
│ ├─ order
│ │ ├─ pages
│ │ ├─ components
│ │ └─ services
│ │
│ └─ product
│ ├─ pages
│ ├─ components
│ └─ services
│
├─ core
│ ├─ models
│ ├─ utils
│ └─ store
│
└─ app.ets
这种结构的优势在于:
-
功能内聚:相关代码组织在一起,减少跨目录引用。
-
独立开发:不同模块可以由不同团队并行开发。
-
按需加载:鸿蒙支持模块的按需加载,优化启动性能。
6.2 跨模块通信方案
模块化后,跨模块通信成为需要解决的问题。我常用的方案有:
-
接口抽象:定义共享接口,避免直接依赖具体实现。
-
事件总线:使用全局事件系统进行松耦合通信。
-
依赖注入:通过容器管理服务依赖。
例如,使用事件总线实现模块通信:
typescript复制// core/event-bus.ets
class EventBus {
private listeners: Map<string, Function[]> = new Map()
on(event: string, callback: Function) {
if (!this.listeners.has(event)) {
this.listeners.set(event, [])
}
this.listeners.get(event)?.push(callback)
}
emit(event: string, ...args: any[]) {
this.listeners.get(event)?.forEach(cb => cb(...args))
}
}
export const eventBus = new EventBus()
// order/services/OrderService.ets
eventBus.emit('orderCreated', orderDetails)
// user/pages/UserDashboard.ets
eventBus.on('orderCreated', (order) => {
this.updateUserOrderCount()
})
7. 状态管理的架构选择
7.1 何时需要状态管理
对于状态管理,我的经验法则是:
-
小型项目:使用组件本地状态和简单的props传递即可。
-
中型项目:可以引入AppStorage进行简单的全局状态共享。
-
大型复杂项目:考虑使用专门的状态管理库,如Redux模式实现。
7.2 自定义状态管理实现
下面是一个简单的Redux-like状态管理实现:
typescript复制// core/store/user-store.ets
type UserState = {
currentUser: User | null
loading: boolean
}
type UserAction =
| { type: 'LOGIN_START' }
| { type: 'LOGIN_SUCCESS', user: User }
| { type: 'LOGIN_FAILURE' }
class UserStore {
private state: UserState = {
currentUser: null,
loading: false
}
private subscribers: Function[] = []
getState() {
return this.state
}
dispatch(action: UserAction) {
switch (action.type) {
case 'LOGIN_START':
this.state = { ...this.state, loading: true }
break
case 'LOGIN_SUCCESS':
this.state = { currentUser: action.user, loading: false }
break
case 'LOGIN_FAILURE':
this.state = { ...this.state, loading: false }
break
}
this.notify()
}
subscribe(callback: Function) {
this.subscribers.push(callback)
return () => {
this.subscribers = this.subscribers.filter(cb => cb !== callback)
}
}
private notify() {
this.subscribers.forEach(cb => cb(this.state))
}
}
export const userStore = new UserStore()
使用时可以这样连接组件:
typescript复制@Component
struct UserProfile {
@State userState = userStore.getState()
private unsubscribe?: () => void
aboutToAppear() {
this.unsubscribe = userStore.subscribe(state => {
this.userState = state
})
}
onPageHide() {
this.unsubscribe?.()
}
build() {
Column() {
if (this.userState.loading) {
LoadingIndicator()
} else if (this.userState.currentUser) {
UserCard({ user: this.userState.currentUser })
} else {
LoginButton()
}
}
}
}
8. AI集成项目的特殊考量
8.1 AI服务层的设计
对于集成AI能力的应用,我建议增加专门的AI层:
code复制entry
├─ ai
│ ├─ services
│ │ ├─ ChatService.ets
│ │ └─ VisionService.ets
│ │
│ ├─ managers
│ │ ├─ PromptManager.ets
│ │ └─ ModelManager.ets
│ │
│ └─ routers
│ └─ AIRouter.ets
AI服务层的典型实现:
typescript复制// ai/services/ChatService.ets
export class ChatService {
private conversationId: string = ''
private history: ChatMessage[] = []
async sendMessage(message: string): Promise<ChatResponse> {
const prompt = PromptManager.getChatPrompt(message, this.history)
try {
const response = await ModelManager.chatCompletion({
model: 'gpt-4',
messages: prompt,
temperature: 0.7
})
this.history.push(
{ role: 'user', content: message },
{ role: 'assistant', content: response.answer }
)
return response
} catch (error) {
console.error('Chat error:', error)
throw new Error('AI服务暂时不可用')
}
}
}
8.2 AI与业务层的集成
将AI能力集成到业务逻辑中时,要注意:
-
错误隔离:AI服务可能不稳定,需要有降级方案。
-
性能考量:AI调用通常较慢,需要优化用户体验。
-
成本控制:特别是按token计费的API,需要管理调用频率。
一个集成了AI的商品推荐服务示例:
typescript复制// services/ProductRecommendationService.ets
export class ProductRecommendationService {
private chatService = new ChatService()
private productService = new ProductService()
async getPersonalizedRecommendations(userId: string): Promise<Product[]> {
// 先尝试从缓存获取
const cached = this.getCachedRecommendations(userId)
if (cached) return cached
try {
// 获取用户历史行为
const history = await this.productService.getUserHistory(userId)
// 生成AI推荐
const prompt = `基于以下用户历史购买记录,推荐5个相关商品:
${JSON.stringify(history)}`
const response = await this.chatService.sendMessage(prompt)
const products = this.parseAIResponse(response)
// 验证推荐商品是否存在
const validProducts = await this.validateProducts(products)
// 缓存结果
this.cacheRecommendations(userId, validProducts)
return validProducts
} catch (error) {
console.error('AI推荐失败,使用默认推荐', error)
return this.getDefaultRecommendations()
}
}
}
9. 代码结构设计的黄金法则
经过多个鸿蒙项目的实践,我总结了以下必须遵守的结构设计原则:
-
严格分层:UI组件不应该直接访问网络或数据库,业务逻辑不应该包含UI代码。
-
单向数据流:数据应该沿着单一方向流动(如Service → Store → Page → Component)。
-
明确依赖:模块间依赖应该清晰可见,避免隐式耦合。
-
适度抽象:不要过度设计,但必要的抽象(如Service接口)可以显著提高可维护性。
-
一致性:整个项目保持统一的结构和命名约定。
在实际项目中,我通常会先绘制简单的架构图,明确各层的职责和交互方式,然后再开始编码。这种方法虽然前期花费一些时间,但能避免后期的重构痛苦。
10. 性能优化的结构考量
好的代码结构本身就能带来性能优势:
-
按需加载:模块化结构天然支持动态导入和懒加载。
-
渲染优化:合理的组件拆分可以减少不必要的重新渲染。
-
内存管理:清晰的生命周期管理可以避免内存泄漏。
例如,对于重型组件:
typescript复制@Component
struct HeavyComponent {
@State data: HeavyData | null = null
aboutToAppear() {
this.loadData()
}
onPageHide() {
// 明确释放资源
this.data = null
}
async loadData() {
this.data = await HeavyDataLoader.load()
}
build() {
Column() {
if (this.data) {
// 只在数据就绪时渲染重型内容
HeavyRenderer({ data: this.data })
}
}
}
}
11. 测试友好的结构设计
可测试性应该是代码结构设计的重要考量:
-
依赖注入:通过构造函数注入依赖,便于测试时替换。
-
纯函数:尽可能使用纯函数实现业务逻辑,便于单元测试。
-
接口抽象:依赖接口而非具体实现,方便mock。
例如,一个可测试的服务实现:
typescript复制export interface IUserRepository {
getUsers(): Promise<User[]>
}
export class UserService {
constructor(private userRepo: IUserRepository) {}
async getActiveUsers(): Promise<User[]> {
const users = await this.userRepo.getUsers()
return users.filter(u => u.isActive)
}
}
// 测试时
class MockUserRepo implements IUserRepository {
async getUsers(): Promise<User[]> {
return [
{ id: 1, name: 'Test', isActive: true },
{ id: 2, name: 'Test2', isActive: false }
]
}
}
const testService = new UserService(new MockUserRepo())
const activeUsers = await testService.getActiveUsers()
// 断言结果包含1个用户
12. 团队协作的结构规范
在团队开发中,我建议制定并强制执行以下规范:
-
目录结构公约:统一规定各层级的命名和位置。
-
文件命名规则:如Page后缀用Page,Component后缀用Component。
-
导入路径规范:统一使用相对路径或alias。
-
代码分割标准:明确规定何时应该拆分新文件。
例如,我们的团队规范可能包含:
code复制// 好的实践
import { UserService } from '@services/user'
import UserList from '../components/UserList'
// 不好的实践
import { UserService } from '../../../../services/UserService'
import UserList from './components/list'
13. 渐进式架构演进策略
对于已有项目,我推荐采用渐进式重构策略:
-
识别热点:先重构最常修改、问题最多的部分。
-
建立边界:在新旧代码间建立清晰边界。
-
逐步替换:随着功能开发逐步迁移到新结构。
-
保持兼容:确保重构过程中现有功能不受影响。
例如,重构大型页面可以这样做:
- 先将业务逻辑提取到新Service
- 然后拆分出子组件
- 最后引入状态管理
14. 工具与自动化支持
好的工具可以强化好的结构:
-
代码生成:模板生成标准结构文件。
-
静态分析:ESLint规则检查架构违规。
-
依赖图:生成可视化模块依赖图。
-
自动化测试:架构变更时自动运行测试。
例如,配置ESLint规则确保Service不被UI直接导入:
javascript复制// .eslintrc.js
module.exports = {
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['*/pages/*'],
message: '页面不应直接导入Service,请通过容器组件注入'
}
]
}
]
}
}
15. 持续演进与学习
鸿蒙生态仍在快速发展,架构模式也需要持续演进。我建议:
-
关注官方更新:新的API可能带来更好的架构选择。
-
学习社区实践:其他开发者的经验非常宝贵。
-
定期反思:每个版本后回顾架构决策,持续改进。
-
平衡创新与稳定:不要盲目追求新技术,但也要适时升级。
我在实际项目中发现,保持架构的适度灵活性能更好地适应需求变化。每次重大功能迭代前,我都会重新评估当前结构是否仍然适用,必要时进行调整。