Files
Kg.SeaTime/Web/src/extends/RequestExtend.ts

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)
}
}