JSON 判断放在前面是因为接口或许不是按照标准 HTTP 返回状态码
Response 接口的只读属性
ok包含一个布尔值,表明响应是否成功(状态码在 200-299 范围内)
import { cloneDeep, delay, isBrowser, withTimeout } from 'es-toolkit'
import { toast } from 'sonner'
const core = async (promise: () => Promise<Response>, retry: number) => {
try {
const res = await promise()
const contentType = res.headers.get('Content-Type')
// JSON
if (contentType?.includes('application/json')) {
const data = await res.json()
if (res.ok) return data
throw new Error(JSON.stringify(data))
}
if (!res.ok) {
throw new Error(JSON.stringify(res.statusText))
}
// Text
if (contentType?.includes('text/plain')) {
return await res.text()
}
// Blob
return await res.blob()
} catch (error) {
if (retry > 1) {
await delay(200)
return core(promise, retry - 1)
}
const message = JSON.parse((error as Error).message)
const text = typeof message == 'string' ? message : JSON.stringify(message, null, 2)
if (isBrowser()) {
if (window.location.pathname.startsWith('/dashboard')) {
toast.error(text, { id: text, richColors: true })
}
}
return Promise.reject(text)
}
}
interface FetchOptions extends Omit<RequestInit, 'body' | 'headers' | 'method'> {
body?: any
headers?: Record<string, string>
method?: Method
}
/**
* 封装的基础请求方法
*/
export const ifetch = async <T = any>(input: RequestInfo | URL, { body, headers = {}, ...init }: FetchOptions = {}): Promise<T> => {
headers = cloneDeep(headers) // 避免修改原对象
if (!(body instanceof FormData)) {
if (!headers['Content-Type']) {
headers['Content-Type'] = 'application/json'
}
if (headers['Content-Type'].includes('application/json')) {
body = JSON.stringify(body)
}
}
if (typeof input == 'string') {
input = input.replace(/(?<!:)\/{2,}/g, '/')
if (input.endsWith('/')) {
input = input.slice(0, -1)
}
}
return core(async () => {
return withTimeout(async () => fetch(input, { body, headers, ...init }), 5000)
}, 3)
}