Vue3封装hook,在开发中做一个无脑的CV吧

2,019 阅读3分钟

引言

在 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,可使代码更简洁、高效,便于扩展和维护。