import Axios, { AxiosInstance, AxiosResponse, CancelToken } from 'axios' import deepmerge from 'deepmerge' import qs from 'qs' import { cancelResponse } from './filter/cancel-response' import { DefaultLogger } from './utils/default-logger' import { ILogger, LogLevel } from './intf/ILogger' import { IPlugin } from './intf/IPlugin' import { IRequestOptions } from './intf/IRequestOptions' import { max } from './filter/max' import { only } from './filter/only' import { serializeData } from './plugins/serialize-data' import { serializeUrl } from './plugins/serialize-url' import { loading } from './filter/loading' import { IData } from './intf/IData' import { transformNetworkError } from './filter/transform-network-error' import { reportError } from './filter/report-error' export { env } from './plugins/env' export { mock } from './plugins/mock' export { signature } from './plugins/signature' export { dynamicProxy } from './plugins/dynamic-proxy' export { filterNullValue } from './plugins/filter-null-value' export { loading } from './plugins/loading' export { transformRequest } from './plugins/transform-request' export { transformResponse } from './plugins/transform-response' export { errorHandler } from './plugins/error-handler' export { IPlugin } from './intf/IPlugin' export { IEnv } from './intf/IEnv' export { IRequestOptions } from './intf/IRequestOptions' export { IData } from './intf/IData' export { ILogger } from './intf/ILogger' /** 基于 Axios的 http 请求库扩展 */ export default class Request { /** 自定义日志工具 */ private logger: ILogger /** axios 实例 */ public axios: AxiosInstance /** cancel function 实例 */ private cancelers: Array<{ url: string; canceler: (message?: string) => void }> = [] /** 已加载插件实例 */ private plugins: Array = [] constructor(options?: IRequestOptions) { /** 注入 logger */ this.logger = options?.logger ?? new DefaultLogger() /** 创建基础 axios 实例 */ this.axios = Axios.create({ withCredentials: true, timeout: 60 * 1000, timeoutErrorMessage: 'request timeout!', validateStatus: (status) => status < 400, ...options }) /** 挂载默认插件 */ this.use(serializeData()) // 默认添加: 请求参数序列化, 根据 'content-type' .use(serializeUrl()) // 默认添加: 请求url序列化, 附加: baseUrl, url 去除重复 '/' } /** 中断请求 */ public static reqeustAbout() { throw new Error('request about') } /** 引用插件 */ public use(plugin: IPlugin): Request { this.logger.log(LogLevel.Debug, 'use plugin:', plugin.pluginName) // > 向 plugin 注入 axios 实例, 让 plugin 自行注入配置 plugin.handler(this.axios, this.plugins) this.plugins.push(plugin.pluginName) return this } /** 中止所有在进行的请求 */ public cancelAll() { this.logger.log(LogLevel.Debug, 'cancel requests.') while (this.cancelers.length > 0) { const canceler = this.cancelers.shift() if (canceler) { this.logger.log(LogLevel.Trace, 'cacncel request:', canceler.url) canceler.canceler('request about') } } } /** 创建中止方法 */ private createCanceler(options: IRequestOptions): void { const { url } = options ?? {} if (!url) return const cancelToken: CancelToken = new Axios.CancelToken((canceler) => this.cancelers.push({ url, canceler })) options.cancelToken = cancelToken } /** 请求前检查 */ private precheck(options: IRequestOptions): void { const { url } = options if (!url) throw new Error(`缺少必要参数 'url'`) } /** 请求执行过程 */ @reportError() // 抛出异常 report to logger @transformNetworkError() // 处理网络错误, request timeout。转化为标准响应格式. @cancelResponse() // 过滤器 - 此过滤器将拦截 cancel 产生的响应 (未考虑问题, 终止后, 在 only, max中缓存的请求如何处理) @only() // 过滤器 - 唯一请求 @loading() // 过滤器 loading @max() // 过滤器 - 最大请求并发 private async execute(options: IRequestOptions): Promise { try { this.createCanceler(options) // > execute const res: AxiosResponse = await this.axios.request(options) // ? 如果响应值类型是 origin, 则返回原始的 AxiosResponse 对象 return options.responseData === 'origin' ? res : res.data } catch (error) { if (error instanceof Error) { throw error } else if (typeof error === 'string') { throw new Error(error) } else { throw error } } } /** 添加 header */ appendHeader(key: string, value: any) { this.axios.defaults.headers.common[key] = value as string } /** 移除 header */ removeHeader(key: string) { delete this.axios.defaults.headers.common[key] } /** 设置 header */ setHeaders(headers: IData) { Object.assign(this.axios.defaults.headers.common, headers) } /** 发起请求 */ public async request(options: IRequestOptions): Promise { // @ merge options options = deepmerge(this.axios.defaults, options) // ? pre check options this.precheck(options) // > execute return await this.execute(options) } /** get 请求 */ public async get(url: string, params?: any, options?: IRequestOptions): Promise { return await this.request({ method: 'GET', url, params, ...options }) } /** post 请求 */ public async post(url: string, data?: any, options?: IRequestOptions): Promise { return await this.request({ method: 'POST', url, data, ...options }) } /** delete 请求 */ public async delete(url: string, params?: any, options?: IRequestOptions): Promise { return await this.request({ method: 'DELETE', url, params, ...options }) } /** head 请求 */ public async head(url: string, params?: any, options?: IRequestOptions): Promise { return await this.request({ method: 'HEAD', url, params, ...options }) } /** options 请求 */ public async options(url: string, params?: any, options?: IRequestOptions): Promise { return await this.request({ method: 'OPTIONS', url, params, ...options }) } /** put 请求 */ public async put(url: string, data?: any, options?: IRequestOptions): Promise { return await this.request({ method: 'PUT', url, data, ...options }) } /** patch 请求 */ public async patch(url: string, data?: any, options?: IRequestOptions): Promise { return await this.request({ method: 'PATCH', url, data, ...options }) } /** 发送无响应请求, 用于在特定时机(如: 浏览器关闭)时, 保证埋点等功能上报成功 * @description 注意数据量不要太大, 否则将影响请求上波啊稳定性! */ public sendBeacon(url: string, params?: any) { const isAbsoluteURL = (url: string): boolean => { // A URL is considered absolute if it begins with "://" or "//" (protocol-relative URL). // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed // by any combination of letters, digits, plus, period, or hyphen. return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url) } const combineURLs = (baseURL: string, relativeURL: string): string => { return relativeURL ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '') : baseURL } if (this.axios.defaults.baseURL && isAbsoluteURL(url)) { url = combineURLs(this.axios.defaults.baseURL, url) } if (params) { url += (url.indexOf('?') === -1 ? '?' : '&') + qs.stringify(params) } this.logger.log('navigator.sendBeacon() ->', url) // > send data navigator.sendBeacon?.(url) } }