在 ArkTS 和 ArkUI 开发中,一个页面通常包含三部分内容:
- 页面展示;
- 用户交互;
- 数据请求和业务处理。
如果所有代码都写在一个 @Component 中,随着功能增加,组件会变得越来越臃肿。MVC、MVP 和 MVVM 的作用,就是将页面、业务和数据进行合理拆分。
本文使用同一个"登录页面"来理解这三种架构。
一、三种架构共同解决的问题
假设登录页面需要完成以下功能:
- 获取用户输入的账号和密码;
- 校验账号和密码;
- 调用登录接口;
- 显示加载状态;
- 显示登录成功或失败;
- 登录成功后跳转页面。
如果这些逻辑全部写在页面组件中:
ts
@Entry
@Component
struct LoginPage {
@State account: string = ''
@State password: string = ''
@State loading: boolean = false
@State message: string = ''
private async login(): Promise<void> {
// 参数校验
// 网络请求
// 错误处理
// 页面状态修改
// 页面跳转
}
build() {
// 页面布局
}
}
随着功能增加,LoginPage 会同时负责:
- UI 布局;
- 状态管理;
- 输入校验;
- 网络请求;
- 数据转换;
- 错误处理;
- 页面跳转。
这类组件通常被称为"胖组件"。
MVC、MVP、MVVM 的核心目标,就是让不同对象承担不同职责。
二、公共的 Model 层
无论采用哪种架构,Model 的职责基本一致。
Model 负责:
- 数据实体;
- 网络请求;
- 数据库存储;
- 业务数据处理;
- 底层服务能力。
例如:
ts
class User {
id: number
name: string
constructor(id: number, name: string) {
this.id = id
this.name = name
}
}
class LoginService {
async login(account: string, password: string): Promise<User> {
if (account === 'admin' && password === '123456') {
return new User(1, '管理员')
}
throw new Error('账号或密码错误')
}
}
LoginService 只负责完成登录业务,不关心页面中使用的是:
text
Text
Button
TextInput
LoadingProgress
Model 不应该依赖具体 UI。
三、MVC:Controller 直接协调 Model 和 View
MVC 分为:
text
Model
View
Controller
其中:
- Model:负责数据和业务能力;
- View:负责界面展示;
- Controller:接收用户事件,调用 Model,并更新 View。
在 ArkUI 中,@Component 经常同时承担 View 和 Controller 的职责:
text
build() → View
事件处理方法 → Controller
MVC 示例
ts
@Entry
@Component
struct LoginMvcPage {
@State account: string = ''
@State password: string = ''
@State loading: boolean = false
@State message: string = ''
private loginService: LoginService = new LoginService()
private async onLoginClick(): Promise<void> {
if (this.account.length === 0) {
this.message = '请输入账号'
return
}
if (this.password.length < 6) {
this.message = '密码不能少于 6 位'
return
}
this.loading = true
this.message = ''
try {
const user = await this.loginService.login(
this.account,
this.password
)
this.message = `欢迎你,${user.name}`
} catch (error) {
this.message = '账号或密码错误'
} finally {
this.loading = false
}
}
build() {
Column({ space: 16 }) {
TextInput({
placeholder: '请输入账号',
text: this.account
})
.onChange((value: string) => {
this.account = value
})
TextInput({
placeholder: '请输入密码',
text: this.password
})
.onChange((value: string) => {
this.password = value
})
Button(this.loading ? '登录中...' : '登录')
.enabled(!this.loading)
.onClick(() => {
this.onLoginClick()
})
if (this.loading) {
LoadingProgress()
}
Text(this.message)
}
.padding(20)
}
}
MVC 的数据流
text
用户点击
↓
View 触发事件
↓
Controller 处理逻辑
↓
Controller 调用 Model
↓
Model 返回结果
↓
Controller 修改页面状态
↓
View 重新渲染
可以概括为:
text
View → Controller → Model
View ← Controller ← Model
MVC 的特点
MVC 最大的特点是:
Controller 直接读取 View 的数据,也直接修改 View 的状态。
例如:
ts
this.loading = true
this.message = '登录失败'
这种方式简单直接,适合业务较少的页面。
但页面复杂后,Controller 逻辑容易全部堆积在组件中,最终形成"胖组件"。
四、MVP:Presenter 通过接口命令 View
MVP 分为:
text
Model
View
Presenter
Presenter 可以理解为从 Controller 中抽离出来的业务协调者。
它负责:
- 获取用户输入;
- 校验数据;
- 调用 Model;
- 处理业务结果;
- 命令 View 更新页面。
MVP 的核心是:
Presenter 不直接操作具体 UI,而是通过 View 接口控制页面。
定义 View 接口
ts
interface LoginViewContract {
getAccount(): string
getPassword(): string
showLoading(): void
hideLoading(): void
showMessage(message: string): void
showLoginSuccess(user: User): void
}
这个接口规定登录页面必须具备哪些能力。
Presenter 只认识这个接口,并不知道页面具体使用的是 Text、Toast 还是 Dialog。
Presenter 实现
ts
class LoginPresenter {
private view: LoginViewContract
private loginService: LoginService
constructor(
view: LoginViewContract,
loginService: LoginService
) {
this.view = view
this.loginService = loginService
}
async login(): Promise<void> {
const account = this.view.getAccount()
const password = this.view.getPassword()
if (account.length === 0) {
this.view.showMessage('请输入账号')
return
}
if (password.length < 6) {
this.view.showMessage('密码不能少于 6 位')
return
}
this.view.showLoading()
try {
const user = await this.loginService.login(account, password)
this.view.showLoginSuccess(user)
} catch (error) {
this.view.showMessage('账号或密码错误')
} finally {
this.view.hideLoading()
}
}
}
Presenter 中没有出现任何 ArkUI 控件:
text
@Component
@State
TextInput
Button
Text
因此 Presenter 可以脱离页面独立测试。
MVP 页面
ts
@Entry
@Component
struct LoginMvpPage {
@State account: string = ''
@State password: string = ''
@State loading: boolean = false
@State message: string = ''
private presenter: LoginPresenter | null = null
aboutToAppear(): void {
const view: LoginViewContract = {
getAccount: (): string => {
return this.account
},
getPassword: (): string => {
return this.password
},
showLoading: (): void => {
this.loading = true
},
hideLoading: (): void => {
this.loading = false
},
showMessage: (message: string): void => {
this.message = message
},
showLoginSuccess: (user: User): void => {
this.message = `欢迎你,${user.name}`
}
}
this.presenter = new LoginPresenter(
view,
new LoginService()
)
}
build() {
Column({ space: 16 }) {
TextInput({
placeholder: '请输入账号',
text: this.account
})
.onChange((value: string) => {
this.account = value
})
TextInput({
placeholder: '请输入密码',
text: this.password
})
.onChange((value: string) => {
this.password = value
})
Button(this.loading ? '登录中...' : '登录')
.enabled(!this.loading)
.onClick(() => {
this.presenter?.login()
})
if (this.loading) {
LoadingProgress()
}
Text(this.message)
}
.padding(20)
}
}
MVP 的数据流
text
用户点击
↓
View 将事件交给 Presenter
↓
Presenter 获取 View 输入
↓
Presenter 调用 Model
↓
Model 返回结果
↓
Presenter 调用 View 接口
↓
View 修改页面状态
↓
页面重新渲染
可以概括为:
text
View ↔ Presenter ↔ Model
MVP 的核心特征
Presenter 会调用:
ts
this.view.showLoading()
this.view.showMessage('登录失败')
this.view.showLoginSuccess(user)
这些代码表达的是:
text
View,请显示加载状态
View,请显示错误信息
View,请显示登录成功
所以 MVP 的本质是:
Presenter 输出的是行为和命令。
Presenter 虽然不认识具体控件,但它知道 View 可以执行哪些操作。
五、MVVM:ViewModel 提供页面状态
MVVM 分为:
text
Model
View
ViewModel
ViewModel 负责:
- 保存页面状态;
- 接收页面输入;
- 处理输入校验;
- 调用 Model;
- 转换页面数据;
- 提供页面可以直接使用的状态。
MVVM 的核心是:
ViewModel 不命令 View 做什么,只修改自身状态,View 根据状态重新渲染。
ViewModel 实现
ts
@Observed
class LoginViewModel {
account: string = ''
password: string = ''
loading: boolean = false
message: string = ''
private loginService: LoginService
constructor(loginService: LoginService) {
this.loginService = loginService
}
get loginEnabled(): boolean {
return this.account.length > 0 &&
this.password.length >= 6 &&
!this.loading
}
setAccount(account: string): void {
this.account = account
}
setPassword(password: string): void {
this.password = password
}
async login(): Promise<void> {
if (this.account.length === 0) {
this.message = '请输入账号'
return
}
if (this.password.length < 6) {
this.message = '密码不能少于 6 位'
return
}
this.loading = true
this.message = ''
try {
const user = await this.loginService.login(
this.account,
this.password
)
this.message = `欢迎你,${user.name}`
} catch (error) {
this.message = '账号或密码错误'
} finally {
this.loading = false
}
}
}
ViewModel 中没有:
text
Text
Button
TextInput
LoadingProgress
它只维护页面状态。
MVVM 页面
ts
@Entry
@Component
struct LoginMvvmPage {
@State viewModel: LoginViewModel =
new LoginViewModel(new LoginService())
build() {
Column({ space: 16 }) {
TextInput({
placeholder: '请输入账号',
text: this.viewModel.account
})
.onChange((value: string) => {
this.viewModel.setAccount(value)
})
TextInput({
placeholder: '请输入密码',
text: this.viewModel.password
})
.onChange((value: string) => {
this.viewModel.setPassword(value)
})
Button(
this.viewModel.loading
? '登录中...'
: '登录'
)
.enabled(this.viewModel.loginEnabled)
.onClick(() => {
this.viewModel.login()
})
if (this.viewModel.loading) {
LoadingProgress()
}
Text(this.viewModel.message)
}
.padding(20)
}
}
MVVM 的数据流
text
用户输入或点击
↓
View 调用 ViewModel
↓
ViewModel 调用 Model
↓
Model 返回结果
↓
ViewModel 修改自身状态
↓
View 读取新状态
↓
页面重新渲染
可以概括为:
text
View ↔ ViewModel ↔ Model
MVVM 的核心特征
ViewModel 只会修改:
ts
this.loading = true
this.message = '登录失败'
它不会调用:
ts
this.view.showLoading()
this.view.showMessage()
View 根据状态决定页面如何显示:
ts
if (this.viewModel.loading) {
LoadingProgress()
}
Text(this.viewModel.message)
因此 MVVM 的本质是:
ViewModel 输出的是状态和数据。
六、MVP 和 MVVM 的核心区别
MVP 和 MVVM 都把业务逻辑从页面组件中抽离,但它们与 View 的通信方式不同。
MVP:Presenter 命令 View
ts
this.view.showLoading()
this.view.showMessage('登录失败')
Presenter 的意思是:
text
View,你现在显示加载。
View,你现在显示错误。
Presenter 输出的是行为。
MVVM:ViewModel 修改状态
ts
this.loading = true
this.message = '登录失败'
ViewModel 的意思是:
text
我现在处于加载状态。
我现在有一条错误信息。
View 根据这些状态决定如何渲染。
ViewModel 输出的是状态。
因此可以记住:
text
Presenter 输出行为
ViewModel 输出状态
七、Model 和 ViewModel 的区别
Model 表示真实业务数据。
例如:
ts
class User {
firstName: string = ''
lastName: string = ''
age: number = 0
vip: boolean = false
}
这些是服务器或数据库中的原始数据。
但页面可能需要显示:
text
张三
18岁
VIP会员
ViewModel 可以将 Model 转换成适合页面直接展示的数据:
ts
class UserViewModel {
displayName: string = ''
ageText: string = ''
vipText: string = ''
update(user: User): void {
this.displayName =
`${user.firstName}${user.lastName}`
this.ageText =
`${user.age}岁`
this.vipText =
user.vip ? 'VIP会员' : '普通用户'
}
}
所以:
text
Model:真实业务数据
ViewModel:页面需要的数据和状态
八、三种架构的核心对比
| 架构 | 中间层 | 业务逻辑位置 | UI 更新方式 |
|---|---|---|---|
| MVC | Controller | Component 或 Controller | Controller 直接修改状态 |
| MVP | Presenter | Presenter | Presenter 通过接口命令 View |
| MVVM | ViewModel | ViewModel | View 根据 ViewModel 状态渲染 |
进一步理解:
| 问题 | MVC | MVP | MVVM |
|---|---|---|---|
| 谁处理点击事件 | Controller | Presenter | ViewModel |
| 谁执行输入校验 | Controller | Presenter | ViewModel |
| 谁调用 Model | Controller | Presenter | ViewModel |
| 谁修改 UI | Controller | View | View |
| 中间层输出什么 | 直接操作 | 行为、命令 | 状态、数据 |
| 是否依赖状态绑定 | 不强制 | 不强制 | 通常依赖 |
| 测试难度 | 相对较高 | 较低 | 较低 |
九、ArkTS 项目应该如何选择
简单页面使用 MVC
例如:
- 关于页面;
- 静态详情页;
- 简单设置页;
- 只有少量状态和交互的页面。
这类页面直接在 Component 中处理逻辑即可,没必要创建大量额外对象。
流程型页面适合 MVP
例如:
- 多步骤注册;
- 实名认证;
- 权限申请;
- 复杂表单提交;
- 分步骤业务流程。
这类页面经常需要明确控制 View:
text
显示下一步
隐藏当前区域
弹出错误提示
进入成功页面
Presenter 输出行为会比较直观。
状态复杂的页面适合 MVVM
例如:
- 下载页面;
- 播放器页面;
- 聊天页面;
- 购物车页面;
- 股票行情页面;
- 多状态列表页面。
这些页面通常有大量状态:
text
加载中
加载成功
加载失败
空数据
刷新中
是否可点击
下载进度
播放进度
未读数量
MVVM 可以把这些状态集中放在 ViewModel 中,由 View 声明式渲染。
十、为什么 MVVM 与 ArkUI 很契合
ArkUI 是声明式 UI。
开发者不是一步一步命令页面:
text
创建 Loading
添加 Loading
隐藏按钮
修改文字
移除 Loading
而是描述页面在某种状态下应该是什么样子:
ts
if (this.viewModel.loading) {
LoadingProgress()
}
Button('登录')
.enabled(this.viewModel.loginEnabled)
Text(this.viewModel.message)
当状态变化时,ArkUI 会重新计算和渲染相关 UI。
这与 MVVM 的状态驱动思想天然一致:
text
ViewModel 管理状态
View 描述状态对应的界面
十一、任何架构都可能变得臃肿
MVC 可能出现:
text
Massive Component
MVP 可能出现:
text
Massive Presenter
MVVM 可能出现:
text
Massive ViewModel
如果 ViewModel 同时负责:
- 网络请求;
- 数据库存储;
- JSON 解析;
- 页面跳转;
- 权限申请;
- 埋点;
- 文件下载;
- 缓存处理;
- 页面状态;
那么它同样会非常臃肿。
更合理的结构可以继续拆分为:
text
View
↓
ViewModel / Presenter
↓
UseCase / Service
↓
Repository
↓
网络和数据库
架构的重点不是类名,而是职责是否清晰。
十二、最终总结
MVC、MVP 和 MVVM 都是在解决页面代码职责混乱的问题。
MVC
text
View 触发事件
→ Controller 调用 Model
→ Controller 直接修改 View
一句话总结:
Controller 直接协调 Model 和 View。
MVP
text
View 把事件交给 Presenter
→ Presenter 调用 Model
→ Presenter 通过接口命令 View
一句话总结:
Presenter 不直接操作控件,但会命令 View 做什么。
MVVM
text
View 把输入交给 ViewModel
→ ViewModel 调用 Model
→ ViewModel 修改状态
→ View 根据状态重新渲染
一句话总结:
ViewModel 不命令 View,只提供 View 需要的状态。
最终可以压缩为三句话:
text
MVC:Controller 直接修改 View。
MVP:Presenter 通过接口命令 View。
MVVM:ViewModel 修改状态,View 根据状态自己更新。
再进一步压缩:
text
MVC = 直接协调
MVP = 行为驱动
MVVM = 状态驱动