Vue3 後台分頁寫膩了?我用 1 個 Hook 刪掉 90% 重複代碼(附原碼)

image

實戰推薦:

還在為每個列表頁寫重複的分頁代碼而煩惱嗎?還在複製貼上 currentPage、pageSize、loading 等狀態嗎?一個 Hook 幫你解決所有分頁痛點,減少 90% 重複代碼。

背景與痛點

在後台管理系統開發中,分頁列表查詢非常常見,我們通常需要處理:

  • 當前頁、頁大小、總數等分頁狀態
  • 加載中、錯誤處理等請求狀態
  • 搜尋、刷新、翻頁等分頁操作
  • 資料快取和重複請求處理

這些重複邏輯分散在各個元件中,維護起來很麻煩。

為了解決這個煩惱,我專門封裝了分頁資料管理 Hook。現在只需要幾行代碼,就能輕鬆實現分頁查詢,省時又高效,減少了大量重複勞動。

使用前提 - 接口格式約定

查詢接口返回的資料格式:

{
  list: [        // 當前頁數據數組
    { id: 1, name: 'user1' },
    { id: 2, name: 'user2' }
  ],
  total: 100     // 資料總條數
}

先看效果:分頁查詢只需幾行代碼!

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:分頁邏輯封裝,專門處理分頁相關的狀態和操作
usePageFetch (分頁業務層)
├── 管理 page / pageSize / total 狀態
├── 處理搜尋、刷新、翻頁邏輯  
├── 統一錯誤處理和用戶提示
└── 呼叫 useFetch (請求基礎層)
    ├── 管理 loading / data / error 狀態
    ├── 可選快取機制(避免重複請求)
    └── 成功回調適配不同接口格式

核心實現

useFetch - 基礎請求封裝

// 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 - 分頁邏輯封裝

// 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 舉例

<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>

高級用法

帶快取

const { 
  data,
  isFetching,
  search
} = usePageFetch(getUserList, {
  cache: (params) => `user-list-${JSON.stringify(params)}` // 自訂快取 key
})

設計思路解析

  • 職責分離:useFetch 專注請求狀態管理,usePageFetch 專注分頁邏輯
  • 統一錯誤處理:在 usePageFetch 層統一處理錯誤
  • 智能快取機制:支持多種快取策略
  • 生命週期集成:自動在元件掛載時請求數據

總結

這套分頁管理 Hook 的優勢:

  • 開發效率高,減少 90% 的重複代碼,新增列表頁從 30 分鐘縮短到 5 分鐘
  • 狀態管理完善,自動處理加載、錯誤、數據狀態
  • 快取機制,避免重複請求
  • 錯誤處理統一,用戶體驗一致
  • 易於擴展,支持自訂配置和回調

如果覺得對您有幫助,歡迎按讚 👍 收藏 ⭐ 關注 🔔 支持一下!


原文出處:https://juejin.cn/post/7549096640340426802


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝11   💬6   ❤️6
472
🥈
我愛JS
📝1   💬5   ❤️4
92
🥉
AppleLily
📝1   💬4   ❤️1
53
#4
💬1  
5
#5
xxuan
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次