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() // SSR 上下文类型从 ssrContext.ts 引入 // useFetch 的配置选项 interface UseFetchOptions { key?: string server?: boolean default?: () => any transform?: (data: any) => T onError?: (error: Error) => void } // useFetch 返回值类型 interface UseFetchReturn { data: Ref error: Ref pending: Ref refresh: () => Promise execute: () => Promise } /** * SSR 兼容的 useFetch hook * 支持服务端预取和客户端水合 */ export function useFetch( url: string | (() => string) | (() => Promise), options: UseFetchOptions = {} ): UseFetchReturn { const { key, server = true, default: defaultValue, transform, onError } = options // 生成缓存键 const cacheKey = key || (typeof url === 'string' ? url : `fetch-${Date.now()}`) // 响应式状态 const data = ref(null) const error = ref(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 => { 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 => { // 清除缓存 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, error: error as Ref, pending: pending as Ref, refresh, execute } }