透过现象看本质:CRUD系统的架构设计

前言

在后台管理系统的世界里,如果说前端是"观音菩萨",那么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 处理
    ↓
更新数据源 (重新渲染)

为什么这样设计?

  1. 可预测性 - 数据流向清晰,易于调试
  2. 可组合性 - 组件可以任意嵌套组合
  3. 可测试性 - 给定相同的 props,输出必然相同
  4. 可维护性 - 改动某个组件不会影响其他

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 这个"小麻雀",能帮助小伙伴们理解更大的系统设计思想。

相关推荐
song150265372982 小时前
PLC控制编程,触摸屏程序开发设计解析
开发语言·javascript·ecmascript
Mintopia2 小时前
☁️ Cloud Code 模型演进的优势:从“本地编译”到“云端智能协作”
前端·人工智能·aigc
Mintopia2 小时前
🤖 AIGC与人类协作:Web内容生产的技术分工新范式
前端·javascript·aigc
顾安r2 小时前
11.11 脚本网页 跳棋
前端·javascript·游戏·flask·html
拉拉拉拉拉拉拉马2 小时前
HTML 快速入门指南
前端·html
万少3 小时前
记第一次鸿蒙应用上架之旅:一场略带遗憾的旅途
前端·harmonyos
鹏多多3 小时前
H5开发避坑!解决Safari浏览器的video会覆盖z-index:1的绝对定位元素
前端·javascript·vue.js
恋猫de小郭3 小时前
来了解一下,为什么你的 Flutter WebView 在 iOS 26 上有点击问题?
android·前端·flutter
charlie1145141913 小时前
CSS学习笔记5:CSS 盒模型 & Margin 注意事项
前端·css·笔记·学习·教程