引言
在 React 中,Hooks 是从 React 16.8 版本引入的新特性,它允许你在不编写 class 的情况下使用 state 以及其他 React 特性。像 useState 可以让函数组件拥有自己的状态,useEffect 则能处理副作用操作,比如数据获取、订阅等。通过使用 Hooks,开发者能够将组件内的逻辑拆分成更小的函数,避免了传统 class 组件中复杂的 this 指向问题,让代码结构更加清晰。
而在 Vue 3 里,组合式 API 借鉴了类似的思想,自定义 Hook 成为了组织和复用逻辑的强大工具。它可以将多个响应式状态以及相关的业务逻辑封装在一起,以函数的形式提供给组件使用,使得代码的复用变得轻而易举。
场景
在开发中,尤其在各种后台管理系统里,表格是绕不过的一个点,在做表格业务的时候总会及到数据获取、分页、排序和过滤等一系列操作。若每次使用该组件都重复编写这些逻辑,会使代码变得冗长且难以维护。
<template>
<t-table
:data="data"
:columns="columns"
:loading="loading"
:pagination="pagination"
@change="rehandleChange"
@page-change="onPageChange"
@select-change="onSelectChange"
bordered
stripe
lazyLoad
>
</t-table>
</template>
<script setup lang="jsx">
import { onMounted, ref, reactive } from 'vue';
import columns from './columns';
import { getTableDataApi } from './api';
const data = ref([]);
const loading = ref(false);
const selectedRowKeys = ref([]);
const rowKey = ref('phone');
const pagination = reactive({
current: 1,
pageSize: 10,
showJumper: true,
onChange: (pageInfo) => {
console.log('pagination.onChange', pageInfo);
},
});
const fetchTableData = async (paginationData = pagination) => {
try {
isLoading.value = true;
const { current, pageSize } = paginationData;
const response = await getTableDataApi({
page: current,
pageSize: pageSize,
});
data.value = response.results;
pagination.total = response.total;
} catch (err) {
data.value = [];
console.error('获取表格数据失败:', err);
} finally {
isLoading.value = false;
}
};
const rehandleChange = (changeParams, triggerAndData) => {
console.log('分页、排序、过滤等发生变化时会触发 change 事件:', changeParams, triggerAndData);
};
const onPageChange = async (pageInfo) => {
console.log('page-change', pageInfo);
pagination.current = pageInfo.current;
pagination.pageSize = pageInfo.pageSize;
await fetchTableData(pageInfo);
};
onMounted(async () => {
await fetchTableData({
current: pagination.current,
pageSize: pagination.pageSize,
});
});
</script>
这里用TdesginUI举例,仅表格的数据请求和分页,看看使用hook之后的效果,首先需要封装一个useTable
封装一个useTable
import _ from 'lodash';
import { ref, watch } from 'vue';
/**
*
* @param api 请求方法
* @param queryParams 固定的请求参数,不需要在查询区如id等标识参数
* @param filterParams 查询条件
* @returns
*/
export default function (api, queryParams, filterParams = {}, sort = null, format = null) {
const loading = ref(false);
const params = ref({});
const filter = ref({});
(params.value = queryParams), (filter.value = filterParams);
// 分页器
const pagination = ref({
total: 0,
current: 1,
pageSize: 10,
pageSizeOptions: [5, 10, 20],
onChange: ({ current, pageSize }) => {
pagination.value.current = current;
pagination.value.pageSize = pageSize;
},
});
const sortion = ref({
sort: [],
sortBy: '',
sortField: '',
});
// 排序
if (sort) {
sortion.value = sort;
}
const dataSource = ref([]);
const getTableData = async (data = {}) => {
if (Object.keys(data).length > 0) filter.value = data;
loading.value = true;
try {
const res = await api({
...sortion.value,
...params.value,
...filter.value,
...data,
pagination: {
pageNum: pagination.value.current,
pageSize: pagination.value.pageSize,
},
});
// console.log(res);
if (format) {
dataSource.value = format(res.items);
}
dataSource.value = res.items || [];
pagination.value.total = Number(res?.pagination.total);
loading.value = false;
} catch (error) {
console.log(error);
loading.value = false;
}
};
const refesh = (savefilter = false) => {
// 重置表格 savefilter默认为false 表头查询条件不保留 true 保留表头查询条件
pagination.value.current = 1;
pagination.value.pageSize = 10;
sortion.value.sort = [];
sortion.value.sortField = '';
sortion.value.sortBy = '';
if (!savefilter) {
filterParams = {};
filter.value = {};
}
getTableData();
};
// 查询操作时需要重置分页器
const refeshPagination = () => {
pagination.value.current = 1;
pagination.value.pageSize = 10;
};
// 过滤参数发生变化时触发
const onFilterChange = async (filters, ctx) => {
// filter.value = filters
refeshPagination();
getTableData(filters);
};
// 点击单元格
const onCellClick = () => {};
/**
* 点击排序
* 多字段排序需按照文档要求给table设置multiple-sort属性
*/
const sortChange = (sort) => {
const isMultipleSort = Array.isArray(sortion.value.sort);
if (isMultipleSort && sortion.value.sort.length > 0) {
// 多字段排序
sortion.value.sort = sort.map((item) => {
return {
field: _.upperFirst(item.sortBy),
by: item.descending ? 'DESC' : 'ASC',
};
});
} else {
const { sortBy, descending } = sort;
sortion.value.sortField = sortBy;
sortion.value.sortBy = descending ? 'DESC' : 'ASC';
}
getTableData();
};
// 分页改变
watch(
() => pagination.value.current,
() => getTableData({ ...filter.value }),
);
watch(
() => pagination.value.pageSize,
() => {
pagination.value.current = 1;
getTableData({ ...filter.value });
},
);
watch(
() => filter.value,
() => {
// console.log(filter.value);
},
);
return {
getTableData,
onFilterChange,
onCellClick,
sortChange,
refesh,
refeshPagination,
loading,
pagination,
dataSource,
};
}
同样的场景下我们使用hook
<template>
<t-table
:data="dataSource"
:columns="columns"
:rowKey="rowKey"
:loading="loading"
:pagination="pagination"
bordered
stripe
lazyLoad
>
</t-table>
</template>
<script setup lang="jsx">
import { onMounted, ref, reactive } from 'vue';
import columns from './columns';
import { getTableDataApi } from './api';
const {
getTableData,
refesh,
refeshPagination,
loading,
pagination,
dataSource
} = useTable(getTableDataApi);
// 初始化数据
onMounted(async () => {
await getTableData();
}
</script>
总结
通过对比可以清晰看到,使用自定义 Hook 封装 TDesign UI 表格逻辑,能显著减少代码量,提升代码复用性和可维护性。在 Vue 3 开发中,合理运用组合式 API 和自定义 Hook,可使代码更简洁、高效,便于扩展和维护。