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