useRequest 请求管理
HTTP请求管理组合函数,提供数据获取、缓存、重试、轮询等完整的请求处理功能。
📋 基础用法
简单请求
vue
<template>
<div>
<el-button
:loading="loading"
@click="run"
>
获取用户信息
</el-button>
<div v-if="error" class="error">
错误: {{ error.message }}
<el-button @click="run">重试</el-button>
</div>
<div v-if="data">
<h3>用户信息</h3>
<p>姓名: {{ data.name }}</p>
<p>邮箱: {{ data.email }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { useRequest } from '@/composables/use-request'
import { getUserInfo } from '@/api/system/user'
const {
data,
loading,
error,
run
} = useRequest(getUserInfo, {
defaultParams: [1], // 默认参数
immediate: false // 不立即执行
})
</script>带参数的请求
vue
<template>
<div>
<el-form inline>
<el-form-item label="用户ID">
<el-input-number v-model="userId" :min="1" />
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="loading"
@click="fetchUser"
>
查询
</el-button>
</el-form-item>
</el-form>
<el-table
v-loading="loading"
:data="data ? [data] : []"
empty-text="暂无数据"
>
<el-table-column prop="id" label="ID" />
<el-table-column prop="username" label="用户名" />
<el-table-column prop="email" label="邮箱" />
</el-table>
</div>
</template>
<script setup lang="ts">
const userId = ref(1)
const {
data,
loading,
error,
run: fetchUser
} = useRequest(
(id: number) => getUserInfo(id),
{
manual: true, // 手动触发
onSuccess: (result) => {
ElMessage.success('获取成功')
},
onError: (err) => {
ElMessage.error(`获取失败: ${err.message}`)
}
}
)
const handleQuery = () => {
fetchUser(userId.value)
}
</script>🎯 核心功能
useRequest 实现
typescript
// composables/use-request.ts
import { ref, computed, unref, onUnmounted } from 'vue'
export interface RequestOptions<T = any, P extends any[] = any[]> {
// 基础配置
immediate?: boolean // 是否立即执行
manual?: boolean // 是否手动执行
defaultParams?: P // 默认参数
// 缓存配置
cacheKey?: string // 缓存键
cacheTime?: number // 缓存时间(ms)
staleTime?: number // 数据过期时间(ms)
getCacheKey?: (...params: P) => string // 动态缓存键
// 重试配置
retryCount?: number // 重试次数
retryDelay?: number // 重试延迟(ms)
retryCondition?: (error: any) => boolean // 重试条件
// 轮询配置
pollingInterval?: number // 轮询间隔(ms)
pollingWhenHidden?: boolean // 隐藏时是否轮询
pollingWhenOffline?: boolean // 离线时是否轮询
// 节流防抖
debounceWait?: number // 防抖延迟(ms)
throttleWait?: number // 节流延迟(ms)
// 回调函数
onBefore?: (params: P) => void
onSuccess?: (data: T, params: P) => void
onError?: (error: any, params: P) => void
onFinally?: (data?: T, error?: any, params?: P) => void
// 数据处理
transform?: (data: any) => T // 数据转换
errorTransform?: (error: any) => any // 错误转换
// 其他配置
loadingDelay?: number // 加载状态延迟显示(ms)
refreshOnWindowFocus?: boolean // 窗口聚焦时刷新
refreshOnReconnect?: boolean // 网络重连时刷新
}
interface RequestState<T> {
data: T | undefined
loading: boolean
error: any
params: any[]
}
export function useRequest<T = any, P extends any[] = any[]>(
service: (...args: P) => Promise<T>,
options: RequestOptions<T, P> = {}
) {
const {
immediate = true,
manual = false,
defaultParams = [] as unknown as P,
cacheKey,
cacheTime = 5 * 60 * 1000,
staleTime = 0,
retryCount = 0,
retryDelay = 1000,
retryCondition = () => true,
pollingInterval,
pollingWhenHidden = false,
pollingWhenOffline = false,
debounceWait,
throttleWait,
loadingDelay = 0,
refreshOnWindowFocus = false,
refreshOnReconnect = false,
onBefore,
onSuccess,
onError,
onFinally,
transform,
errorTransform,
getCacheKey
} = options
// 状态管理
const state = reactive<RequestState<T>>({
data: undefined,
loading: false,
error: undefined,
params: []
})
// 计算属性
const data = computed(() => state.data)
const loading = computed(() => state.loading)
const error = computed(() => state.error)
// 请求控制
let requestId = 0
let pollingTimer: number | null = null
let loadingTimer: number | null = null
let retryTimer: number | null = null
// 缓存管理
const cache = new Map<string, { data: T; timestamp: number }>()
/**
* 获取缓存键
*/
const getRequestCacheKey = (params: P): string => {
if (getCacheKey) {
return getCacheKey(...params)
}
if (cacheKey) {
return `${cacheKey}_${JSON.stringify(params)}`
}
return ''
}
/**
* 获取缓存数据
*/
const getCacheData = (key: string) => {
const cached = cache.get(key)
if (!cached) return null
const isExpired = Date.now() - cached.timestamp > cacheTime
if (isExpired) {
cache.delete(key)
return null
}
return cached.data
}
/**
* 设置缓存数据
*/
const setCacheData = (key: string, data: T) => {
cache.set(key, {
data,
timestamp: Date.now()
})
}
/**
* 清除加载状态定时器
*/
const clearLoadingTimer = () => {
if (loadingTimer) {
clearTimeout(loadingTimer)
loadingTimer = null
}
}
/**
* 设置加载状态
*/
const setLoading = (loading: boolean) => {
if (loading) {
if (loadingDelay > 0) {
loadingTimer = setTimeout(() => {
state.loading = true
loadingTimer = null
}, loadingDelay)
} else {
state.loading = true
}
} else {
clearLoadingTimer()
state.loading = false
}
}
/**
* 执行请求
*/
const runAsync = async (...params: P): Promise<T> => {
const currentRequestId = ++requestId
state.params = params
state.error = undefined
// 检查缓存
const cacheKeyStr = getRequestCacheKey(params)
if (cacheKeyStr) {
const cachedData = getCacheData(cacheKeyStr)
if (cachedData && Date.now() - cache.get(cacheKeyStr)!.timestamp < staleTime) {
state.data = cachedData
return cachedData
}
}
// 前置回调
onBefore?.(params)
setLoading(true)
try {
let result = await service(...params)
// 检查请求是否已被取消
if (currentRequestId !== requestId) {
throw new Error('Request cancelled')
}
// 数据转换
if (transform) {
result = transform(result)
}
state.data = result
state.error = undefined
// 设置缓存
if (cacheKeyStr) {
setCacheData(cacheKeyStr, result)
}
// 成功回调
onSuccess?.(result, params)
return result
} catch (error) {
// 检查请求是否已被取消
if (currentRequestId !== requestId) {
throw error
}
let processedError = error
if (errorTransform) {
processedError = errorTransform(error)
}
state.error = processedError
// 错误回调
onError?.(processedError, params)
throw processedError
} finally {
if (currentRequestId === requestId) {
setLoading(false)
onFinally?.(state.data, state.error, params)
}
}
}
/**
* 执行请求(带重试)
*/
const runWithRetry = async (...params: P): Promise<T> => {
let lastError: any
let attempts = 0
while (attempts <= retryCount) {
try {
const result = await runAsync(...params)
return result
} catch (error) {
lastError = error
attempts++
if (attempts <= retryCount && retryCondition(error)) {
await new Promise(resolve => {
retryTimer = setTimeout(resolve, retryDelay * attempts)
})
} else {
throw error
}
}
}
throw lastError
}
/**
* 防抖执行
*/
const debouncedRun = debounceWait
? debounce(runWithRetry, debounceWait)
: runWithRetry
/**
* 节流执行
*/
const throttledRun = throttleWait
? throttle(debouncedRun, throttleWait)
: debouncedRun
/**
* 最终执行函数
*/
const run = (...params: P) => {
return throttledRun(...params).catch(() => {
// 错误已在 runAsync 中处理,这里不需要再次处理
})
}
/**
* 刷新(使用上次参数)
*/
const refresh = () => {
return run(...(state.params as P))
}
/**
* 取消请求
*/
const cancel = () => {
requestId++
setLoading(false)
clearPolling()
clearTimers()
}
/**
* 变更数据
*/
const mutate = (data: T | ((oldData?: T) => T)) => {
if (typeof data === 'function') {
state.data = (data as Function)(state.data)
} else {
state.data = data
}
}
/**
* 开始轮询
*/
const startPolling = () => {
if (!pollingInterval) return
clearPolling()
const poll = () => {
// 检查轮询条件
if (!pollingWhenHidden && document.hidden) {
return
}
if (!pollingWhenOffline && !navigator.onLine) {
return
}
refresh()
}
pollingTimer = setInterval(poll, pollingInterval)
}
/**
* 停止轮询
*/
const clearPolling = () => {
if (pollingTimer) {
clearInterval(pollingTimer)
pollingTimer = null
}
}
/**
* 清除所有定时器
*/
const clearTimers = () => {
clearLoadingTimer()
if (retryTimer) {
clearTimeout(retryTimer)
retryTimer = null
}
}
// 窗口聚焦时刷新
if (refreshOnWindowFocus) {
const handleFocus = () => {
if (!state.loading && state.data !== undefined) {
refresh()
}
}
window.addEventListener('focus', handleFocus)
onUnmounted(() => {
window.removeEventListener('focus', handleFocus)
})
}
// 网络重连时刷新
if (refreshOnReconnect) {
const handleOnline = () => {
if (!state.loading && state.data !== undefined) {
refresh()
}
}
window.addEventListener('online', handleOnline)
onUnmounted(() => {
window.removeEventListener('online', handleOnline)
})
}
// 自动执行
if (!manual && immediate) {
run(...defaultParams)
}
// 自动轮询
if (pollingInterval) {
startPolling()
}
// 清理
onUnmounted(() => {
cancel()
})
return {
// 状态
data,
loading,
error,
params: computed(() => state.params),
// 方法
run,
runAsync,
refresh,
cancel,
mutate,
// 轮询控制
startPolling,
clearPolling
}
}
// 工具函数
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => Promise<ReturnType<T>> {
let timeout: number | null = null
return (...args: Parameters<T>): Promise<ReturnType<T>> => {
return new Promise((resolve, reject) => {
if (timeout) clearTimeout(timeout)
timeout = setTimeout(async () => {
try {
const result = await func(...args)
resolve(result)
} catch (error) {
reject(error)
}
}, wait)
})
}
}
function throttle<T extends (...args: any[]) => any>(
func: T,
wait: number
): T {
let lastTime = 0
return ((...args: Parameters<T>) => {
const now = Date.now()
if (now - lastTime >= wait) {
lastTime = now
return func(...args)
}
}) as T
}🔄 高级功能
分页请求
typescript
// composables/use-pagination.ts
export interface PaginationConfig {
defaultCurrent?: number
defaultPageSize?: number
total?: number
showSizeChanger?: boolean
pageSizeOptions?: number[]
}
export function usePagination<T>(
service: (params: { current: number; pageSize: number; [key: string]: any }) => Promise<{
data: T[]
total: number
}>,
config: PaginationConfig = {}
) {
const {
defaultCurrent = 1,
defaultPageSize = 10,
showSizeChanger = true,
pageSizeOptions = [10, 20, 50, 100]
} = config
const pagination = reactive({
current: defaultCurrent,
pageSize: defaultPageSize,
total: 0
})
const {
data: rawData,
loading,
error,
run,
refresh
} = useRequest(
async (params: any = {}) => {
const result = await service({
current: pagination.current,
pageSize: pagination.pageSize,
...params
})
pagination.total = result.total
return result.data
},
{
immediate: true
}
)
const data = computed(() => rawData.value || [])
// 切换页码
const changeCurrent = (current: number) => {
pagination.current = current
run()
}
// 切换每页条数
const changePageSize = (pageSize: number) => {
pagination.current = 1
pagination.pageSize = pageSize
run()
}
// 重置分页
const resetPagination = () => {
pagination.current = defaultCurrent
pagination.pageSize = defaultPageSize
pagination.total = 0
}
return {
data,
loading,
error,
pagination: readonly(pagination),
run,
refresh,
changeCurrent,
changePageSize,
resetPagination,
pageSizeOptions
}
}无限滚动
typescript
// composables/use-infinite-scroll.ts
export function useInfiniteScroll<T>(
service: (params: { page: number; pageSize: number; [key: string]: any }) => Promise<{
data: T[]
hasMore: boolean
}>,
options: {
pageSize?: number
threshold?: number
immediate?: boolean
} = {}
) {
const { pageSize = 20, threshold = 100, immediate = true } = options
const page = ref(1)
const hasMore = ref(true)
const allData = ref<T[]>([])
const {
loading,
error,
run: loadMore
} = useRequest(
async (params: any = {}) => {
const result = await service({
page: page.value,
pageSize,
...params
})
if (page.value === 1) {
allData.value = result.data
} else {
allData.value.push(...result.data)
}
hasMore.value = result.hasMore
if (result.hasMore) {
page.value++
}
return result
},
{
immediate
}
)
// 重置数据
const reset = () => {
page.value = 1
hasMore.value = true
allData.value = []
}
// 刷新数据
const refresh = () => {
reset()
loadMore()
}
// 滚动监听
const setupScrollListener = (container?: HTMLElement) => {
const element = container || window
const handleScroll = () => {
if (loading.value || !hasMore.value) return
const scrollTop = element === window
? document.documentElement.scrollTop || document.body.scrollTop
: (element as HTMLElement).scrollTop
const scrollHeight = element === window
? document.documentElement.scrollHeight || document.body.scrollHeight
: (element as HTMLElement).scrollHeight
const clientHeight = element === window
? window.innerHeight
: (element as HTMLElement).clientHeight
if (scrollHeight - scrollTop - clientHeight <= threshold) {
loadMore()
}
}
element.addEventListener('scroll', handleScroll)
onUnmounted(() => {
element.removeEventListener('scroll', handleScroll)
})
}
return {
data: computed(() => allData.value),
loading,
error,
hasMore,
loadMore,
reset,
refresh,
setupScrollListener
}
}乐观更新
typescript
// composables/use-optimistic.ts
export function useOptimistic<T>(
queryKey: string,
mutationFn: (variables: any) => Promise<T>
) {
const queryCache = new Map<string, T>()
const {
data,
loading,
error,
run: mutate
} = useRequest(mutationFn, {
manual: true,
onBefore: (variables) => {
// 保存当前数据作为回滚点
const currentData = queryCache.get(queryKey)
if (currentData) {
queryCache.set(`${queryKey}_rollback`, currentData)
}
},
onError: () => {
// 出错时回滚数据
const rollbackData = queryCache.get(`${queryKey}_rollback`)
if (rollbackData) {
queryCache.set(queryKey, rollbackData)
queryCache.delete(`${queryKey}_rollback`)
}
},
onSuccess: (result) => {
// 成功时更新缓存
queryCache.set(queryKey, result)
queryCache.delete(`${queryKey}_rollback`)
}
})
// 乐观更新
const optimisticUpdate = (updater: (oldData?: T) => T, variables: any) => {
const oldData = queryCache.get(queryKey)
const optimisticData = updater(oldData)
// 立即更新 UI
queryCache.set(queryKey, optimisticData)
// 执行实际请求
mutate(variables)
}
return {
data,
loading,
error,
mutate,
optimisticUpdate
}
}📊 请求状态管理
全局请求状态
typescript
// composables/use-global-loading.ts
export function useGlobalLoading() {
const loadingRequests = ref(new Set<string>())
const loading = computed(() => loadingRequests.value.size > 0)
const addLoading = (key: string) => {
loadingRequests.value.add(key)
}
const removeLoading = (key: string) => {
loadingRequests.value.delete(key)
}
const clearLoading = () => {
loadingRequests.value.clear()
}
return {
loading,
addLoading,
removeLoading,
clearLoading
}
}
// 带全局加载状态的请求
export function useRequestWithGlobalLoading<T, P extends any[]>(
service: (...args: P) => Promise<T>,
options: RequestOptions<T, P> & { loadingKey?: string } = {}
) {
const { loadingKey = 'default', ...requestOptions } = options
const { addLoading, removeLoading } = useGlobalLoading()
return useRequest(service, {
...requestOptions,
onBefore: (params) => {
addLoading(loadingKey)
options.onBefore?.(params)
},
onFinally: (data, error, params) => {
removeLoading(loadingKey)
options.onFinally?.(data, error, params)
}
})
}请求队列管理
typescript
// composables/use-request-queue.ts
export class RequestQueue {
private queue: Array<() => Promise<any>> = []
private running: Set<Promise<any>> = new Set()
private maxConcurrent: number
constructor(maxConcurrent = 6) {
this.maxConcurrent = maxConcurrent
}
async add<T>(requestFn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
const result = await requestFn()
resolve(result)
return result
} catch (error) {
reject(error)
throw error
}
})
this.processQueue()
})
}
private async processQueue() {
if (this.running.size >= this.maxConcurrent || this.queue.length === 0) {
return
}
const requestFn = this.queue.shift()!
const promise = requestFn()
this.running.add(promise)
try {
await promise
} finally {
this.running.delete(promise)
this.processQueue()
}
}
clear() {
this.queue.length = 0
}
get pending() {
return this.queue.length
}
get active() {
return this.running.size
}
}
export function useRequestQueue(maxConcurrent = 6) {
const queue = new RequestQueue(maxConcurrent)
const addRequest = <T>(requestFn: () => Promise<T>): Promise<T> => {
return queue.add(requestFn)
}
return {
addRequest,
pending: computed(() => queue.pending),
active: computed(() => queue.active),
clear: () => queue.clear()
}
}useRequest组合函数为Vue3应用提供了完整的HTTP请求管理解决方案,支持缓存、重试、轮询、分页、无限滚动等各种复杂场景。
