@twotwoba/vv-cli
Version:
Easily create Vite + React/Vue3 project with TailwindCSS and other useful libraries. Also support Chrome extension.
129 lines (108 loc) • 3.89 kB
text/typescript
import { getAuthToken, isPublicApi } from "@/lib/auth"
import { FetchError, processData, resolveError } from "./server-helper"
import { AUTH_KEY } from "@/lib/global-keys"
import { filterObjNull } from "@/lib/utils"
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"
export interface FetcherOptions {
baseURL?: string
method?: HttpMethod
}
interface RequestConfig extends Omit<RequestInit, "body"> {
body?: Record<string, unknown> | FormData | string
params?: Record<string, unknown>
}
// ============================================================================
// Core Fetcher
// ============================================================================
/**
* 构建请求 headers,每次请求创建新对象避免并发问题
*/
const buildHeaders = (customHeaders?: HeadersInit): HeadersInit => {
const headers: Record<string, string> = {
"Content-Type": "application/json"
}
const token = getAuthToken()
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
}
/**
* 核心 fetch 函数
*/
export const fetcher = async <T = unknown>(
endpoint: string,
options: RequestConfig & FetcherOptions = {}
): Promise<T> => {
const {
baseURL = import.meta.env.VITE_API_URL,
method = "GET",
params,
body,
...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(跳过公开接口的 token)
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 response = await fetch(url, {
...rest,
method,
headers,
body: finalBody
})
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
}