UNPKG

@twotwoba/vv-cli

Version:

Easily create Vite + React19/Vue3 web/h5/mini-program/chrome-extension projects.

166 lines (138 loc) 5.08 kB
import { filterObjNull } from '@/utils' import { FetchError, processData, resolveError } from './server-helper' // ============================================================================ // Types // ============================================================================ type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' interface RequestConfig extends Omit<RequestInit, 'body' | 'signal'> { body?: Record<string, unknown> | FormData | string params?: Record<string, unknown> } interface FetcherOptions { baseURL?: string method?: HttpMethod timeout?: number } // ============================================================================ // Constants // ============================================================================ const AUTH_KEY = 'auth_token' const DEFAULT_TIMEOUT = 10_000 // ============================================================================ // Helpers // ============================================================================ /** * 构建请求 headers */ const buildHeaders = (customHeaders?: HeadersInit): HeadersInit => { const headers: Record<string, string> = { 'Content-Type': 'application/json' } const token = localStorage.getItem(AUTH_KEY) if (token) { headers[AUTH_KEY] = token } return { ...headers, ...(customHeaders as Record<string, string>) } } /** * 构建完整 URL */ const buildURL = (baseURL: string, endpoint: string, params?: Record<string, unknown>): string => { let url: string if (baseURL.startsWith('http://') || baseURL.startsWith('https://')) { const base = new URL(baseURL) const pathname = base.pathname === '/' ? '' : base.pathname const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}` url = new URL(pathname + normalizedEndpoint, base.origin).toString() } else { const separator = baseURL.endsWith('/') || endpoint.startsWith('/') ? '' : '/' url = baseURL + separator + endpoint.replace(/^\//, '') } if (params) { const filtered = filterObjNull(params) if (filtered && Object.keys(filtered).length > 0) { const searchParams = new URLSearchParams(filtered as Record<string, string>) url += (url.includes('?') ? '&' : '?') + searchParams.toString() } } return url } /** * 检查是否为公开 API */ const isPublicApi = (url: string): boolean => { return url.includes('/pass/') || url.startsWith('/pass') } // ============================================================================ // Core Fetcher // ============================================================================ /** * 核心 fetch 函数 */ export const fetcher = async <T = unknown>( endpoint: string, options: RequestConfig & FetcherOptions = {} ): Promise<T> => { const { baseURL = import.meta.env.VITE_FETCH_BASE_URL, method = 'GET', params, body, timeout = DEFAULT_TIMEOUT, ...rest } = options if (!endpoint) { throw new FetchError('Endpoint is required', 400) } // 构建 URL const shouldAppendParams = method === 'GET' || method === 'DELETE' const url = buildURL(baseURL, endpoint, shouldAppendParams ? params : undefined) // 构建 headers const skipAuth = isPublicApi(endpoint) const headers = buildHeaders(rest.headers) if (skipAuth) { delete (headers as Record<string, string>)[AUTH_KEY] } // 构建 body let finalBody: string | FormData | undefined const bodyData = body ?? (!shouldAppendParams ? params : undefined) if (bodyData) { if (bodyData instanceof FormData) { finalBody = bodyData delete (headers as Record<string, string>)['Content-Type'] } else if (typeof bodyData === 'string') { finalBody = bodyData } else { finalBody = JSON.stringify(bodyData) } } const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), timeout) try { const response = await fetch(url, { ...rest, method, headers, body: finalBody, signal: controller.signal }) if (!response.ok) { const msg = resolveError(response.status) throw new FetchError(msg, response.status, { url, method }) } const contentType = response.headers.get('content-type') if (contentType?.includes('application/json')) { const res = await response.json() return processData(res) as T } return response.text() as unknown as T } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') { throw new FetchError('请求超时,请稍后重试', 408, { url, method }) } throw error } finally { clearTimeout(timeoutId) } } export { FetchError, isFetchError } from './server-helper'