import {HttpStatusError} from "./errors/httpStatusError"; import {BaseError} from "./errors/baseError"; import qs from "qs" import urijs from "urijs" export declare interface FetchAdapterConfig { baseUrl?: string | Array enableCancelRepeatRequest?: boolean, enableResponseHidden?: boolean,//在浏览器的Response隐藏后端返回的数据 requestInitConfig?: RequestInit, requestInterceptor?: (params: RequestInterceptorParams) => Promise //超时 timeout?: number pingTestTimeout?: number responseInterceptor?: ( { response, request, requestMeta }: { response: FetchAdapterResponse, request: Request, requestMeta?: RequestMeta } ) => Promise<{ response: FetchAdapterResponse, request: Request, requestMeta?: RequestMeta }> responseErrorInterceptor?: (error: BaseError | Error | any) => Promise, successStatusCode?: Array } export enum FetchAdapterMethod { GET = "GET", POST = "POST", PUT = "PUT", DELETE = "DELETE", SEARCH = "SEARCH", PATCH = "PATCH", } export declare interface FetchAdapterRequestConfig { url: string, requestInitConfig?: RequestInit, params?: BodyInit | null, enableCancelRepeatRequest?: boolean, } export declare interface RequestMeta { requestSentAt?: number //创建请求的时间戳 单位:毫秒 requestCompleteAt?: number //结束请求的时间戳 单位:毫秒 requestWaitingTime?: number //http连接时间戳 单位:毫秒 } export declare interface RequestInterceptorParams { url: string, config: RequestInit, requestMeta?: RequestMeta } const getHost = (url: string) => { //为了兼容RN端 const parseURL = URL ?? urijs const _url = new parseURL(url) // @ts-ignore return typeof _url.host === "string" ? _url.host : _url.host() } /** * ## FetchAdapterResponse **http响应体** * **/ export class FetchAdapterResponse extends Response { constructor(body?: BodyInit | null, init?: ResponseInit) { super(body, init); } /** 将headers转换成json形式 **/ public async getTransformHeaders(): Promise { const headerEntries = this.headers.entries() const headers = {} for (let [key, value] of headerEntries) { headers[key] = value } return headers } } /** * ## FetchAdapter **http适配器** * **/ export class FetchAdapter { public readonly baseRequestConfig: RequestInit = { headers: { "content-type": "application/json; charset=utf-8" } } public readonly requestTaskControllerList: Map = new Map() public readonly enableCancelRepeatRequest: boolean = true public baseUrl: Array = [] public readonly successStatusCode: Array = [200, 201, 301, 302, 303, 307, 308] public readonly enableResponseHidden: boolean = false public readonly timeout: number = 30000 public readonly pingTestTimeout: number = 1000 private readonly httpQualityStore = {} constructor(config: FetchAdapterConfig = {}) { const { baseUrl, requestInitConfig, requestInterceptor, responseInterceptor, responseErrorInterceptor, enableCancelRepeatRequest, successStatusCode, enableResponseHidden, timeout, pingTestTimeout, } = config this.baseRequestConfig = {...this.baseRequestConfig, ...requestInitConfig} if (typeof baseUrl === "string") { this.baseUrl = [baseUrl] } else { this.baseUrl = baseUrl ?? [] } this.requestInterceptor = requestInterceptor ?? this.requestInterceptor this.responseInterceptor = responseInterceptor ?? this.responseInterceptor this.responseErrorInterceptor = responseErrorInterceptor ?? this.responseErrorInterceptor this.enableCancelRepeatRequest = enableCancelRepeatRequest ?? this.enableCancelRepeatRequest this.successStatusCode = successStatusCode ?? this.successStatusCode this.enableResponseHidden = enableResponseHidden ?? this.enableResponseHidden this.timeout = timeout ?? this.timeout this.pingTestTimeout = pingTestTimeout ?? this.pingTestTimeout } /** 测速 **/ public async runPingTest({isIgnoreHttpErrors = false}: { isIgnoreHttpErrors?: boolean } = {}): Promise { if (!this.baseUrl || this.baseUrl.length <= 1) return // @ts-ignore const baseConfig: Request = {redirect: "follow", keepalive: true} const queue: Array> = [] for (let url of this.baseUrl) { queue.push(new Promise(async (resolve, reject) => { const startTime = new Date().getTime() const abortController = new AbortController() setTimeout(async () => { abortController.abort() }, this.pingTestTimeout) try { await fetch(url, {...baseConfig, signal: abortController.signal}) const endTime = new Date().getTime() return resolve({url, time: endTime - startTime,}) } catch (e) { const endTime = new Date().getTime() if (isIgnoreHttpErrors) { return resolve({url, time: endTime - startTime,}) } else { return reject({url, time: endTime - startTime,}) } } })) } const resultsList = await Promise.allSettled(queue) let successList = resultsList.filter(value => value.status === "fulfilled") successList.sort((a, b) => { // @ts-ignore return a.value.time - b.value.time }) //全部测试地址都死亡的话 则排列顺序按照用户的最初设定 if (successList.length !== 0) { // @ts-ignore this.baseUrl = successList.map(value => value.value.url) } } //达到一定条件 根据每个baseurl的请求分数 重新排序 private async resortBaseUrl() { if (this.baseUrl.length <= 1) return const baseUrlToString = JSON.stringify(this.baseUrl) //排序条件阀值 当totalTimes可以被threshold整除的时候 就会重新排序 const threshold = 100 let totalTimes = 0 let httpQualityStoreToBaseUrl: string[] = [] for (let host of Object.keys(this.httpQualityStore)) { if (baseUrlToString.includes(host)) { const val = this.httpQualityStore[host] totalTimes += val.successesTime httpQualityStoreToBaseUrl.push(host) } } if (totalTimes >= threshold && Math.round(totalTimes % threshold) === 0) { httpQualityStoreToBaseUrl = [...new Set(httpQualityStoreToBaseUrl)] httpQualityStoreToBaseUrl.sort((a, b) => { return this.httpQualityStore[b].score - this.httpQualityStore[a].score }) // @ts-ignore const parallel: string[] = httpQualityStoreToBaseUrl.map(value => this.baseUrl.find(value1 => value1.includes(value))).filter(value => value && typeof value === "string") this.baseUrl = [...new Set([...parallel, ...this.baseUrl])] } } /** 获取动态url **/ public async getDynamicBaseUrl(): Promise { if (this.baseUrl.length <= 1) { return this.baseUrl[0] ?? "" } await this.resortBaseUrl() const weightList: string[] = [] for (let i = 0; i < this.baseUrl.length; i++) { const url = this.baseUrl[i] const count = this.baseUrl.length - i for (let j = 0; j < count; j++) { weightList.push(url) } } const index = Math.floor(Math.random() * weightList.length) return weightList[index] ?? "" } /** 对url以及query参数进行处理 **/ public async makeUrl(originalUrl, params?: Object | null): Promise { const url: string = await this.getDynamicBaseUrl() originalUrl = originalUrl.includes("http") ? originalUrl : `${url}${originalUrl}` let parsedOriginalQuery: any = "" if (originalUrl.includes("?")) { parsedOriginalQuery = originalUrl.slice(originalUrl.indexOf("?") + 1) parsedOriginalQuery = qs.parse(parsedOriginalQuery) params = {...parsedOriginalQuery, ...params} originalUrl = originalUrl.slice(0, originalUrl.indexOf("?")) } params = qs.stringify(params) if ((params as string).length !== 0) { params = `?${params}` } return `${originalUrl}${params}` } /** 对body参数进行处理 **/ public async makeBody(params?: BodyInit | null): Promise { if (!params) return null if (Object.prototype.toString.call(params) === "[object Object]") { return JSON.stringify(params) } return params } /** 生成请求token 用来取消重复的请求 **/ public async generateRequestToken(config: FetchAdapterRequestConfig): Promise { const params = {...config.requestInitConfig, url: config.url} const bodyToString = (body: any) => { if (Object.prototype.toString.call(body) === "[object FormData]") { return JSON.stringify(Array.from(body.entries())) } if (Object.prototype.toString.call(body) === "[object File]") { return body.lastModified + body.lastModifiedDate + body.name + body.type + body.size } return JSON.stringify(body) } return params.url + params.method + params.headers + bodyToString(params.body) } /** 生成取消请求的控制信号 **/ public async getRequestSignal(config: FetchAdapterRequestConfig, timeout?: number): Promise { timeout = timeout ?? this.timeout let signal: AbortSignal | null = null const requestToken = await this.generateRequestToken(config) const abortController = new AbortController() if (this.requestTaskControllerList.has(requestToken)) { // @ts-ignore this.requestTaskControllerList.get(requestToken).abort() this.requestTaskControllerList.set(requestToken, abortController) signal = abortController.signal } else { this.requestTaskControllerList.set(requestToken, abortController) signal = abortController.signal } setTimeout(async () => { abortController.abort() }, timeout) return signal } private async createHttpQualityInfo({request, requestMeta}: { request?: Request, requestMeta?: RequestMeta }) { if (!request || !requestMeta) return const host = getHost(request.url) if (host) { const maxNumber = Number.MAX_SAFE_INTEGER * 0.5 if (!this.httpQualityStore[host]) this.httpQualityStore[host] = { successesTime: 0, waitingTime: 0, minimumWaitingTime: 0, maximumWaitingTime: 0, score: 0 } //如果该库应用在服务端 //避免数字超出系统最大运算范围造成未知问题 if (this.httpQualityStore[host].successesTime > maxNumber || this.httpQualityStore[host].waitingTime > maxNumber || this.httpQualityStore[host].score > maxNumber) { this.httpQualityStore[host] = { successesTime: 0, waitingTime: 0, minimumWaitingTime: 0, maximumWaitingTime: 0, score: 0 } } const requestWaitingTime = Number(requestMeta.requestWaitingTime) if (requestWaitingTime && Number.isNaN(requestWaitingTime) === false) { this.httpQualityStore[host].waitingTime += requestWaitingTime this.httpQualityStore[host].successesTime++ if (requestWaitingTime < this.httpQualityStore[host].minimumWaitingTime || this.httpQualityStore[host].minimumWaitingTime === 0) { this.httpQualityStore[host].minimumWaitingTime = requestWaitingTime } if (requestWaitingTime > this.httpQualityStore[host].maximumWaitingTime || this.httpQualityStore[host].maximumWaitingTime === 0) { this.httpQualityStore[host].maximumWaitingTime = requestWaitingTime } //请求耗时理想值 const idealTime = 1000 //每个请求平均耗时 const averageTime = this.httpQualityStore[host].waitingTime / this.httpQualityStore[host].successesTime //分数公式 //平均耗时占50% 最小耗时占30% 最大耗时占20% //耗时分数 (理想值-花费的时间)* 分数占比=分数 //时间越比理想值小 则分数越高 this.httpQualityStore[host].score = ((idealTime - averageTime) * 0.5) + ((idealTime - this.httpQualityStore[host].minimumWaitingTime) * 0.3) + ((idealTime - this.httpQualityStore[host].maximumWaitingTime) * 0.2) } } } /** 构造基本的请求方法 **/ public request(config: FetchAdapterRequestConfig): Promise<{ response: FetchAdapterResponse, request: Request }> { return new Promise(async (resolve, reject) => { let url: string const enableCancelRepeatRequest: boolean = config.enableCancelRepeatRequest ?? this.enableCancelRepeatRequest config.requestInitConfig = config.requestInitConfig ?? {} config.requestInitConfig.method = config?.requestInitConfig?.method ?? FetchAdapterMethod.GET if ([FetchAdapterMethod.GET, FetchAdapterMethod.SEARCH].includes(config.requestInitConfig.method as FetchAdapterMethod)) { url = await this.makeUrl(config.url, config.params) } else { url = await this.makeUrl(config.url) if (config.params) { config.requestInitConfig.body = await this.makeBody(config.params) } } if (enableCancelRepeatRequest) { config.requestInitConfig.signal = config.requestInitConfig.signal ?? await this.getRequestSignal(config) } let requestInterceptorResult = await this.requestInterceptorBySystem({ url, config: {...this.baseRequestConfig, ...config.requestInitConfig} }) requestInterceptorResult = await this.requestInterceptor(requestInterceptorResult) const requestConfig = new Request(requestInterceptorResult.url, requestInterceptorResult.config) return await fetch(requestConfig) .then(async (response: Response) => { //*注意这里必须先转换成blob 否则react native端获取body.json()会出现空的情况 //*这里之所以要clone一个新对象 是因为如果在源对象转换成blob 会导致在浏览器的Response不显示后端的数据 //这意味着可以利用这个特性对Response进行轻度加密,这个特性至少可以在chrome100以下的浏览器生效 const cloneResponse = !this.enableResponseHidden ? response.clone() : response const transformBody = await cloneResponse.blob() const systemAfter = await this.responseInterceptorBySystem( { response: new FetchAdapterResponse(transformBody, response), request: requestConfig, requestMeta: requestInterceptorResult.requestMeta } ) resolve(await this.responseInterceptor(systemAfter)) }) .catch(async (error: Error) => { const requestMeta = requestInterceptorResult.requestMeta as RequestMeta //计算时间差 // @ts-ignore requestMeta.requestCompleteAt = new Date().getTime() // @ts-ignore requestMeta.requestWaitingTime = requestMeta.requestCompleteAt - requestMeta.requestSentAt const newError = new BaseError( { request: requestConfig, requestMeta, // @ts-ignore code: error.code ?? 0, message: error.message, response: new FetchAdapterResponse(), stack: error.stack, // @ts-ignore cause: error.cause } ) this.responseErrorInterceptorBySystem(newError) .then(async error2 => { try { resolve(await this.responseErrorInterceptor(error2)) } catch (e) { reject(e) } }) .catch(async error2 => { try { resolve(await this.responseErrorInterceptor(error2)) } catch (e) { reject(e) } }) }) }) } /** 系统默认请求拦截器 **/ private async requestInterceptorBySystem(params: RequestInterceptorParams): Promise { params.requestMeta = { ...params.requestMeta, ...{ requestSentAt: new Date().getTime() } } return params } /** 请求拦截器 **/ private async requestInterceptor(params: RequestInterceptorParams): Promise { return params } /** 系统默认响应拦截器 **/ private async responseInterceptorBySystem( { response, request, requestMeta }: { response: FetchAdapterResponse, request: Request, requestMeta?: RequestMeta } ): Promise<{ response: FetchAdapterResponse, request: Request, requestMeta?: RequestMeta }> { // @ts-ignore requestMeta.requestCompleteAt = new Date().getTime() // @ts-ignore requestMeta.requestWaitingTime = requestMeta.requestCompleteAt - requestMeta.requestSentAt if (this.successStatusCode.includes(response.status) === false) { throw new HttpStatusError({code: response.status, response, request, requestMeta}) } this.createHttpQualityInfo({request, requestMeta}) return {response, request, requestMeta} } /** 响应拦截器 **/ private async responseInterceptor( { response, request, requestMeta }: { response: FetchAdapterResponse, request: Request, requestMeta?: RequestMeta } ): Promise<{ response: FetchAdapterResponse, request: Request, requestMeta?: RequestMeta }> { return {response, request, requestMeta} } /** 系统默认错误拦截器 **/ private async responseErrorInterceptorBySystem(error: BaseError | Error | any): Promise { throw error } /** 错误拦截器 **/ private async responseErrorInterceptor(error: BaseError | Error | any): Promise { throw error } public async get(url: string, params?: Object | BodyInit | null, enableCancelRepeatRequest?: boolean): Promise { return await this.request({ url, // @ts-ignore params, enableCancelRepeatRequest, requestInitConfig: {method: FetchAdapterMethod.GET} }) } public async post(url: string, params?: Object | BodyInit | null, enableCancelRepeatRequest?: boolean): Promise { return await this.request({ url, // @ts-ignore params, enableCancelRepeatRequest, requestInitConfig: {method: FetchAdapterMethod.POST} }) } public async put(url: string, params?: Object | BodyInit | null, enableCancelRepeatRequest?: boolean): Promise { return await this.request({ url, // @ts-ignore params, enableCancelRepeatRequest, requestInitConfig: {method: FetchAdapterMethod.PUT} }) } public async delete(url: string, params?: Object | BodyInit | null, enableCancelRepeatRequest?: boolean): Promise { return await this.request({ url, // @ts-ignore params, enableCancelRepeatRequest, requestInitConfig: {method: FetchAdapterMethod.DELETE} }) } public async search(url: string, params?: Object | BodyInit | null, enableCancelRepeatRequest?: boolean): Promise { return await this.request({ url, // @ts-ignore params, enableCancelRepeatRequest, requestInitConfig: {method: FetchAdapterMethod.SEARCH} }) } public async patch(url: string, params?: Object | BodyInit | null, enableCancelRepeatRequest?: boolean): Promise { return await this.request({ url, // @ts-ignore params, enableCancelRepeatRequest, requestInitConfig: {method: FetchAdapterMethod.PATCH} }) } }