一、前言
大家好,今天来聊聊vue3中如何使自定义hook及如何对组件进行封装,下面我将基于elememnt-plus组件库,封装一个支持分页和过滤数据的表格组件,主要通过组合式api的方式,对table表格的一些公共逻辑组合成一个useTable钩子函数,再对相关组件进行灵活封装,以适应更多需求的变化。
关键字:vue3组合式api、ts、hook钩子函数、组件封装。
二、分页表格hook函数封装
下面通过一个未作任何封装的用户数据查看页面,进行分析,逐步拆分组件和提取公共属性和方法。
1.未作封装时的页面
我们可以从中提取一些每个页面都会重写一遍的属性或方法进行封装成useTable函数:
(1)数据列表:list
(2)总数:total
(3)当前页索引:pageIndex
(4)每页条目数量:pageSize
(5)是否正在获取数据:loading
(6)获取数据方法: getList
一个基础的分页表格用过必备以上内容,所以先将其提取到hook函数当中,其中获取数据方法是不确定的,所以需求使用useTable方法时,通过参数的方式传递进来进行初始化。除此之外list的数据类型还不能确定,所以可通过泛型的方式,在函数调用时进行传递。
2.useTable钩子函数实现
为了保持灵活性,初始化时默认不考虑过滤参数,而是在获取数据方法参数进行传递,因为进入页面时一般都不会有过滤选项,如果有需要,可自行在初始化时进行扩展。
note: 你可能也注意到了获取数据时对filterOptions参数使用toValue()方法进行了解构,这个方法是vue3.3+新增的api,可以将值、refs 或 getters 规范化为值。这与 unref() 类似,不同的是此函数也会规范化 getter 函数。如果参数是一个 getter,它将会被调用并且返回它的返回值。
想了解更多关于信息可参考:toValue()官方文档
3.使用useTable前后对比
使用useTable时
未使用useTable时
通过以上对比可以明显看到使用useTable后的页面简洁了许多,当需要再添加一个页面时,不用再重复定义多个属性,只需几行代码直接引入即可,直接减少了代码的冗余。
4.Table表格组件封装
二次封装为了样式统一性及更好的维护。
5.Pagination分页组件封装
二次封装为了样式统一性及更好的维护。
三、数据过滤表单封装
数据展示则少不了数据过滤这一环节,所以可以封装一个过滤表单,鉴于需要对过滤表单进行重置,所以可将表单的ref实例放到组件内部,这样也不用每个页面都重写表单重置方法了。组件外部只处理又过滤表单触发的事件即可。
FilterForm表单组件封装
FilterForm表单使用
由于query表单信息每个页面是不固定的,所以在页面中维护即可。
四、布局组件封装
到这里我们已经有了过滤表单组件、表格组件、分页组件,当使用时我们不可能直接使用就完事了,知识还得对其边距进行调整。我们都知道使用组件库时都会有布局组件,我们也可以参考其封装布局组件来对样式及加载状态进行统一管理。
TableLayout布局组件封装
五、页面完整使用示例
六、结语
当使用vue3进行开发时应该打破vue2时使用的选项式API思维,只有破冰后才能更好的拥抱hook,拥抱前端的未来。最后也推荐一个vue官网推荐学习的VueUse组合式函数集合,帮助你早日拥抱hook。
源码
useTable.ts
typescript
import { ref, toValue, watchEffect } from 'vue'
// 接口返回数据结构
interface IResResult<IData> {
data: IData,
total: number
}
// 初始化参数类型
type TProps<IData> = {
// 获取数据方法
fetchData: (...args: any) => Promise<IResResult<IData[]>>
}
export const useTable = <IData>({
fetchData
}: TProps<IData>) => {
// 数据列表
const list = ref<IData[]>()
// 总数
const total = ref(0)
// 当前页
const pageIndex = ref(1)
// 每页显示条目个数
const pageSize = ref(2)
// 加载状态
const loading = ref(false)
// 获取数据方法
const getList = (filterOptions?: object): Promise<IResResult<IData[]>> => {
loading.value = true
return new Promise((resolve, reject) => {
fetchData({
index: pageIndex.value,
size: pageSize.value,
// 将值、refs 或 getters 规范化为值
...toValue(filterOptions)
}).then(res => {
list.value = res.data
total.value = res.total
// 如果需要在组件中特殊处理,返回数据
resolve(res)
}).catch(err => {
console.log('获取数据失败!', err)
reject(err)
}).finally(() => {
loading.value = false
console.log('接口调用完成!')
})
})
}
// 页大小改变
const onSizeChange = (size: number) => {
pageSize.value = size
}
// 翻页
const onIndexChange = (index: number) => {
pageIndex.value = index
}
watchEffect(() => {
// 使用watchEffect可监听getList方法中使用的响应式依赖变化后执行方法
getList()
})
return {
list,
total,
pageIndex,
pageSize,
loading,
onSizeChange,
onIndexChange,
getList
}
}
TableLayout.vue
vue
<!-- 表格布局组件,用来控制布局样式 -->
<template>
<div class="table-layout" v-loading="loading">
<div class="header">
<slot name="header"></slot>
</div>
<div class="body">
<slot name="body"></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
loading: boolean
}>()
</script>
<style scoped>
.table-layout {
background-color: #fff;
padding: 10px;
}
.footer {
margin-top: 20px;
}
</style>
FilterForm.vue
vue
<template>
<div class="filter">
<div class="form">
<el-form ref="form" inline v-bind="$attrs">
<slot></slot>
</el-form>
</div>
<div class="hanle">
<el-button type="primary" @click="submit">搜索</el-button>
<el-button @click="reset">重置</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { FormInstance } from 'element-plus'
const emit = defineEmits<{
(e: 'submit'): void,
(e: 'reset'): void
}>()
const form = ref<FormInstance>()
const submit = () => {
emit('submit')
}
const reset = () => {
form.value?.resetFields()
emit('reset')
}
</script>
<style scoped lang="scss">
.filter {
display: flex;
.hanle {
margin-left: 20px;
}
}
</style>
Table.vue
vue
<template>
<el-table :data="data" style="width: 100%">
<slot></slot>
</el-table>
</template>
<script setup lang="ts">
defineProps<{
data?: object[]
}>()
</script>
<style scoped>
</style>
Pagination.vue
vue
<template>
<el-pagination
:page-sizes="[1, 2, 3, 4]"
layout="total, sizes, prev, pager, next, jumper"
/>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>