一个基本的crud页面,状态管理应该怎么做

102 阅读6分钟

思考下:一个基本的增删改查页面(vue3版本),该怎么写:查询表单,表格,分页、创建表单、编辑表单各自封装一个组件?这样,似乎没错。但是状态怎么做,把各个状态分散在各组件中,但是汇总这些状态总感觉很麻烦,都是通过ref或者event来上下组件传递的话,也不是特别的便利。这样虽然把一个页面的各个模块拆成了一个组件,但是似乎这个组件,根本不是单独成立,这种拆分感觉意思不是很大。provideinject(以前我虽然知道,但是一直感觉没有多大用),但是这个和自定义hooks结合,仿佛打开了我的新世界大门,我用这个结合,似乎真的可以实现,组件就是纯粹的展示组件,不需要做任何状态的管理,所有的状态管理都聚焦在一个个的store中,通过provide注入,各个子组件inject调用,这样拆分之后,感觉维护性/可读性提升巨大。用一个经典的curd页面来示范下:

设计如下图

component设计图

组件设计.png

页面store设计图

页面store设计.png

表单store设计图

表单store设计.png

component和store结合图(放大观看)

组件和store结合.png

代码如下

index.vue
<template>
  <div>
    <QueryAndCreateForm />
    <TableAndEditForm />
    <Pagination />
  </div>
</template>
<script setup lang="ts">
import QueryAndCreateForm from './components/container/QueryAndCreateForm.vue'
import TableAndEditForm from './components/container/TableAndEditForm.vue'
import Pagination from './components/single/Pagination.vue'
import { providePageStore } from './store/usePageStore'
const store = providePageStore()
store.handleQuery()
</script>
<style lang="scss" scoped></style>

容器组件

QueryAndCreateForm.vue
<template>
  <div>
    <QueryFrom />
    <CreateForm />
  </div>
</template>
<script setup lang="ts">
import QueryFrom from '../single/QueryForm.vue'
import CreateForm from '../single/CreateForm.vue'
import { provideCreateStore } from '../../store/useCreateFormStore'
provideCreateStore()
</script>
TableAndEditForm.vue
<template>
  <div>
    <BusinessTable />
    <EditForm />
  </div>
</template>
<script setup lang="ts">
import BusinessTable from '../single/BusinessTable.vue'
import EditForm from '../single/EditForm.vue'
import { provideEditStore } from '../../store/useEditFormStore'
provideEditStore()
</script>

单个组件

Pagination.vue
<template>
  <div>
    <el-pagination v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize"
      :disabled="loading" :page-sizes="pageSizes" layout="total, sizes, prev, pager, next, jumper" :total="total"
      @size-change="handleSizeChange" @current-change="handleCurrentChange" />
  </div>
</template>
<script setup lang="ts">
import { injectPageStore } from '../../store/usePageStore'
import type { PageStore } from '../../store/usePageStore'
const { pagination, total, loading, pageSizes, handleCurrentChange, handleSizeChange } = injectPageStore() as PageStore
</script>

QueryForm.vue

<template>
  <el-form :inline="true" label-suffix=":" :disabled="loading">
    <el-form-item label="用户名">
      <el-input v-model="queryForm.userName" placeholder="请输入用户名" clearable />
    </el-form-item>
    <el-form-item label="ID">
      <el-input v-model="queryForm.id" placeholder="请输入ID" clearable />
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="handleFormQuery" :loading="loading">查询</el-button>
      <el-button @click="handleFormReset" :loading="loading">重置</el-button>
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="setVisible(true)" :disabled="loading">创建用户</el-button>
    </el-form-item>
  </el-form>
</template>
<script lang="ts" setup>
import { injectPageStore } from '../../store/usePageStore'
import type { PageStore } from '../../store/usePageStore'
import { injectCreateStore } from '../../store/useCreateFormStore'
const { queryForm, loading, handleFormQuery, handleFormReset } = injectPageStore() as PageStore
const { setVisible } = injectCreateStore()
</script>

表单相关组件设计

BaseForm.vue

<template>
  <el-form ref="ruleFormRef" style="max-width: 600px" :model="formData" status-icon :rules="rules" label-width="auto"
    class="demo-ruleForm">
    <el-form-item label="用户名" prop="userName">
      <el-input v-model="formData.userName" type="text" />
    </el-form-item>
  </el-form>
</template>
<script lang="ts" setup>
import { injectBaseStore } from '../../store/useBaseFormStore'
const { rules, formData, ruleFormRef } = injectBaseStore()
</script>

