Vue3 后台分页写腻了?我用 1 个 Hook 删掉 90% 重复代码(附源码)

还在为每个列表页写重复的分页代码而烦恼吗? 还在复制粘贴 currentPage、pageSize、loading 等状态吗? 一个 Hook 帮你解决所有分页痛点,减少90%重复代码

背景与痛点

在后台管理系统开发中,分页列表查询非常常见,我们通常需要处理:

  • 当前页、页大小、总数等分页状态
  • 加载中、错误处理等请求状态
  • 搜索、刷新、翻页等分页操作
  • 数据缓存和重复请求处理

这些重复逻辑分散在各个组件中,维护起来很麻烦。

为了解决这个烦恼,我专门封装了分页数据管理 Hook。现在只需要几行代码,就能轻松实现分页查询,省时又高效,减少了大量重复劳动

使用前提 - 接口格式约定

查询接口返回的数据格式:

js 复制代码
{
  list: [        // 当前页数据数组
    { id: 1, name: 'user1' },
    { id: 2, name: 'user2' }
  ],
  total: 100     // 数据总条数
}

先看效果:分页查询只需几行代码!

js 复制代码
import usePageFetch from '@/hooks/usePageFetch' // 引入分页查询 Hook,封装了分页逻辑和状态管理
import { getUserList } from '@/api/user'        // 引入请求用户列表的 API 方法

// 使用 usePageFetch Hook 实现分页数据管理
const {
  currentPage,      // 当前页码
  pageSize,         // 每页条数
  total,            // 数据总数
  data,             // 当前页数据列表
  isFetching,       // 加载状态,用于控制 loading 效果
  search,           // 搜索方法
  onSizeChange,     // 页大小改变事件处理方法
  onCurrentChange   // 页码改变事件处理方法
} = usePageFetch(
  getUserList,  // 查询API
  { initFetch: false }  // 是否自动请求一次(组件挂载时自动拉取第一页数据)     
)

这样子每次分页查询只需要引入hook,然后传入查询接口就好了,减少了大量重复劳动

解决方案

我设计了两个相互配合的 Hook:

  • useFetch:基础请求封装,处理请求状态和缓存
  • usePageFetch:分页逻辑封装,专门处理分页相关的状态和操作
scss 复制代码
usePageFetch (分页业务层)
├── 管理 page / pageSize / total 状态
├── 处理搜索、刷新、翻页逻辑  
├── 统一错误处理和用户提示
└── 调用 useFetch (请求基础层)
    ├── 管理 loading / data / error 状态
    ├── 可选缓存机制(避免重复请求)
    └── 成功回调适配不同接口格式

核心实现

useFetch - 基础请求封装

js 复制代码
// hooks/useFetch.js
import { ref } from 'vue'

const Cache = new Map()

/**
 * 基础请求 Hook
 * @param {Function} fn - 请求函数
 * @param {Object} options - 配置选项
 * @param {*} options.initValue - 初始值
 * @param {string|Function} options.cache - 缓存配置
 * @param {Function} options.onSuccess - 成功回调
 */
function useFetch(fn, options = {}) {
  const isFetching = ref(false)
  const data = ref()
  const error = ref()

  // 设置初始值
  if (options.initValue !== undefined) {
    data.value = options.initValue
  }

  function fetch(...args) {
    isFetching.value = true
    let promise

    if (options.cache) {
      const cacheKey = typeof options.cache === 'function'
        ? options.cache(...args)
        : options.cache || `${fn.name}_${args.join('_')}`

      promise = Cache.get(cacheKey) || fn(...args)
      Cache.set(cacheKey, promise)
    } else {
      promise = fn(...args)
    }

    // 成功回调处理
    if (options.onSuccess) {
      promise = promise.then(options.onSuccess)
    }

    return promise
      .then(res => {
        data.value = res
        isFetching.value = false
        error.value = undefined
        return res
      })
      .catch(err => {
        isFetching.value = false
        error.value = err
        return Promise.reject(err)
      })
  }

  return {
    fetch,
    isFetching,
    data,
    error
  }
}

export default useFetch

usePageFetch - 分页逻辑封装

js 复制代码
// hooks/usePageFetch.js
import { ref, onMounted, toRaw, watch } from 'vue'
import useFetch from './useFetch' // 即上面的hook ---> useFetch 
import { ElMessage } from 'element-plus'

/**
 * 分页数据管理 Hook
 * @param {Function} fn - 请求函数
 * @param {Object} options - 配置选项
 * @param {Object} options.params - 默认参数
 * @param {boolean} options.initFetch - 是否自动初始化请求
 * @param {Ref} options.formRef - 表单引用
 */
