一文学会vue3如何自定义hook钩子函数和封装组件

一、前言

大家好,今天来聊聊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>
相关推荐
集成显卡几秒前
axios平替!用浏览器自带的fetch处理AJAX(兼容表单/JSON/文件上传)
前端·ajax·json
焚琴煮鹤的熊熊野火9 分钟前
前端垂直居中的多种实现方式及应用分析
前端
我是苏苏29 分钟前
C# Main函数中调用异步方法
前端·javascript·c#
转角羊儿40 分钟前
uni-app文章列表制作⑧
前端·javascript·uni-app
大G哥1 小时前
python 数据类型----可变数据类型
linux·服务器·开发语言·前端·python
hong_zc1 小时前
初始 html
前端·html
小小吱1 小时前
HTML动画
前端·html
糊涂涂是个小盆友2 小时前
前端 - 使用uniapp+vue搭建前端项目(app端)
前端·vue.js·uni-app
浮华似水2 小时前
Javascirpt时区——脱坑指南
前端
王二端茶倒水2 小时前
大龄程序员兼职跑外卖第五周之亲身感悟
前端·后端·程序员