359 lines
9.5 KiB
TypeScript
359 lines
9.5 KiB
TypeScript
type RequestConfig = {
|
|
url: string
|
|
method: string
|
|
headers: Record<string, string>
|
|
timeout?: number
|
|
body?: BodyInit | null
|
|
}
|
|
|
|
type ResponseWrapper<T = unknown> = {
|
|
data: T
|
|
status: number
|
|
statusText: string
|
|
headers: Headers
|
|
}
|
|
|
|
type RequestInterceptor = {
|
|
onFulfilled?: (config: RequestConfig) => RequestConfig | Promise<RequestConfig>
|
|
onRejected?: (error: unknown) => RequestConfig | Promise<RequestConfig>
|
|
}
|
|
|
|
type ResponseInterceptor = {
|
|
onFulfilled?: (
|
|
response: ResponseWrapper<unknown>
|
|
) => ResponseWrapper<unknown> | Promise<ResponseWrapper<unknown>>
|
|
onRejected?: (error: unknown) => unknown | Promise<unknown>
|
|
}
|
|
|
|
export class RequestExtend {
|
|
private baseURL: string
|
|
private timeout: number
|
|
private headers: Record<string, string>
|
|
|
|
private static defaultBaseURL = ''
|
|
private static defaultTimeout = 30000
|
|
private static defaultHeaders: Record<string, string> = {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
|
|
private static requestInterceptors: RequestInterceptor[] = []
|
|
private static responseInterceptors: ResponseInterceptor[] = []
|
|
|
|
constructor(config?: {
|
|
baseURL?: string
|
|
timeout?: number
|
|
headers?: Record<string, string>
|
|
}) {
|
|
this.baseURL = config?.baseURL || RequestExtend.defaultBaseURL
|
|
this.timeout = config?.timeout || RequestExtend.defaultTimeout
|
|
this.headers = {
|
|
...RequestExtend.defaultHeaders,
|
|
...config?.headers
|
|
}
|
|
}
|
|
|
|
static setDefaultConfig(config: {
|
|
baseURL?: string
|
|
timeout?: number
|
|
headers?: Record<string, string>
|
|
}) {
|
|
if (config.baseURL) {
|
|
this.defaultBaseURL = config.baseURL
|
|
}
|
|
|
|
if (config.timeout) {
|
|
this.defaultTimeout = config.timeout
|
|
}
|
|
|
|
if (config.headers) {
|
|
this.defaultHeaders = { ...this.defaultHeaders, ...config.headers }
|
|
}
|
|
}
|
|
|
|
static addRequestInterceptor(interceptor: RequestInterceptor) {
|
|
this.requestInterceptors.push(interceptor)
|
|
}
|
|
|
|
static addResponseInterceptor(interceptor: ResponseInterceptor) {
|
|
this.responseInterceptors.push(interceptor)
|
|
}
|
|
|
|
private async executeRequestInterceptors(config: RequestConfig): Promise<RequestConfig> {
|
|
let result = config
|
|
|
|
for (const interceptor of RequestExtend.requestInterceptors) {
|
|
try {
|
|
if (interceptor.onFulfilled) {
|
|
result = await interceptor.onFulfilled(result)
|
|
}
|
|
} catch (error) {
|
|
if (interceptor.onRejected) {
|
|
result = await interceptor.onRejected(error)
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
private async executeResponseInterceptors(
|
|
response: ResponseWrapper<unknown>
|
|
): Promise<ResponseWrapper<unknown>> {
|
|
let result = response
|
|
|
|
for (const interceptor of RequestExtend.responseInterceptors) {
|
|
try {
|
|
if (interceptor.onFulfilled) {
|
|
result = await interceptor.onFulfilled(result)
|
|
}
|
|
} catch (error) {
|
|
if (interceptor.onRejected) {
|
|
const interceptedError = await interceptor.onRejected(error)
|
|
throw interceptedError
|
|
}
|
|
|
|
throw error
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
private async executeResponseErrorInterceptors(error: unknown): Promise<unknown> {
|
|
let result = error
|
|
|
|
for (const interceptor of RequestExtend.responseInterceptors) {
|
|
if (!interceptor.onRejected) {
|
|
continue
|
|
}
|
|
|
|
try {
|
|
result = await interceptor.onRejected(result)
|
|
} catch (interceptorError) {
|
|
result = interceptorError
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
private buildURL(url: string, params?: Record<string, unknown>): string {
|
|
let fullURL = url
|
|
|
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
fullURL = this.baseURL + url
|
|
}
|
|
|
|
if (params && Object.keys(params).length > 0) {
|
|
const searchParams = new URLSearchParams()
|
|
|
|
for (const key in params) {
|
|
const value = params[key]
|
|
if (value !== undefined && value !== null) {
|
|
searchParams.append(key, String(value))
|
|
}
|
|
}
|
|
|
|
const queryString = searchParams.toString()
|
|
if (queryString) {
|
|
fullURL += (fullURL.includes('?') ? '&' : '?') + queryString
|
|
}
|
|
}
|
|
|
|
return fullURL
|
|
}
|
|
|
|
private async parseResponseBody(response: Response): Promise<unknown> {
|
|
const contentType = response.headers.get('content-type') || ''
|
|
|
|
if (contentType.includes('application/json')) {
|
|
return response.json()
|
|
}
|
|
|
|
if (contentType.includes('application/octet-stream')) {
|
|
return response.blob()
|
|
}
|
|
|
|
const text = await response.text()
|
|
if (!text) {
|
|
return text
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(text)
|
|
} catch {
|
|
return text
|
|
}
|
|
}
|
|
|
|
private getErrorMessage(result: unknown, fallback: string): string {
|
|
if (typeof result === 'string' && result.trim()) {
|
|
return result
|
|
}
|
|
|
|
if (result && typeof result === 'object') {
|
|
const payload = result as { msg?: string; message?: string }
|
|
return payload.msg || payload.message || fallback
|
|
}
|
|
|
|
return fallback
|
|
}
|
|
|
|
private async request<T = unknown>(
|
|
method: string,
|
|
url: string,
|
|
options: {
|
|
params?: Record<string, unknown>
|
|
data?: unknown
|
|
headers?: Record<string, string>
|
|
} = {}
|
|
): Promise<T> {
|
|
const { params, data, headers } = options
|
|
|
|
const config: RequestConfig = {
|
|
method: method.toUpperCase(),
|
|
headers: {
|
|
...this.headers,
|
|
...headers
|
|
},
|
|
timeout: this.timeout,
|
|
url: params && method.toUpperCase() === 'GET'
|
|
? this.buildURL(url, params)
|
|
: this.buildURL(url)
|
|
}
|
|
|
|
if (data && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
|
|
if (data instanceof FormData) {
|
|
config.body = data
|
|
delete config.headers['Content-Type']
|
|
} else {
|
|
config.body = JSON.stringify(data)
|
|
}
|
|
}
|
|
|
|
const interceptedConfig = await this.executeRequestInterceptors(config)
|
|
|
|
try {
|
|
const response = await fetch(interceptedConfig.url, {
|
|
method: interceptedConfig.method,
|
|
headers: interceptedConfig.headers,
|
|
body: interceptedConfig.body,
|
|
signal: interceptedConfig.timeout
|
|
? AbortSignal.timeout(interceptedConfig.timeout)
|
|
: undefined
|
|
})
|
|
|
|
const result = await this.parseResponseBody(response)
|
|
const wrappedResponse: ResponseWrapper<unknown> = {
|
|
data: result,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers: response.headers
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const handledError = await this.executeResponseErrorInterceptors({
|
|
...wrappedResponse,
|
|
message: this.getErrorMessage(result, response.statusText || 'Request failed')
|
|
})
|
|
throw handledError
|
|
}
|
|
|
|
const interceptedResponse = await this.executeResponseInterceptors(wrappedResponse)
|
|
return interceptedResponse.data as T
|
|
} catch (error) {
|
|
if (error && typeof error === 'object' && ('status' in error || 'code' in error)) {
|
|
throw error
|
|
}
|
|
|
|
const handledError = await this.executeResponseErrorInterceptors({
|
|
message: error instanceof Error ? error.message : '网络请求失败',
|
|
code:
|
|
error && typeof error === 'object' && 'code' in error
|
|
? (error as { code?: string }).code || 'NETWORK_ERROR'
|
|
: 'NETWORK_ERROR',
|
|
status:
|
|
error && typeof error === 'object' && 'status' in error
|
|
? Number((error as { status?: number }).status || 0)
|
|
: 0
|
|
})
|
|
|
|
throw handledError
|
|
}
|
|
}
|
|
|
|
async get<T = unknown>(url: string, options?: {
|
|
params?: Record<string, unknown>
|
|
headers?: Record<string, string>
|
|
}): Promise<T> {
|
|
return this.request<T>('GET', url, options)
|
|
}
|
|
|
|
async post<T = unknown>(url: string, data?: unknown, options?: {
|
|
params?: Record<string, unknown>
|
|
headers?: Record<string, string>
|
|
}): Promise<T> {
|
|
return this.request<T>('POST', url, { ...options, data })
|
|
}
|
|
|
|
async put<T = unknown>(url: string, data?: unknown, options?: {
|
|
params?: Record<string, unknown>
|
|
headers?: Record<string, string>
|
|
}): Promise<T> {
|
|
return this.request<T>('PUT', url, { ...options, data })
|
|
}
|
|
|
|
async delete<T = unknown>(url: string, options?: {
|
|
params?: Record<string, unknown>
|
|
headers?: Record<string, string>
|
|
}): Promise<T> {
|
|
return this.request<T>('DELETE', url, options)
|
|
}
|
|
|
|
async patch<T = unknown>(url: string, data?: unknown, options?: {
|
|
params?: Record<string, unknown>
|
|
headers?: Record<string, string>
|
|
}): Promise<T> {
|
|
return this.request<T>('PATCH', url, { ...options, data })
|
|
}
|
|
|
|
async upload<T = unknown>(url: string, file: File | FormData, options?: {
|
|
params?: Record<string, unknown>
|
|
headers?: Record<string, string>
|
|
}): Promise<T> {
|
|
const formData = file instanceof FormData ? file : new FormData()
|
|
|
|
if (file instanceof File) {
|
|
formData.append('file', file)
|
|
}
|
|
|
|
return this.request<T>('POST', url, {
|
|
...options,
|
|
data: formData
|
|
})
|
|
}
|
|
|
|
async download(url: string, filename?: string): Promise<void> {
|
|
const response = await fetch(this.buildURL(url), {
|
|
method: 'GET',
|
|
headers: this.headers
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('下载失败')
|
|
}
|
|
|
|
const blob = await response.blob()
|
|
const downloadURL = window.URL.createObjectURL(blob)
|
|
const link = document.createElement('a')
|
|
link.href = downloadURL
|
|
link.download = filename || 'download'
|
|
document.body.appendChild(link)
|
|
link.click()
|
|
document.body.removeChild(link)
|
|
window.URL.revokeObjectURL(downloadURL)
|
|
}
|
|
}
|