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