vue3中分页组合式函数的一种实现

302 阅读4分钟

Hooks或者组合式函数,可用于订阅变化的数据源、事件源并封装成可被复用的逻辑。封装的关键在于封装不变的部分,暴露变化的部分给用户。

在常见的分页这一逻辑中,查询参数、分页参数、列表数据及分页、查询、重置等方法都需慎重考虑哪部分应该被封装在组合式函数内,哪部分应该留给用户,下方就是一个对此业务的组合式函数的用法和实现:

用法:

<template>
  <el-input clearable v-model='query.goodsName'></el-input>
  <el-button type="primary" @click="search"> 查询 </el-button>
  <el-button @click="reset">重置</el-button>
  <el-table :data="list">
  <!-- ... -->
  </el-table>
  <el-pagination
    v-if="list?.length"
    @size-change="handleSizeChange"
    @current-change="handleCurrentChange"
    :size="meta.size"
    :total="meta.total"
    :current-page="meta.current"
    :pageSizes="meta.sizes"
    layout="total, sizes, prev, pager, next, jumper"
  ></el-pagination>
</template>
<script>
import usePaging from '@/hooks/usePaging'
const query = ref({ goodsName: '' }) // 查询参数
// 获取列表数据函数
const fetchList = async (params: any) => {
  try {
    const res = await getGoodsListApi(params)
    return res?.data
  } catch (error) {
    console.error('fetchList-error', error)
    return null
  }
}
// 使用分页组合式函数,获取列表数据及分页信息
const { meta, list, search, getData, reset, handleSizeChange, handleCurrentChange } =
  usePaging(fetchList, query)
</script>

实现:

ts版本

import { ref, reactive, onMounted, type Ref } from 'vue'
// --- Types start ----
// 定义获取列表数据的参数接口
interface FetchListParams {
  condition: Record<string, any>
  current: number
  size: number
}
// 定义获取列表数据的函数类型
type FetchListFunction = (params: FetchListParams) => Promise<any>
// 定义分页元数据接口
interface Meta {
  current: number
  size: number
  total: number
  sizes: number[]
}
// 定义额外选项的接口,包含初始查询数据、是否在mounted时调用getData以及初始分页配置
interface ExtraOptions {
  initialQueryData?: Record<string, any>
  isCalledOnMounted?: boolean
  initialMeta?: Meta
}
// --- Types end ----
/**
 * usePaging 组合式函数用于管理分页数据的获取和状态。
 *
 * @param fetchList - 用于获取列表数据的异步函数。
 * @param query - 包含查询条件的响应式引用对象。
 * @param extraOptions - 包含额外配置的可选对象,包括初始查询条件、是否在组件挂载时获取数据以及初始分页配置。
 * @returns 分页和列表管理的一系列方法和状态。
 */
