You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

220 lines
5.8 KiB

import { ref, onMounted, onServerPrefetch, Ref } from 'vue'
import { getCurrentInstance } from 'vue'
import type { SSRContext } from './ssrContext'
import { resolveSSRContext } from './ssrContext'
import { $fetch } from 'ofetch'
// 全局数据缓存,用于 SSR 数据共享
const globalCache = new Map<string, any>()
// SSR 上下文类型从 ssrContext.ts 引入
// useFetch 的配置选项
interface UseFetchOptions<T = any> {
key?: string
server?: boolean
default?: () => any
transform?: (data: any) => T
onError?: (error: Error) => void
}
// useFetch 返回值类型
interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<Error | null>
pending: Ref<boolean>
refresh: () => Promise<void>
execute: () => Promise<void>
}
/**
* SSR 兼容的 useFetch hook
* 支持服务端预取和客户端水合
*/
export function useFetch<T = any>(
url: string | (() => string) | (() => Promise<string>),
options: UseFetchOptions<T> = {}
): UseFetchReturn<T> {
const {
key,
server = true,
default: defaultValue,
transform,
onError
} = options
// 生成缓存键
const cacheKey = key || (typeof url === 'string' ? url : `fetch-${Date.now()}`)
// 响应式状态
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const pending = ref(false)
// 获取当前组件实例
const instance = getCurrentInstance()
// 获取 SSR 上下文
const getSSRContext = (): SSRContext | null => resolveSSRContext(instance)
// 获取缓存
const getCache = () => {
const ssrContext = getSSRContext()
return ssrContext?.cache || globalCache
}
// 设置缓存
const setCache = (key: string, value: any) => {
const cache = getCache()
cache.set(key, value)
}
// 获取缓存数据
const getCachedData = () => {
const cache = getCache()
return cache.get(cacheKey)
}
// 执行 fetch 请求
const execute = async (): Promise<void> => {
try {
pending.value = true
error.value = null
// 获取 URL
const fetchUrl = typeof url === 'function' ? await url() : url
// 仅在服务端注入 Cookie,客户端浏览器会自动携带
let requestInit: RequestInit | undefined
if (typeof window === 'undefined') {
const ssrContext = getSSRContext()
const cookieHeader = ssrContext?.cookies
? Object.entries(ssrContext.cookies)
.filter(([k, v]) => k && v != null)
.map(([k, v]) => `${k}=${String(v)}`)
.join('; ')
: undefined
if (cookieHeader) {
requestInit = { headers: { Cookie: cookieHeader } }
}
}
// 执行请求
const response = await $fetch(fetchUrl, requestInit)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
let result = await response.json()
// 应用转换函数
if (transform) {
result = transform(result)
}
data.value = result
setCache(cacheKey, result)
// 收集服务端返回的 Set-Cookie,回传到最终响应头
if (typeof window === 'undefined') {
const ssrContext = getSSRContext()
if (ssrContext) {
const setCookieValues: string[] = []
const anyHeaders: any = response.headers as any
// undici 扩展:getSetCookie()
if (typeof anyHeaders?.getSetCookie === 'function') {
try {
const arr = anyHeaders.getSetCookie()
if (Array.isArray(arr)) setCookieValues.push(...arr)
} catch {}
}
// node-fetch/raw headers API
if (typeof anyHeaders?.raw === 'function') {
try {
const raw = anyHeaders.raw()
const arr = raw?.['set-cookie']
if (Array.isArray(arr)) setCookieValues.push(...arr)
} catch {}
}
// 兜底:单值
const single = response.headers.get('set-cookie')
if (single) setCookieValues.push(single)
if (setCookieValues.length) {
if (!Array.isArray(ssrContext.setCookies)) ssrContext.setCookies = []
ssrContext.setCookies.push(...setCookieValues)
}
}
}
} catch (err) {
const fetchError = err instanceof Error ? err : new Error(String(err))
error.value = fetchError
if (onError) {
onError(fetchError)
}
// 设置默认值
if (defaultValue) {
data.value = typeof defaultValue === 'function' ? defaultValue() : defaultValue
}
} finally {
pending.value = false
}
}
// 刷新数据
const refresh = async (): Promise<void> => {
// 清除缓存
const cache = getCache()
cache.delete(cacheKey)
await execute()
}
// 服务端预取
if (server && typeof window === 'undefined') {
onServerPrefetch(async () => {
// 检查是否已有缓存数据
const cachedData = getCachedData()
if (cachedData !== undefined) {
data.value = cachedData
return
}
// 执行预取
await execute()
})
}
// 立即检查缓存数据(服务端和客户端都需要)
const cachedData = getCachedData()
if (cachedData !== undefined) {
data.value = cachedData
console.log(`[useFetch] 从缓存加载数据: ${cacheKey}`, cachedData)
} else {
console.log(`[useFetch] 缓存中无数据: ${cacheKey}`)
}
// 客户端水合
if (typeof window !== 'undefined') {
onMounted(async () => {
// 如果已经有缓存数据,不需要再次请求
if (cachedData !== undefined) {
return
}
// 如果没有预取数据,则执行请求
await execute()
})
}
return {
data: data as Ref<T | null>,
error: error as Ref<Error | null>,
pending: pending as Ref<boolean>,
refresh,
execute
}
}