createForm.vue

<template>
  <el-dialog v-model="visible" title="创建用户" width="500" :before-close="handleCancel">
    <BaseForm />
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="handleCancel">取消</el-button>
        <el-button type="primary" @click="handleSubmit">
          创建
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script lang="ts" setup>
import BaseForm from '../single/BaseForm.vue'
import { injectCreateStore } from '../../store/useCreateFormStore'
const { visible, handleSubmit, handleCancel } = injectCreateStore()
</script>

EditForm.vue

<template>
  <el-dialog v-model="visible" title="创建用户" width="500" :before-close="handleCancel">
    <BaseForm />
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="handleCancel">取消</el-button>
        <el-button type="primary" @click="handleSubmit">
          更新
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script lang="ts" setup>
import BaseForm from '../single/BaseForm.vue'
import { injectEditStore } from '../../store/useEditFormStore'
const { visible, handleSubmit, handleCancel } = injectEditStore()
</script>

store实现

usePageStore.ts

import { inject, provide } from 'vue'
import { usePaginationStore } from './usePaginationStore'
import type { PaginationStore } from './usePaginationStore'
import { useQueryFormStore } from './useQueryFormStore'
import type { QueryFormStore } from './useQueryFormStore'
import { usePageQueryStore } from './usePageQueryStore'
import type { PageQueryStore } from './usePageQueryStore'

const storeKey = Symbol('key')

interface BasePageStore {
  handleQuery: () => void
  handleFormQuery: () => void
  handleFormReset: () => void
  handleSizeChange: (val: number) => void
  handleCurrentChange: (val: number) => void
}

type PaginationStoreWithOutMethod = Omit<PaginationStore, 'sizeChange' | 'currentChange'>

type QueryFormStoreWithOutMethod = Omit<QueryFormStore, 'resetForm'>

type PageQueryStoreWithOutMethod = Omit<PageQueryStore, 'query'>

export type PageStore = BasePageStore & PaginationStoreWithOutMethod & QueryFormStoreWithOutMethod & PageQueryStoreWithOutMethod

const usePageStore = (): PageStore => {

  const { queryForm, resetForm } = useQueryFormStore()

  const { pagination, pageSizes, currentChange, sizeChange } = usePaginationStore()

  const { list, loading, query, total, columns } = usePageQueryStore()

  const handleFormQuery = () => {
    currentChange(1)
    handleQuery()
  }

  const handleFormReset = () => {
    resetForm()
    handleFormQuery()
  }

  const mergeParams = () => {
    return { ...pagination.value, ...queryForm.value }
  }

  const handleSizeChange = (val: number) => {
    sizeChange(val)
    handleQuery()
  }

  const handleCurrentChange = (val: number) => {
    currentChange(val)
    handleQuery()
  }

  const handleQuery = () => {
    query(mergeParams())
  }

  return {
    list,
    total,
    columns,
    loading,
    queryForm,
    pageSizes,
    pagination,
    handleQuery,
    handleFormReset,
    handleFormQuery,
    handleSizeChange,
    handleCurrentChange
  }
}

export const providePageStore = (): PageStore => {
  const store = usePageStore()
  provide(storeKey, store)
  return store
}

export const injectPageStore = () => {
  const store = inject<PageStore>(storeKey)
  return store
}

usePageQueryStore.ts

import { ref } from 'vue'
import type { Ref } from 'vue'
import { useLoadingStore } from './useLoadingStore'
import type { LoadingStore } from './useLoadingStore'

interface Item {
  date: number
  price: number
  id: string
}

type ColumnPropName = 'operation'

type PropOfValue = keyof Item | ColumnPropName

interface Config {
  prop: PropOfValue
  label: string
  isUseSlot?: boolean
}

interface PageQuery {
  list: Ref<Array<Item>>
  total: Ref<number>
  query: <T>(val: T) => void
  columns: Array<Config>
}

type LoadingStoreWithOutMethod = Omit<LoadingStore, 'setLoading'>

export type PageQueryStore = PageQuery & LoadingStoreWithOutMethod

