Vue 中的 MVVM、MVC 和 MVP:现代前端架构模式深度解析
前言:架构模式的演变之旅
在 Vue 开发中,我们经常听到 MVVM、MVC 这些术语,但它们到底意味着什么?为什么 Vue 选择了 MVVM?这些模式如何影响我们的代码结构?今天,让我们抛开教科书式的定义,从实际 Vue 开发的角度,深入探讨这些架构模式的本质区别。
一、MVC:经典的王者(但已不再适合前端)
1.1 MVC 的核心三要素
javascript
// 模拟一个传统的 MVC 结构(不是 Vue,但可以帮助理解)
class UserModel {
constructor() {
this.users = []
this.currentUser = null
}
addUser(user) {
this.users.push(user)
}
setCurrentUser(user) {
this.currentUser = user
}
}
class UserView {
constructor(controller) {
this.controller = controller
this.userList = document.getElementById('user-list')
this.userForm = document.getElementById('user-form')
// 手动绑定事件
this.userForm.addEventListener('submit', (e) => {
e.preventDefault()
const name = document.getElementById('name').value
const email = document.getElementById('email').value
this.controller.addUser({ name, email })
})
}
renderUsers(users) {
this.userList.innerHTML = users.map(user =>
`<li>${user.name} (${user.email})</li>`
).join('')
}
}
class UserController {
constructor(model) {
this.model = model
this.view = new UserView(this)
}
addUser(userData) {
this.model.addUser(userData)
this.view.renderUsers(this.model.users)
}
}
// 使用
const app = new UserController(new UserModel())
1.2 MVC 在 Vue 中的"遗迹"
虽然 Vue 不是 MVC,但我们能看到 MVC 的影子:
vue
<!-- 这种写法有 MVC 的影子 -->
<template>
<!-- View:负责展示 -->
<div>
<h1>{{ title }}</h1>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
<button @click="loadUsers">加载用户</button>
</div>
</template>
<script>
export default {
data() {
return {
// Model:数据状态
title: '用户列表',
users: []
}
},
methods: {
// Controller:业务逻辑
async loadUsers() {
try {
const response = await fetch('/api/users')
this.users = await response.json()
} catch (error) {
console.error('加载失败', error)
}
}
}
}
</script>
MVC 的关键问题在前端:
- 视图和控制器紧密耦合:DOM 操作和业务逻辑混杂
- 双向依赖:视图依赖控制器,控制器也依赖视图
- 状态管理困难:随着应用复杂,状态散落在各处
二、MVP:试图改进的中间者
2.1 MVP 的核心改进
javascript
// 一个 MVP 模式的示例
class UserModel {
constructor() {
this.users = []
}
fetchUsers() {
return fetch('/api/users').then(r => r.json())
}
}
class UserView {
constructor() {
this.userList = document.getElementById('user-list')
this.loadButton = document.getElementById('load-btn')
}
bindLoadUsers(handler) {
this.loadButton.addEventListener('click', handler)
}
displayUsers(users) {
this.userList.innerHTML = users.map(user =>
`<li>${user.name}</li>`
).join('')
}
showLoading() {
this.userList.innerHTML = '<li>加载中...</li>'
}
}
class UserPresenter {
constructor(view, model) {
this.view = view
this.model = model
// Presenter 初始化时绑定事件
this.view.bindLoadUsers(() => this.onLoadUsers())
}
async onLoadUsers() {
this.view.showLoading()
try {
const users = await this.model.fetchUsers()
this.view.displayUsers(users)
} catch (error) {
console.error('加载失败', error)
}
}
}
// 使用
const view = new UserView()
const model = new UserModel()
new UserPresenter(view, model)
2.2 MVP 的特点
- Presenter 作为中间人:协调 View 和 Model
- View 被动:只负责显示,不包含业务逻辑
- 解耦更好:View 和 Model 不知道彼此存在
- 但仍有问题:Presenter 可能变得臃肿,测试仍复杂
三、MVVM:Vue 的选择与实现
3.1 MVVM 的核心:数据绑定
vue
<!-- 这是典型的 MVVM,Vue 自动处理了绑定 -->
<template>
<!-- View:声明式模板 -->
<div class="user-management">
<input
v-model="newUser.name"
placeholder="用户名"
@keyup.enter="addUser"
>
<button @click="addUser">添加用户</button>
<ul>
<li v-for="user in filteredUsers" :key="user.id">
{{ user.name }}
<button @click="removeUser(user.id)">删除</button>
</li>
</ul>
<input v-model="searchQuery" placeholder="搜索用户...">
</div>
</template>
<script>
export default {
data() {
return {
// Model/ViewModel:响应式数据
users: [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
],
newUser: { name: '' },
searchQuery: ''
}
},
computed: {
// ViewModel:派生状态
filteredUsers() {
return this.users.filter(user =>
user.name.toLowerCase().includes(this.searchQuery.toLowerCase())
)
}
},
methods: {
// ViewModel:操作方法
addUser() {
if (this.newUser.name.trim()) {
this.users.push({
id: Date.now(),
name: this.newUser.name.trim()
})
this.newUser.name = ''
}
},
removeUser(id) {
this.users = this.users.filter(user => user.id !== id)
}
}
}
</script>
3.2 Vue 如何实现 MVVM
让我们看看 Vue 的底层实现:
javascript
// 简化的 Vue 响应式系统
class Vue {
constructor(options) {
this.$options = options
this._data = options.data()
// 1. 数据劫持(核心)
this.observe(this._data)
// 2. 编译模板
this.compile(options.template)
}
observe(data) {
Object.keys(data).forEach(key => {
let value = data[key]
const dep = new Dep() // 依赖收集
Object.defineProperty(data, key, {
get() {
// 收集依赖
if (Dep.target) {
dep.addSub(Dep.target)
}
return value
},
set(newVal) {
if (newVal !== value) {
value = newVal
// 通知更新
dep.notify()
}
}
})
})
}
compile(template) {
// 将模板转换为渲染函数
// 建立 View 和 ViewModel 的绑定
}
}
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => sub.update())
}
}
// Watcher 观察数据变化
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
Dep.target = this
this.value = vm._data[key] // 触发 getter,收集依赖
Dep.target = null
}
update() {
const newValue = this.vm._data[this.key]
if (newValue !== this.value) {
this.value = newValue
this.cb(newValue)
}
}
}
四、三种模式的深度对比
4.1 通信流对比
graph TD
subgraph "MVC"
A[View] -->|用户输入| B[Controller]
B -->|更新| C[Model]
C -->|通知| B
B -->|渲染| A
end
subgraph "MVP"
D[View] -->|委托| E[Presenter]
E -->|更新| F[Model]
F -->|返回数据| E
E -->|更新视图| D
end
subgraph "MVVM"
G[View] <-->|双向绑定| H[ViewModel]
H -->|操作| I[Model]
I -->|响应数据| H
end
4.2 代码结构对比
vue
<!-- 同一个功能,三种模式的不同实现 -->
<!-- MVC 风格(不推荐) -->
<template>
<div>
<input id="username" type="text">
<button id="save-btn">保存</button>
<div id="output"></div>
</div>
</template>
<script>
export default {
mounted() {
// Controller 逻辑散落在各处
document.getElementById('save-btn').addEventListener('click', () => {
const username = document.getElementById('username').value
this.saveUser(username)
})
},
methods: {
saveUser(username) {
// Model 操作
this.$store.commit('SET_USERNAME', username)
// View 更新
document.getElementById('output').textContent = `用户: ${username}`
}
}
}
</script>
<!-- MVP 风格 -->
<template>
<div>
<input v-model="username" type="text">
<button @click="presenter.save()">保存</button>
<div>{{ displayText }}</div>
</div>
</template>
<script>
export default {
data() {
return {
username: '',
displayText: ''
}
},
created() {
// Presenter 处理所有逻辑
this.presenter = {
save: () => {
this.$store.commit('SET_USERNAME', this.username)
this.displayText = `用户: ${this.username}`
}
}
}
}
</script>
<!-- MVVM 风格(Vue 原生) -->
<template>
<div>
<!-- 双向绑定自动处理 -->
<input v-model="username" type="text">
<button @click="saveUser">保存</button>
<!-- 自动响应式更新 -->
<div>用户: {{ username }}</div>
</div>
</template>
<script>
export default {
data() {
return {
username: ''
}
},
methods: {
saveUser() {
// 数据改变,视图自动更新
this.$store.commit('SET_USERNAME', this.username)
}
}
}
</script>
4.3 实际项目中的体现
javascript
// 一个真实的 Vuex + Vue 项目结构
// Model 层:Vuex Store
// store/modules/user.js
export default {
state: {
users: [],
currentUser: null
},
mutations: {
SET_USERS(state, users) {
state.users = users
},
ADD_USER(state, user) {
state.users.push(user)
}
},
actions: {
async fetchUsers({ commit }) {
const users = await api.getUsers()
commit('SET_USERS', users)
}
},
getters: {
activeUsers: state => state.users.filter(u => u.isActive)
}
}
// ViewModel 层:Vue 组件
// UserList.vue
<template>
<!-- View:声明式模板 -->
<div>
<UserFilter @filter-change="setFilter" />
<UserTable :users="filteredUsers" />
<UserPagination
:current-page="currentPage"
@page-change="changePage"
/>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex'
export default {
data() {
return {
// 组件本地状态
currentPage: 1,
filter: ''
}
},
computed: {
// 连接 Model (Vuex) 和 View
...mapState('user', ['users']),
...mapGetters('user', ['activeUsers']),
// ViewModel:计算属性
filteredUsers() {
return this.activeUsers.filter(user =>
user.name.includes(this.filter)
)
}
},
methods: {
...mapActions('user', ['fetchUsers']),
// ViewModel:方法
setFilter(filter) {
this.filter = filter
this.currentPage = 1 // 重置分页
},
changePage(page) {
this.currentPage = page
this.fetchUsers({ page, filter: this.filter })
}
},
created() {
this.fetchUsers()
}
}
</script>
五、Vue 3 组合式 API:MVVM 的进化
5.1 传统 Options API 的问题
vue
<!-- Options API:逻辑分散 -->
<script>
export default {
data() {
return {
users: [],
filter: '',
page: 1
}
},
computed: {
filteredUsers() { /* ... */ }
},
watch: {
filter() { /* 过滤逻辑 */ },
page() { /* 分页逻辑 */ }
},
methods: {
fetchUsers() { /* ... */ },
handleFilter() { /* ... */ }
},
mounted() {
this.fetchUsers()
}
}
</script>
5.2 组合式 API:更好的逻辑组织
vue
<!-- Composition API:逻辑聚合 -->
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
// 用户搜索功能
const {
users,
searchUsers,
isLoading: usersLoading
} = useUserSearch()
// 分页功能
const {
currentPage,
pageSize,
paginatedData,
changePage
} = usePagination(users)
// 筛选功能
const {
filter,
filteredData,
setFilter
} = useFilter(paginatedData)
// 生命周期
onMounted(() => {
searchUsers()
})
// 响应式监听
watch(filter, () => {
currentPage.value = 1
})
</script>
<template>
<!-- View 保持不变 -->
<div>
<input v-model="filter" placeholder="搜索...">
<UserTable :data="filteredData" />
<Pagination
:current-page="currentPage"
@change="changePage"
/>
</div>
</template>
5.3 自定义组合函数
javascript
// composables/useUserManagement.js
import { ref, computed } from 'vue'
import { useUserStore } from '@/stores/user'
export function useUserManagement() {
const userStore = useUserStore()
const localUsers = ref([])
const filter = ref('')
const currentPage = ref(1)
const pageSize = 10
// 计算属性:ViewModel
const filteredUsers = computed(() => {
return localUsers.value.filter(user =>
user.name.toLowerCase().includes(filter.value.toLowerCase())
)
})
const paginatedUsers = computed(() => {
const start = (currentPage.value - 1) * pageSize
return filteredUsers.value.slice(start, start + pageSize)
})
// 方法:ViewModel
const addUser = (user) => {
localUsers.value.push(user)
userStore.addUser(user)
}
const removeUser = (id) => {
localUsers.value = localUsers.value.filter(u => u.id !== id)
}
return {
// 暴露给 View
users: paginatedUsers,
filter,
currentPage,
addUser,
removeUser,
setFilter: (value) => { filter.value = value }
}
}
六、现代 Vue 生态中的架构模式
6.1 Pinia:更现代的"Model"层
javascript
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
users: [],
currentUser: null
}),
actions: {
async fetchUsers() {
const { data } = await api.get('/users')
this.users = data
},
addUser(user) {
this.users.push(user)
}
},
getters: {
activeUsers: (state) => state.users.filter(u => u.isActive),
userCount: (state) => state.users.length
}
})
// 组件中使用
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const { users, activeUsers } = storeToRefs(userStore)
// MVVM 清晰分层:
// Model: userStore
// ViewModel: 组件中的 computed/methods
// View: template
</script>
6.2 基于特性的架构
bash
src/
├── features/
│ ├── user/
│ │ ├── components/ # View
│ │ ├── composables/ # ViewModel
│ │ ├── stores/ # Model
│ │ └── types/ # 类型定义
│ └── product/
│ ├── components/
│ ├── composables/
│ └── stores/
├── shared/
│ ├── components/
│ ├── utils/
│ └── api/
└── App.vue
6.3 服务器状态管理(TanStack Query)
vue
<script setup>
import { useQuery, useMutation } from '@tanstack/vue-query'
// Model:服务器状态
const { data: users, isLoading } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
// ViewModel:本地状态和逻辑
const filter = ref('')
const filteredUsers = computed(() => {
return users.value?.filter(u =>
u.name.includes(filter.value)
) || []
})
// Mutation:修改服务器状态
const { mutate: addUser } = useMutation({
mutationFn: createUser,
onSuccess: () => {
// 自动重新获取 users
}
})
</script>
<template>
<!-- View -->
<div>
<input v-model="filter" placeholder="搜索用户">
<UserList :users="filteredUsers" />
</div>
</template>
七、如何选择合适的模式
7.1 决策矩阵
| 场景 | 推荐模式 | 理由 | Vue 实现 |
|---|---|---|---|
| 小型项目 | MVVM(Vue 原生) | 简单直接,上手快 | Options API |
| 中型项目 | MVVM + 状态管理 | 需要共享状态 | Vuex/Pinia |
| 大型项目 | 组合式 MVVM | 逻辑复用,类型安全 | Composition API + TypeScript |
| 复杂业务逻辑 | 领域驱动设计 | 业务逻辑复杂 | 特性文件夹 + Clean Architecture |
| 实时应用 | MVVM + 响应式增强 | 需要复杂响应式 | Vue + RxJS/Signals |
7.2 架构演进示例
javascript
// 阶段1:简单 MVVM(适合 todo 应用)
export default {
data() {
return { todos: [], newTodo: '' }
},
methods: {
addTodo() {
this.todos.push({ text: this.newTodo, done: false })
this.newTodo = ''
}
}
}
// 阶段2:加入状态管理(适合电商网站)
// store/todos.js + TodoList.vue + TodoItem.vue
// 阶段3:组合式架构(适合 SaaS 平台)
// features/todo/
// ├── useTodoList.js
// ├── useTodoFilter.js
// ├── TodoStore.js
// └── components/
// 阶段4:微前端架构(适合大型企业应用)
// app-todo/ + app-user/ + app-order/ + 主应用
7.3 代码质量检查清单
javascript
// 好的 MVVM 代码应该:
// 1. View(模板)保持简洁,只负责展示
<template>
<!-- ✅ 好:声明式 -->
<button @click="handleSubmit">提交</button>
<!-- ❌ 不好:包含逻辑 -->
<button @click="validate() && submit()">提交</button>
</template>
// 2. ViewModel(脚本)处理所有逻辑
<script>
export default {
methods: {
// ✅ 好:逻辑在 ViewModel
handleSubmit() {
if (this.validate()) {
this.submit()
}
},
// ❌ 不好:直接操作 DOM
badMethod() {
document.getElementById('btn').disabled = true
}
}
}
</script>
// 3. Model(数据)清晰分层
// ✅ 好:状态管理集中
state: {
users: [], // 原始数据
ui: { // UI 状态
loading: false,
error: null
}
}
// ❌ 不好:状态混杂
data() {
return {
apiData: [], // API 数据
isLoading: false, // UI 状态
localData: {} // 本地状态
}
}
八、总结:Vue 架构模式的核心要义
8.1 三种模式的本质区别
| 模式 | 核心思想 | Vue 中的体现 | 适用场景 |
|---|---|---|---|
| MVC | 关注点分离,但耦合度高 | 早期 jQuery 时代 | 传统后端渲染 |
| MVP | Presenter 中介,View 被动 | 某些 Vue 2 项目 | 需要严格测试 |
| MVVM | 数据绑定,自动同步 | Vue 核心设计 | 现代前端应用 |
8.2 Vue 为什么选择 MVVM?
- 开发效率:数据绑定减少样板代码
- 维护性:响应式系统自动处理更新
- 可测试性:ViewModel 可以独立测试
- 渐进式:可以从简单开始,逐步复杂化
8.3 现代 Vue 开发的最佳实践
- 拥抱 MVVM:理解并善用响应式系统
- 合理分层 :
- View:只负责展示,尽量简单
- ViewModel:处理业务逻辑和状态
- Model:管理数据和业务规则
- 组合优于继承:使用组合式 API 组织代码
- 状态管理:在需要时引入 Pinia/Vuex
- 关注点分离:按特性组织代码,而非技术
8.4 记住的关键点
- Vue 不是严格的 MVVM,但受其启发
- 架构模式是工具,不是教条,根据项目选择
- 代码组织比模式名称更重要
- 渐进式是 Vue 的核心优势,可以从简单开始
最后,无论你使用哪种模式,记住 Vue 的核心原则:让开发者专注于业务逻辑,而不是框架细节。这才是 Vue 成功的真正原因。
思考题:在你的 Vue 项目中,你是如何组织代码的?有没有遇到过架构选择上的困惑?或者有什么独特的架构实践想要分享?欢迎在评论区交流讨论!