Vue3组合式函数封装:打造可复用的异步请求Composable

为什么需要封装请求Composable

Vue3项目中,每个页面组件都在重复写loading状态管理、错误处理、数据缓存逻辑。用useFetch这类组合式函数统一收口,可以把重复代码降到接近零,同时保证所有接口调用的行为一致性——统一的错误提示、统一的Token刷新、统一的请求取消。

核心类型定义与基础结构

用TypeScript定义核心类型:

interface UseFetchOptions<T> extends Omit<RequestInit, 'body'> {
  immediate?: boolean
  initialData?: T
  refetch?: boolean
  debounce?: number
  onSuccess?: (data: T) => void
  onError?: (error: FetchError) => void
}

interface FetchError {
  code: number
  message: string
  detail?: unknown
}

interface UseFetchReturn<T> {
  data: Ref<T | undefined>
  error: Ref<FetchError | null>
  loading: Ref<boolean>
  execute: (body?: unknown) => Promise<T | undefined>
  abort: () => void
  refresh: () => Promise<T | undefined>
}

useFetch完整实现

import { ref, watch, onUnmounted, type Ref } from 'vue'

const BASE_URL = import.meta.env.VITE_API_BASE || '/api'
let refreshPromise: Promise<string> | null = null

async function ensureToken(): Promise<string> {
  const token = localStorage.getItem('access_token')
  if (!token) return ''
  // Token过期检测与刷新
  const payload = JSON.parse(atob(token.split('.')[1]))
  if (payload.exp * 1000 < Date.now() - 5000) {
    if (!refreshPromise) {
      refreshPromise = fetch('/api/auth/refresh', {
        method: 'POST',
        credentials: 'include'
      }).then(r => r.json()).then(data => {
        localStorage.setItem('access_token', data.access_token)
        refreshPromise = null
        return data.access_token
      })
    }
    return refreshPromise
  }
  return token
}

export function useFetch<T>(url: string, options: UseFetchOptions<T> = {}): UseFetchReturn<T> {
  const {
    immediate = true,
    initialData,
    debounce: debounceMs = 0,
    onSuccess,
    onError,
    ...fetchOptions
  } = options

  const data = ref<T | undefined>(initialData)
  const error = ref<FetchError | null>(null)
  const loading = ref(false)

  let controller: AbortController | null = null
  let debounceTimer: ReturnType<typeof setTimeout> | null = null

  const execute = async (body?: unknown): Promise<T | undefined> => {
    controller?.abort()
    controller = new AbortController()

    loading.value = true
    error.value = null

    try {
      const token = await ensureToken()
      const headers: Record<string, string> = {
        'Content-Type': 'application/json',
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
        ...(fetchOptions.headers as Record<string, string> || {})
      }

      const response = await fetch(`${BASE_URL}${url}`, {
        ...fetchOptions,
        headers,
        signal: controller.signal,
        body: body ? JSON.stringify(body) : fetchOptions.body
      })

      if (!response.ok) {
        const errData = await response.json().catch(() => ({}))
        throw { code: response.status, message: errData.message || response.statusText, detail: errData }
      }

      const result = await response.json()
      data.value = result
      onSuccess?.(result)
      return result
    } catch (e: unknown) {
      if (e instanceof DOMException && e.name === 'AbortError') return
      const fetchError = (e as FetchError) || { code: 0, message: 'Unknown error' }
      error.value = fetchError
      onError?.(fetchError)
      return undefined
    } finally {
      loading.value = false
    }
  }

  const debouncedExecute = debounceMs > 0
    ? (body?: unknown) => {
        debounceTimer && clearTimeout(debounceTimer)
        return new Promise<T | undefined>(resolve => {
          debounceTimer = setTimeout(() => execute(body).then(resolve), debounceMs)
        })
      }
    : execute

  const abort = () => {
    controller?.abort()
    debounceTimer && clearTimeout(debounceTimer)
  }

  onUnmounted(abort)

  if (immediate) execute()

  return {
    data: data as Ref<T | undefined>,
    error,
    loading,
    execute: debouncedExecute,
    abort,
    refresh: execute
  }
}

派生Composable:分页与表单场景

基于useFetch封装分页请求:

interface PaginationParams {
  page: number
  pageSize: number
  [key: string]: unknown
}

interface PaginatedResult<T> {
  list: T[]
  total: number
  page: number
  pageSize: number
}

export function usePaginatedFetch<T>(url: string) {
  const page = ref(1)
  const pageSize = ref(20)
  const total = ref(0)
  const filters = ref<Record<string, unknown>>({})

  const { data, loading, error, execute } = useFetch<PaginatedResult<T>>(url, {
    immediate: false,
    onSuccess: (res) => {
      total.value = res.total
    }
  })

  const fetchPage = (p?: number) => {
    if (p !== undefined) page.value = p
    const params: PaginationParams = {
      page: page.value,
      pageSize: pageSize.value,
      ...filters.value
    }
    const qs = new URLSearchParams(
      Object.entries(params).reduce((acc, [k, v]) => ({ ...acc, [k]: String(v) }), {})
    ).toString()
    return execute(undefined)
  }

  watch([page, pageSize], () => fetchPage())

  return { data, loading, error, page, pageSize, total, filters, fetchPage }
}

组件中使用:

<script setup lang="ts">
const { data, loading, page, total, fetchPage } = usePaginatedFetch<UserItem>('/users')
fetchPage(1)
</script>

<template>
  <el-table :data="data?.list" v-loading="loading">
    <!-- 列定义 -->
  </el-table>
  <el-pagination
    v-model:current-page="page"
    :total="total"
    @current-change="fetchPage"
  />
</template>

请求缓存与SWR策略

给useFetch加上缓存层,避免组件重新挂载时重复请求:

const cache = new Map<string, { data: unknown; timestamp: number }>()
const CACHE_TTL = 5 * 60 * 1000 // 5分钟

export function useCachedFetch<T>(url: string, options: UseFetchOptions<T> = {}) {
  const cacheKey = `${options.method || 'GET'}:${url}`
  const cached = cache.get(cacheKey)

  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    // SWR:立即返回缓存,后台刷新
    const fetchReturn = useFetch<T>(url, { ...options, initialData: cached.data as T })
    fetchReturn.refresh()
    return fetchReturn
  }

  return useFetch<T>(url, {
    ...options,
    onSuccess: (data) => {
      cache.set(cacheKey, { data, timestamp: Date.now() })
      options.onSuccess?.(data)
    }
  })
}

Composable封装的本质是职责收口。当所有HTTP请求走同一管道,错误处理、Token刷新、缓存策略只需维护一处,任何组件都能零成本复用这套基础设施。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-han-shu-feng-zhuang-da-zao-ke-fu-yong-de-yi/

(0)
小编小编
上一篇 12小时前
下一篇 12小时前

相关推荐