ArkTS中的MVC、MVP、MVVM

在 ArkTS 和 ArkUI 开发中,一个页面通常包含三部分内容:

  • 页面展示;
  • 用户交互;
  • 数据请求和业务处理。

如果所有代码都写在一个 @Component 中,随着功能增加,组件会变得越来越臃肿。MVC、MVP 和 MVVM 的作用,就是将页面、业务和数据进行合理拆分。

本文使用同一个"登录页面"来理解这三种架构。


一、三种架构共同解决的问题

假设登录页面需要完成以下功能:

  1. 获取用户输入的账号和密码;
  2. 校验账号和密码;
  3. 调用登录接口;
  4. 显示加载状态;
  5. 显示登录成功或失败;
  6. 登录成功后跳转页面。

如果这些逻辑全部写在页面组件中:

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 = 状态驱动
相关推荐
特立独行的猫A1 小时前
Node.js 三方库移植到 OpenHarmony 鸿蒙PC:一篇实操指南
harmonyos
达子6661 小时前
第16章_HarmonyOs开发图解之 音频
华为·音视频·harmonyos
程序员黑豆2 小时前
鸿蒙应用开发之双向绑定实战:从 V1 到 V2 的完整迁移指南
前端·harmonyos
molihuan5 小时前
最新 spine 4.3 flutter 适配鸿蒙
flutter·动画·harmonyos·鸿蒙·spine
qizayaoshuap5 小时前
# 鸿蒙 HarmonyOS 应用开发实战(第33期)|喝水提醒(Water Reminder)— 可视化水杯与进度动画
华为·harmonyos
程序员黑豆5 小时前
鸿蒙应用开发之父子组件传参:@Param、@Event、@Once 装饰器详解与实战
前端·harmonyos
FF2501_940228586 小时前
HarmonyOS应用开发实战:猫猫大作战-AVPlayer 音视频播放
harmonyos·鸿蒙
达子6668 小时前
第17章_HarmonyOs开发图解之 媒体会话管理
华为·harmonyos·媒体
A富得流油的咸鸭蛋9 小时前
安卓鸿蒙面试
android·面试·harmonyos