export default function usePaging(
  fetchList: FetchListFunction,
  query: Ref<Record<string, any>>,
  extraOptions?: ExtraOptions
) {
  // 设置 extraOptions 的默认值
  const options = {
    initialQueryData: {},
    isCalledOnMounted: true,
    initialMeta: {
      current: 1,
      size: 10,
      total: 0,
      sizes: [10, 15, 20, 30, 40],
    },
    ...extraOptions,
  }
  const list = ref([]) // 列表数据
  const meta = reactive(options.initialMeta) // 使用 options 中的 initialMeta 作为初始分页配置
  const initialQuery = { ...options.initialQueryData }
  onMounted(() => {
    if (options.isCalledOnMounted) getData()
  })
  /**
   * 获取数据
   */
  const getData = async () => {
    try {
      const result = await fetchList({
        condition: filterData(query.value),
        current: meta.current,
        size: meta.size,
      })
      if (result) {
        meta.total = result.total
        list.value = result.records
      }
    } catch (error) {
      console.error('getData-error', error)
    }
  }
  /**
   * 过滤数据:过滤空值参数、并将 * 转换为 %
   * @param data
   * @returns
   */
  const filterData = (data: Record<string, any>) => {
    const temp = Object.entries(data).reduce((result: Record<string, any>, [key, value]) => {
      // 过滤值为 '' undefined null 空数组 的字段
      if (
        value !== null &&
        value !== '' &&
        value !== undefined &&
        (!Array.isArray(value) || value.length > 0)
      ) {
        result[key] = value
      }
      return result
    }, {})
    Object.entries(temp).forEach(([key, value]) => {
      if (typeof value === 'string') {
        temp[key] = value.replace(/*/g, '%')
      }
    })
    return temp
  }
​
  /**
   * 搜索
   */
  const search = () => {
    meta.current = 1
    getData()
  }
​
  /**
   * 重置
   */
  const reset = () => {
    Object.keys(query.value).forEach((key) => {
      query.value[key] = initialQuery[key]
    })
    meta.current = 1
    getData()
  }
​
  /**
   * 页面数据条数变化
   * @param val
   */
  const handleSizeChange = (val: number | Event) => {
    meta.size = typeof val === 'number' ? val : (val as CustomEvent).detail
    meta.current = 1
    getData()
  }
​
  /**
   * 页码变化
   * @param val
   */
  const handleCurrentChange = (val: number | Event) => {
    meta.current = typeof val === 'number' ? val : (val as CustomEvent).detail
    getData()
  }
​
  return {
    meta, // 分页配置
    list, // 列表数据
    getData, // 获取数据
    search, // 搜索
    reset, // 重置
    handleSizeChange, // 页面条数变化处理函数
    handleCurrentChange, // 页码变化函数
  }
}
​

js版本

import { ref, reactive, onMounted } from 'vue';
​
/**
 * usePaging 组合式函数用于管理分页数据的获取和状态。
 *
 * @param {Function} fetchList - 用于获取列表数据的异步函数。
 * @param {Object} query - 包含查询条件的响应式引用对象。
 * @param {Object} [extraOptions] - 包含额外配置的可选对象,包括初始查询条件、是否在组件挂载时获取数据以及初始分页配置。
 * @returns {Object} 分页和列表管理的一系列方法和状态。
 */
export default function usePaging(fetchList, query, extraOptions) {
  // 设置 extraOptions 的默认值
  const options = {
    initialQueryData: {},
    isCalledOnMounted: true,
    initialMeta: {
      current: 1,
      size: 10,
      total: 0,
      sizes: [10, 15, 20, 30, 40],
    },
    ...extraOptions,
  };
  const list = ref([]); // 列表数据
  const meta = reactive(options.initialMeta); // 使用 options 中的 initialMeta 作为初始分页配置
  const initialQuery = { ...options.initialQueryData };
  onMounted(() => {
    if (options.isCalledOnMounted) getData();
  });
​
  /**
   * 获取数据
   */
  const getData = async () => {
    try {
      const result = await fetchList({
        condition: filterData(query.value),
        current: meta.current,
        size: meta.size,
      });
      if (result) {
        meta.total = result.total;
        list.value = result.records;
      }
    } catch (error) {
      console.error('getData-error', error);
    }
  };
​
  /**
   * 过滤数据:过滤空值参数、并将 * 转换为 %
   * @param {Object} data
   * @returns {Object}
   */
  const filterData = (data) => {
    const temp = Object.entries(data).reduce((result, [key, value]) => {
      // 过滤值为 '' undefined null 空数组 的字段
      if (value !== null && value !== '' && value !== undefined && (!Array.isArray(value) || value.length > 0)) {
        result[key] = value;
      }
      return result;
    }, {});
    Object.entries(temp).forEach(([key, value]) => {
      if (typeof value === 'string') {
        temp[key] = value.replace(/*/g, '%');
      }
    });
    return temp;
  };
​
  /**
   * 搜索
   */
  const search = () => {
    meta.current = 1;
    getData();
  };
​
  /**
   * 重置
   */
  const reset = () => {
    Object.keys(query.value).forEach((key) => {
      query.value[key] = initialQuery[key];
    });
    meta.current = 1;
    getData();
  };
​
  /**
   * 页面数据条数变化
   * @param {number|Event} val
   */
  const handleSizeChange = (val) => {
    meta.size = typeof val === 'number' ? val : (val as CustomEvent).detail;
    meta.current = 1;
    getData();
  };
​
  /**
   * 页码变化
   * @param {number|Event} val
   */
  const handleCurrentChange = (val) => {
    meta.current = typeof val === 'number' ? val : (val as CustomEvent).detail;
    getData();
  };
​
  return {
    meta, // 分页配置
    list, // 列表数据
    getData, // 获取数据
    search, // 搜索
    reset, // 重置
    handleSizeChange, // 页面条数变化处理函数
    handleCurrentChange, // 页码变化函数
  };
}

配置参数说明


usePaging(

    fetchList: FetchListFunction,

    query: Ref<Record<string, any>>,

    extraOptions?: ExtraOptions

)
参数名类型说明
fetchListFetchListFunction获取列表数据的函数
queryRef<Record<string, any>>查询参数的ref引用
extraOptionsExtraOptions额外参数配置
initialQueryData: Record<string, any>初始默认query参数对象,未传则使用初次传入的query对象
isCalledOnMounted: boolean是否在onMounted中调用获取数据操作,默认true
initialMeta:Meta初始的分页参数配置,默认如下:
{
  current: 1,
  size: 10,
  total: 0,
  sizes: [10, 15, 20, 30, 40],
}