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.
219 lines
5.8 KiB
219 lines
5.8 KiB
import { ref, onMounted, onServerPrefetch, Ref } from 'vue'
|
|
import { getCurrentInstance } from 'vue'
|
|
import type { SSRContext } from './ssrContext'
|
|
import { resolveSSRContext } from './ssrContext'
|
|
|
|
// 全局数据缓存,用于 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
|
|
}
|
|
}
|
|
|