export const usePageQueryStore = (): PageQueryStore => {

  const total = ref(0)

  const columns: Array<Config> = [
    {
      prop: 'date',
      label: '日期'
    },
    {
      prop: 'price',
      label: '金额'
    },
    {
      prop: 'id',
      label: 'ID'
    },
    {
      prop: 'operation',
      isUseSlot: true,
      label: '操作栏'
    }
  ]

  const list = ref<Array<Item>>([])

  const { loading, setLoading } = useLoadingStore()

  const setTotal = (val: number) => total.value = val

  const mockData = (length = 10) => {
    return Array(length).fill(1).map(() => {
      return {
        date: new Date().getTime(),
        price: Math.random() * 100,
        id: crypto.randomUUID()
      }
    })
  }

  const query = <T>(params: T) => {
    console.log('params', params)
    setLoading(true)
    setTimeout(() => {
      list.value = mockData()
      setTotal(120)
      setLoading(false)
    }, 1500)
  }

  return {
    list,
    columns,
    total,
    loading,
    query,
  }
}

useLoading.ts

import { ref } from 'vue'
import type { Ref } from 'vue'

export interface LoadingStore {
  loading: Ref<boolean>
  setLoading: (val: boolean) => void
}

export const useLoadingStore = (defaultValue = false): LoadingStore => {

  const loading = ref(defaultValue)

  const setLoading = (val: boolean) => loading.value = val
  
  return {
    loading,
    setLoading
  }
}

usePaginationStore.ts

import { ref } from 'vue'
import type { Ref } from 'vue'

interface Pagination {
  currentPage: number
  pageSize: number
}

export interface PaginationStore {
  pagination: Ref<Pagination>
  sizeChange: (val: number) => void
  currentChange: (val: number) => void
  pageSizes: number[]
}

export const usePaginationStore = (): PaginationStore => {
  const pagination = ref({
    currentPage: 1,
    pageSize: 10
  })

  const pageSizes = [10, 20, 30, 40]

  const sizeChange = (val: number) => {
    pagination.value.pageSize = val
  }

  const currentChange = (val: number) => {
    pagination.value.currentPage = val
  }

  return {
    pagination,
    sizeChange,
    currentChange,
    pageSizes
  }
}

useQueryFormStore.ts

import { ref } from 'vue'
import type { Ref } from 'vue'

interface QueryForm {
  userName: string
  id: string
}

type FormKey = keyof QueryForm

export interface QueryFormStore {
  queryForm: Ref<QueryForm>
  resetForm: () => void
}

export const useQueryFormStore = (): QueryFormStore => {

  const queryForm = ref<QueryForm>({
    userName: '',
    id: '',
  })

  const resetForm = () => {
    Object.keys(queryForm.value).forEach((key) => {
      queryForm.value[key as FormKey] = ''
    })
  }

  return {
    queryForm,
    resetForm
  }
}

useBaseFormStore.ts

import { inject, ref } from 'vue'
import type { Ref } from 'vue'
import type { FormItemRule, FormInstance } from 'element-plus'
import type { ValidateFieldsError } from 'async-validator'
import _ from 'lodash'

export const storeKey = Symbol('key')

interface FormData {
  userName: string
}

interface CallBack {
  (isValid: boolean): void
}

type FormDataKey = keyof FormData

type Rules = Record<FormDataKey, FormItemRule>

export interface BaseFormStore {
  ruleFormRef: Ref<FormInstance | undefined>
  formData: Ref<FormData>
  rules: Rules | Array<Rules>
  validateForm: (cb: CallBack) => void
  resetForm: () => void
  setFormData: (formData: FormData) => void
}

export const useBaseFormStore = (): BaseFormStore => {

  const ruleFormRef = ref<FormInstance>()

  const formData = ref<FormData>({
    userName: '',
  })

  const rules: Rules = {
    userName: { required: true, message: '请输入名字', trigger: 'blur' }
  }

  const setFormData = (value: FormData) => {
    formData.value = value
  }

  const validateForm = (callBack: CallBack) => {
    if (ruleFormRef.value) {
      ruleFormRef.value.validate((isValid: boolean, invalidFields?: ValidateFieldsError) => {
        if (!isValid) {
          const filed = _.first(_.keys(invalidFields))
          if (filed) ruleFormRef.value?.scrollToField(filed)
        }
        callBack(isValid)
      })
    }
  }

  const resetForm = () => {
    if (ruleFormRef.value) ruleFormRef.value.resetFields()
  }

  return {
    validateForm,
    resetForm,
    ruleFormRef,
    formData,
    setFormData,
    rules
  }
}

export const injectBaseStore = () => {
  const store = inject<BaseFormStore>(storeKey) as BaseFormStore
  return store
}

useCreateFormStore.ts

import { inject, provide, ref } from 'vue'
import type { Ref } from 'vue'
import { useBaseFormStore, storeKey } from './useBaseFormStore'
import type { BaseFormStore } from './useBaseFormStore'
import { useLoadingStore } from './useLoadingStore'
import type { LoadingStore } from './useLoadingStore'
import { ElMessage } from 'element-plus'

