前言
在后台管理系统的世界里,如果说前端是"观音菩萨",那么CRUD系统就是最基础的"手掌"。本文通过解剖一个完整的Vue3用户管理系统(CRUD系统),揭示其背后的设计哲学和通用模式。
简而言之,后台管理系统就是玩好Table/Modal/Form
1. CRUD系统的四层架构
1.1 经典分层模式
一个完整的CRUD系统可以分解为四个清晰的层次:
scss
┌─────────────────────────────────────┐
│ UI 层 (表格/模态框/表单) │ ← 用户交互入口
├─────────────────────────────────────┤
│ 业务逻辑层 (Composables) │ ← 状态管理和流程控制
├─────────────────────────────────────┤
│ 操作与验证层 (Utils/Services) │ ← 数据操作和验证规则
├─────────────────────────────────────┤
│ 数据模型层 (Types) │ ← 数据结构定义
└─────────────────────────────────────┘
1.2 为什么需要分层?
单一职责原则的实践:
- UI层 只关心如何展示数据
- 业务逻辑层 只关心状态的变化
- 操作层 只关心数据如何被修改
- 模型层 只定义数据的形状
typescript
// ❌ 反面教材:所有逻辑混在一起
const handleSave = async () => {
// 验证
if (!editForm.username) alert("不能为空")
if (!editForm.email.includes("@")) alert("邮箱格式错误")
// 业务逻辑
const newUser = {
id: Math.max(...users.map(u => u.id)) + 1,
...editForm
}
// 状态更新
users.value.push(newUser)
// UI 更新
drawer.value = false
message.success("成功")
}
// ✅ 分层后的做法
const handleSave = async () => {
if (!validateForm(editForm)) return // 验证层
const newUser = createUser(editForm, users) // 操作层
addUser(newUser) // 业务逻辑层
closeDrawer() // UI 层
}
1.3 这种分层的好处
| 维度 | 单层 | 分层 |
|---|---|---|
| 代码行数 | 593 | 400+ (分散) |
| 最大复杂度 | 15+ | 3 |
| 单元测试覆盖 | 低 | 高 |
| 代码复用 | 困难 | 容易 |
| 维护成本 | 高 | 低 |
| 新增功能 | 改多处 | 改一处 |
2. 数据驱动的视图层
2.1 表格组件的本质
表格组件的核心职责只有一个:展示数据。
typescript
// src/components/UserTable.vue
defineProps<{
data: User[];
}>();
const emit = defineEmits<{
edit: [user: User];
clone: [user: User];
delete: [user: User];
selectionChange: [val: User[]];
}>();
关键特点:
- ✅ 不持有任何状态(无状态组件)
- ✅ 通过 props 接收数据
- ✅ 通过 emit 发送事件
- ✅ 完全可复用
2.2 单向数据流的重要性
scss
数据源 (Composable 中的 ref)
↓
组件 props (只读)
↓
组件渲染
↓
用户交互
↓
emit 事件
↓
Composable 处理
↓
更新数据源 (重新渲染)
为什么这样设计?
- 可预测性 - 数据流向清晰,易于调试
- 可组合性 - 组件可以任意嵌套组合
- 可测试性 - 给定相同的 props,输出必然相同
- 可维护性 - 改动某个组件不会影响其他
2.3 表单与模态框的绑定
vue
<UserFormDrawer
:visible="drawerVisible"
:model-value="editForm"
:title="drawerTitle"
@update:visible="handleDrawerClose"
@update:model-value="Object.assign(editForm, $event)"
@confirm="handleConfirm"
/>
这种双向绑定的实现方式:
typescript
// 1. 父组件维护状态
const drawerVisible = ref(false)
const editForm = reactive({...})
// 2. 组件通过 v-model 通知变化
// @update:visible 相当于
drawerVisible.value = val
// 3. 组件通过事件通知业务
@confirm="handleConfirm"
本质上就是:
- 状态在父组件(单一数据源)
- 组件是无状态的演员
- 事件是通信的桥梁
3. 状态管理的艺术
3.1 三层状态管理
一个完整的 CRUD 系统需要管理三类状态:
数据状态 - 真实数据
typescript
// useUserList.ts
const users = ref<User[]>(JSON.parse(...)); // 列表数据
特点:
- 来自 API 或数据库
- 是系统的"源头"
- 所有其他状态都基于它
UI状态 - 页面交互状态
typescript
// useUserForm.ts
const drawerVisible = ref(false); // 抽屉是否显示
const isEditMode = ref(false); // 是否编辑模式
const editForm = reactive({...}); // 编辑中的表单数据
特点:
- 仅影响 UI 显示
- 不持久化
- 随时可重置
查询状态 - 筛选和分页
typescript
// useUserList.ts
const filterForm = reactive({ // 筛选条件
username: "",
department: "",
status: ""
});
const currentPage = ref(1); // 当前页码
const pageSize = ref(10); // 每页数量
特点:
- 是数据状态的视图
- 通过 computed 实时计算
- 不存储原始数据,只存储筛选参数
3.2 计算属性的妙用
typescript
// 状态层次:原始数据 → 筛选 → 分页
const filteredUsers = computed(() => {
return users.value.filter((user) => {
const usernameMatch = user.username.includes(filterForm.username);
const departmentMatch =
!filterForm.department || user.department === filterForm.department;
const statusMatch =
!filterForm.status || user.status === filterForm.status;
return usernameMatch && departmentMatch && statusMatch;
});
});
const paginatedUsers = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return filteredUsers.value.slice(start, end);
});
为什么用 computed 而不是 methods?
| 特性 | computed | methods |
|---|---|---|
| 缓存 | 有,性能好 | 无,每次调用都计算 |
| 依赖跟踪 | 自动 | 需要手动管理 |
| 响应式 | 自动更新 | 需要手动调用 |
| 副作用 | 不允许 | 允许(不推荐) |
3.3 状态的生命周期
typescript
// 1. 初始化 - 创建空状态
const editForm = reactive({
id: 0,
username: "",
email: "",
// ...
})
// 2. 加载 - 从数据源填充
const openEditDrawer = (user: User) => {
editForm.id = user.id
editForm.username = user.username
// ...
}
// 3. 使用 - UI 绑定并修改
// <el-input v-model="editForm.username" />
// 4. 提交 - 转换为业务数据
const handleConfirm = () => {
const newUser = createUser(editForm, users)
// ...
}
// 5. 重置 - 清空状态
const closeDrawer = () => {
resetForm()
drawerVisible.value = false
}
4. 业务逻辑的隔离
4.1 业务逻辑的三个层次
第一层:Composable 层
职责:协调业务流程
typescript
export const useUserOperations = (users: any) => {
const addUser = (editForm: EditForm) => {
const newUser = createUser(editForm, users.value)
users.value.push(newUser)
ElMessage.success("用户已添加")
}
return { addUser }
}
特点:
- 知道业务的整个流程
- 调用工具函数
- 处理 UI 反馈(消息、确认对话框等)
第二层:工具函数层
职责:执行具体的数据操作
typescript
export const createUser = (
editForm: EditForm,
users: User[]
): User => {
return {
id: generateNewUserId(users),
username: editForm.username,
email: editForm.email,
// ...
createTime: getTodayDateString()
}
}
特点:
- 不知道 Vue、React 等框架
- 不知道 UI 层
- 纯函数,无副作用
- 容易测试
第三层:验证层
职责:验证数据的有效性
typescript
export const validateForm = (editForm: EditForm): boolean => {
const validations = [
{ condition: !validateRequired(editForm.username),
message: "用户名不能为空" },
{ condition: !validateEmail(editForm.email),
message: "邮箱格式不正确" },
// ...
]
for (const validation of validations) {
if (validation.condition) {
ElMessage.error(validation.message)
return false
}
}
return true
}
4.2 业务操作的通用流程
所有 CRUD 操作遵循相同的模式:
arduino
验证 → 转换 → 执行 → 更新 → 反馈
// 创建
验证表单 → 转换为 User 对象 → 加入列表 → 重置表单 → 显示成功消息
// 更新
验证表单 → 转换为 User 对象 → 替换列表中的对象 → 关闭抽屉 → 显示成功消息
// 删除
确认 → 查找要删除的对象 → 从列表中移除 → 重置选择 → 显示成功消息
// 查询
获取筛选条件 → 遍历数据 → 匹配条件 → 返回结果 → UI 自动显示
代码体现:
typescript
// 统一的增删改模式
const CRUD = {
// Create
add: (form: EditForm) => {
if (!validateForm(form)) return
const entity = createEntity(form)
entities.value.push(entity)
message.success("创建成功")
},
// Update
update: (form: EditForm) => {
if (!validateForm(form)) return
const index = findIndex(form.id)
entities.value[index] = updateEntity(form)
message.success("更新成功")
},
// Delete
delete: (id: number) => {
confirm("确定删除?").then(() => {
entities.value = entities.value.filter(e => e.id !== id)
message.success("删除成功")
})
},
// Retrieve
list: computed(() => {
return entities.value.filter(e => matches(e, filters))
})
}
5. 验证机制的设计
5.1 验证的三个维度
1. 字段级验证
单个字段的有效性
typescript
export const validateEmail = (email: string): boolean => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
export const validatePhone = (phone: string): boolean => {
return /^1[3-9]\d{9}$/.test(phone)
}
特点:
- 独立的验证规则
- 可复用
- 容易测试
2. 表单级验证
多个字段的组合有效性
typescript
export const validateForm = (editForm: EditForm): boolean => {
const validations = [
{
condition: !validateRequired(editForm.username),
message: "用户名不能为空"
},
{
condition: !validateEmail(editForm.email),
message: "邮箱格式不正确"
},
{
condition: !validatePhone(editForm.phone),
message: "请输入有效的手机号码"
},
]
for (const validation of validations) {
if (validation.condition) {
ElMessage.error(validation.message)
return false
}
}
return true
}
特点:
- 聚合多个验证
- 一处维护所有规则
- 清晰的错误提示
3. 业务级验证
业务规则的检查
typescript
// 示例:检查用户名是否重复
const validateUsernameDuplicate = (
username: string,
users: User[],
excludeId?: number
): boolean => {
return !users.some(u =>
u.username === username &&
(!excludeId || u.id !== excludeId)
)
}
特点:
- 跨越多条数据
- 需要查询数据库或列表
- 可以在创建/更新时调用
5.2 验证的执行时机
markdown
用户输入 → 失焦验证 → 实时反馈
↓
提交 → 表单验证 → 阻止提交
↓
服务器 → 业务验证 → 返回错误
代码实现:
vue
<!-- 失焦时验证 -->
<el-input
v-model="editForm.email"
@blur="(e) => {
if (!validateEmail(e.target.value)) {
error = '邮箱格式不正确'
}
}"
/>
<!-- 提交时验证 -->
<el-button @click="() => {
if (!validateForm(editForm)) return
handleConfirm()
}">
保存
</el-button>
6. 通用模式和可复用方案
6.1 可复用的模式库
模式 1:列表管理 Composable
typescript
export const useEntityList = <T extends { id: number }>(
fetchData: () => Promise<T[]>
) => {
const list = ref<T[]>([])
const filterConditions = reactive({...})
const currentPage = ref(1)
const pageSize = ref(10)
const filteredList = computed(() => {
return list.value.filter(item => matchesFilter(item))
})
const paginatedList = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return filteredList.value.slice(start, start + pageSize.value)
})
onMounted(async () => {
list.value = await fetchData()
})
return {
list,
filteredList,
paginatedList,
filterConditions,
currentPage,
pageSize
}
}
使用方式:
typescript
// 用户管理
const users = useEntityList<User>(fetchUsers)
// 订单管理
const orders = useEntityList<Order>(fetchOrders)
// 产品管理
const products = useEntityList<Product>(fetchProducts)
模式 2:表单管理 Composable
typescript
export const useEntityForm = <T>(defaultValue: T) => {
const dialogVisible = ref(false)
const formData = reactive(defaultValue)
const isEditMode = ref(false)
const open = (data?: T) => {
if (data) {
Object.assign(formData, data)
isEditMode.value = true
} else {
Object.assign(formData, defaultValue)
isEditMode.value = false
}
dialogVisible.value = true
}
const close = () => {
dialogVisible.value = false
Object.assign(formData, defaultValue)
}
return {
dialogVisible,
formData,
isEditMode,
open,
close
}
}
模式 3:增删改操作 Composable
typescript
export const useEntityCRUD = <T extends { id: number }>(
list: Ref<T[]>,
validators: { validate: (data: T) => boolean }
) => {
const create = (newData: T) => {
if (!validators.validate(newData)) return
list.value.push(newData)
ElMessage.success("创建成功")
}
const update = (id: number, newData: Partial<T>) => {
if (!validators.validate(newData)) return
const index = list.value.findIndex(item => item.id === id)
if (index !== -1) {
Object.assign(list.value[index], newData)
ElMessage.success("更新成功")
}
}
const delete = (id: number) => {
ElMessageBox.confirm("确定删除?").then(() => {
list.value = list.value.filter(item => item.id !== id)
ElMessage.success("删除成功")
})
}
const batchDelete = (ids: number[]) => {
const idSet = new Set(ids)
list.value = list.value.filter(item => !idSet.has(item.id))
ElMessage.success(`已删除 ${ids.length} 条数据`)
}
return { create, update, delete, batchDelete }
}
6.2 从具体到抽象
具体应用 → 抽象模式 → 通用框架
─────────────────────────────────────────────────────────
用户管理 → 列表管理 → useEntityList
订单管理 → 列表管理 → useEntityList
产品管理 → 列表管理 → useEntityList
用户编辑 → 表单管理 → useEntityForm
订单编辑 → 表单管理 → useEntityForm
产品编辑 → 表单管理 → useEntityForm
用户增删改 → CRUD操作 → useEntityCRUD
订单增删改 → CRUD操作 → useEntityCRUD
产品增删改 → CRUD操作 → useEntityCRUD
7. 从表象到本质
7.1 CRUD 系统的本质
一个 CRUD 系统的本质是:状态机
scss
[空状态]
↓ (初始化数据)
[数据加载完成]
↓ (用户交互)
[显示列表]
↓ (点击编辑)
[打开编辑框]
↓ (填写表单)
[修改中]
↓ (提交)
[验证中]
↓ (成功)
[关闭编辑框,更新列表]
↓ (显示成功消息)
[回到显示列表]
7.2 CRUD 系统的四大支柱
scss
┌─────────────────────────────────────────────────┐
│ 1. 数据模型(Model) │
│ 定义什么是"用户" │
│ interface User { │
│ id: number │
│ username: string │
│ email: string │
│ // ... │
│ } │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 2. 状态管理(State) │
│ 存储"有多少个用户" │
│ const users = ref<User[]>([]) │
│ const filterForm = reactive({...}) │
│ const editForm = reactive({...}) │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 3. 业务逻辑(Logic) │
│ 操作"用户如何增删改" │
│ addUser(user) │
│ updateUser(id, user) │
│ deleteUser(id) │
│ queryUsers(filter) │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 4. 用户界面(View) │
│ 展示"用户列表、编辑框" │
│ <UserTable :data="users" /> │
│ <UserForm v-model="editForm" /> │
│ <el-pagination /> │
└─────────────────────────────────────────────────┘
7.3 CRUD 系统的通用套路
所有 CRUD 系统都遵循同一个套路,只是细节不同:
javascript
// 通用流程
const CRUD = {
// 初始化
init: async () => {
items = await api.fetchList()
},
// 列表页
list: {
show: () => display(items),
filter: (condition) => items.filter(condition),
paginate: (page, size) => slice(page, size),
sort: (field, order) => sort(items, field, order)
},
// 新增
create: {
open: () => form.reset(),
submit: (data) => {
if (!validate(data)) return
api.create(data).then(id => {
items.push({...data, id})
message.success()
})
}
},
// 编辑
update: {
open: (id) => form.load(items[id]),
submit: (id, data) => {
if (!validate(data)) return
api.update(id, data).then(() => {
items[id] = data
message.success()
})
}
},
// 删除
delete: {
confirm: (id) => {
confirm("确定删除?").then(() => {
api.delete(id).then(() => {
items = items.filter(i => i.id !== id)
message.success()
})
})
}
}
}
8. 最佳实践总结
8.1 设计原则
scss
╔════════════════════════════════════╗
║ 1. 单一职责原则 (SRP) ║
║ 每个模块做好一件事 ║
╚════════════════════════════════════╝
╔════════════════════════════════════╗
║ 2. 依赖倒置原则 (DIP) ║
║ 高层不依赖低层,都依赖抽象 ║
╚════════════════════════════════════╝
╔════════════════════════════════════╗
║ 3. 开闭原则 (OCP) ║
║ 对扩展开放,对修改关闭 ║
╚════════════════════════════════════╝
╔════════════════════════════════════╗
║ 4. 接口隔离原则 (ISP) ║
║ 使用小的、专用的接口 ║
╚════════════════════════════════════╝
8.2 实现检查清单
- 数据模型 清晰定义,Types 集中管理
- 状态管理 使用 Composable,不在组件中混合业务逻辑
- 业务逻辑 在 Composable 和 Utils 中,与 UI 分离
- 验证规则 统一管理,支持复用
- 组件设计 无状态组件,通过 props 和 emit 通信
- 错误处理 验证失败有友好提示,操作异常有回滚
- 用户反馈 操作后有清晰的成功/失败消息
8.3 性能优化
typescript
// ✅ 使用 computed 缓存计算结果
const filteredUsers = computed(() => {
return users.value.filter(...)
})
// ✅ 使用 reactive 而不是多个 ref
const filterForm = reactive({...})
// ✅ 使用 Set 提高查询性能
const userIds = new Set(users.map(u => u.id))
// ✅ 虚拟滚动处理大列表
<el-virtual-scroll>
<UserTable :data="paginatedUsers" />
</el-virtual-scroll>
9. 结语
9.1 核心洞察
CRUD 系统虽然看起来简单,但其背后是数十年软件工程实践的精华:
- 分层设计解决复杂度
- 单一职责提高可维护性
- 状态机降低认知负担
- 通用模式提高开发效率
总结
这不仅是一篇技术文章,更是一份对软件设计思想的探讨。希望通过解剖 CRUD 这个"小麻雀",能帮助小伙伴们理解更大的系统设计思想。