function usePageFetch(fn, options = {}) {
  // 分页状态
  const page = ref(1)
  const pageSize = ref(10)
  const total = ref(0)
  const data = ref([])
  const params = ref()
  const pendingCount = ref(0)

  // 初始化参数
  params.value = options.params

  //  使用基础请求 Hook
  const { isFetching, fetch: fetchFn, error, data: originalData } = useFetch(fn)

  //  核心请求方法
  const fetch = async (searchParams, pageNo, size) => {
    try {
      // 更新分页状态
      page.value = pageNo
      pageSize.value = size
      params.value = searchParams

      // 发起请求
      await fetchFn({
        page: pageNo,
        pageSize: size,
        // 使用 toRaw 避免响应式对象问题
        ...(searchParams ? toRaw(searchParams) : {})
      })

      // 处理响应数据
      data.value = originalData.value?.list || []
      total.value = originalData.value?.total || 0
      pendingCount.value = originalData.value?.pendingCounts || 0
    } catch (e) {
      console.error('usePageFetch error:', e)
      ElMessage.error(e?.msg || e?.message || '请求出错')
      // 清空数据,提供更好的用户体验
      data.value = []
      total.value = 0
    }
  }

  //  搜索 - 重置到第一页
  const search = async (searchParams) => {
    await fetch(searchParams, 1, pageSize.value)
  }

  // 刷新当前页
  const refresh = async () => {
    await fetch(params.value, page.value, pageSize.value)
  }

  // 改变页大小
  const onSizeChange = async (size) => {
    await fetch(params.value, 1, size) // 重置到第一页
  }

  // 切换页码
  const onCurrentChange = async (pageNo) => {
    await fetch(params.value, pageNo, pageSize.value)
  }

  // 组件挂载时自动请求
  onMounted(() => {
    if (options.initFetch !== false) {
      search(params.value)
    }
  })

  // 监听表单引用变化(可选功能)
  watch(
    () => options.formRef,
    (formRef) => {
      if (formRef) {
        console.log('Form ref updated:', formRef)
      }
    }
  )

  return {
    // 分页状态
    currentPage: page,
    pageSize,
    total,
    pendingCount,
    
    // 数据状态
    data,
    originalData,
    isFetching,
    error,
    
    // 操作方法
    search,
    refresh,
    onSizeChange,
    onCurrentChange
  }
}

export default usePageFetch

完整使用示例

用element ui举例

js 复制代码
<template>
    <el-form :model="searchForm" >
      <el-form-item label="用户名">
        <el-input v-model="searchForm.username" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handleSearch">搜索</el-button>
      </el-form-item>
    </el-form>
  <!-- 表格数据展示,绑定 data 和 loading 状态 -->
  <el-table :data="data" v-loading="isFetching">
    <!-- ...表格列定义... -->
  </el-table>

  <!-- 分页组件,绑定当前页、页大小、总数,并响应切换事件 -->
  <el-pagination
    v-model:current-page="currentPage"
    v-model:page-size="pageSize"
    :total="total"
    @size-change="onSizeChange"
    @current-change="onCurrentChange"
  />
</template>
<script setup>
import { ref } from 'vue'
import usePageFetch from '@/hooks/usePageFetch' // 引入分页查询 Hook,封装了分页逻辑和状态管理
import { getUserList } from '@/api/user'        // 引入请求用户列表的 API 方法

// 搜索表单数据,响应式声明
const searchForm = ref({
  username: ''
})

// 使用 usePageFetch Hook 实现分页数据管理
const {
  currentPage,      // 当前页码
  pageSize,         // 每页条数
  total,            // 数据总数
  data,             // 当前页数据列表
  isFetching,       // 加载状态,用于控制 loading 效果
  search,           // 搜索方法
  onSizeChange,     // 页大小改变事件处理方法
  onCurrentChange   // 页码改变事件处理方法
} = usePageFetch(
  getUserList, 
  { initFetch: false }  // 是否自动请求一次(组件挂载时自动拉取第一页数据)     
)

/**
* 处理搜索操作
*/
const handleSearch = () => {
  search({ username: searchForm.value.username })
}

</script>

高级用法

带缓存

js 复制代码
const {
  data,
  isFetching,
  search
} = usePageFetch(getUserList, {
  cache: (params) => `user-list-${JSON.stringify(params)}` // 自定义缓存 key
})

设计思路解析

  • 职责分离:useFetch 专注请求状态管理,usePageFetch 专注分页逻辑
  • 统一错误处理:在 usePageFetch 层统一处理错误
  • 智能缓存机制:支持多种缓存策略
  • 生命周期集成:自动在组件挂载时请求数据

总结

这套分页管理 Hook 的优势:

  • 开发效率高,减少90%的重复代码,新增列表页从 30 分钟缩短到 5 分钟
  • 状态管理完善,自动处理加载、错误、数据状态
  • 缓存机制,避免重复请求
  • 错误处理统一,用户体验一致
  • 易于扩展,支持自定义配置和回调

如果觉得对您有帮助,欢迎点赞 👍 收藏 ⭐ 关注 🔔 支持一下!

相关推荐
崔庆才丨静觅11 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606112 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了12 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅12 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅12 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅13 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment13 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅13 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊13 小时前
jwt介绍
前端
爱敲代码的小鱼13 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax