更新 Web/src/extends/RequestExtend.ts
This commit is contained in:
358
Web/src/extends/RequestExtend.ts
Normal file
358
Web/src/extends/RequestExtend.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user