interface CreateForm {
  visible: Ref<boolean>
  setVisible: (val: boolean) => void
  handleSubmit: () => void
  handleCancel: () => void
}

type LoadingStoreWithOutMethod = Omit<LoadingStore, 'setLoading'>

type BaseFormStoreWithOutMethod = Omit<BaseFormStore, 'validateForm' | 'setFormData' | 'resetForm'>

type CreateFormStore = CreateForm & BaseFormStoreWithOutMethod & LoadingStoreWithOutMethod

export const useCreateFormStore = (): CreateFormStore => {

  const visible = ref(false)

  const { validateForm,
    resetForm,
    ruleFormRef,
    formData,
    rules } = useBaseFormStore()

  const { loading, setLoading } = useLoadingStore()

  const setVisible = (val: boolean) => visible.value = val

  const handleSubmit = () => {
    validateForm((isValid) => {
      if (isValid) {
        setLoading(true)
        setTimeout(() => {
          console.log('formData', formData.value)
          setLoading(false)
          resetForm()
          setVisible(false)
          ElMessage.success('创建成功,看控制台,表单信息有打印')
          // 如果需要刷新列表,使用usePageStore下的inject注入,拿到store调用handleQuery即可
          // 这里纯粹mock,懒得写了
        }, 1500)
      }
    })
  }

  const handleCancel = () => {
    resetForm()
    setVisible(false)
  }

  return {
    ruleFormRef,
    formData,
    rules,
    loading,
    visible,
    setVisible,
    handleSubmit,
    handleCancel,
  }
}

export const provideCreateStore = (): CreateFormStore => {
  const store = useCreateFormStore()
  provide(storeKey, store)
  return store
}

export const injectCreateStore = () => {
  const store = inject<CreateFormStore>(storeKey) as CreateFormStore
  return store
}

useEditFormStore.ts

import { inject, provide, ref } from 'vue'
import type { Ref } from 'vue'
import { useBaseFormStore, storeKey } from './useBaseFormStore'
import type { BaseFormStore } from './useBaseFormStore'
import { useLoadingStore } from './useLoadingStore'
import type { LoadingStore } from './useLoadingStore'
import { ElMessage } from 'element-plus'

interface EditForm {
  visible: Ref<boolean>
  handleSubmit: () => void
  handleCancel: () => void
  getDetailById: (id: string) => void
}

type LoadingStoreWithOutMethod = Omit<LoadingStore, 'setLoading'>

type BaseFormStoreWithOutMethod = Omit<BaseFormStore, 'validateForm' | 'setFormData' | 'resetForm'>

type EditFormStore = EditForm & BaseFormStoreWithOutMethod & LoadingStoreWithOutMethod

export const useEditFormStore = (): EditFormStore => {

  const visible = ref(false)

  const { validateForm,
    resetForm,
    ruleFormRef,
    formData,
    setFormData,
    rules } = useBaseFormStore()

  const { loading, setLoading } = useLoadingStore()

  const setVisible = (val: boolean) => visible.value = val

  const getDetailById = (id: string) => {
    console.log(id)
    setTimeout(() => {
      setFormData({
        userName: '12'
      })
      setVisible(true)
    }, 1500);
  }

  const handleSubmit = () => {
    validateForm((isValid) => {
      if (isValid) {
        setLoading(true)
        setTimeout(() => {
          console.log('formData', formData.value)
          setLoading(false)
          resetForm()
          setVisible(false)
          ElMessage.success('编辑成功,看控制台,表单信息有打印')
          // 如果需要刷新列表,使用usePageStore下的inject注入,拿到store调用handleQuery即可
          // 这里纯粹mock,懒得写了
        }, 1500)
      }
    })
  }

  const handleCancel = () => {
    resetForm()
    setVisible(false)
  }

  return {
    getDetailById,
    ruleFormRef,
    formData,
    rules,
    loading,
    visible,
    handleSubmit,
    handleCancel
  }
}

export const provideEditStore = (): EditFormStore => {
  const store = useEditFormStore()
  provide(storeKey, store)
  return store
}

export const injectEditStore = () => {
  const store = inject<EditFormStore>(storeKey) as EditFormStore
  return store
}

总结:这样拆分下来,虽然写了好多个文件,但是可维护性和可读性,提升的太高了,每一个store/componet,看起来巨简单,很想立马开一个新项目来验证下这种思路,欢迎指正,感谢!!