request.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. import Axios, {
  2. type AxiosInstance,
  3. type AxiosRequestConfig,
  4. type CustomParamsSerializer,
  5. type AxiosResponse,
  6. type InternalAxiosRequestConfig,
  7. type AxiosError
  8. } from 'axios'
  9. import { stringify } from 'qs'
  10. // 基础配置
  11. const defaultConfig: AxiosRequestConfig = {
  12. timeout: 6000,
  13. headers: {
  14. 'Content-Type': 'application/json;charset=utf-8'
  15. },
  16. paramsSerializer: {
  17. serialize: stringify as unknown as CustomParamsSerializer
  18. }
  19. }
  20. // 响应数据基础结构
  21. export interface BaseResponse {
  22. code: number
  23. error?: string
  24. isAuthorized?: boolean
  25. isSuccess?: boolean
  26. }
  27. type OmitBaseResponse<T> = Omit<T, keyof BaseResponse>
  28. export type ResponseData<T = any> = BaseResponse & OmitBaseResponse<T>
  29. export type ResponseValidator<T = any> = (data: ResponseData<T>) => boolean
  30. // 重试配置
  31. export interface RetryConfig {
  32. retries?: number
  33. retryDelay?: number
  34. retryCondition?: (error: AxiosError) => boolean
  35. }
  36. // 拦截器配置类型
  37. interface InterceptorsConfig {
  38. requestInterceptor?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
  39. requestErrorInterceptor?: (error: AxiosError) => Promise<any>
  40. responseInterceptor?: (response: AxiosResponse<ResponseData<any>>) => any
  41. responseErrorInterceptor?: (error: AxiosError) => Promise<any>
  42. }
  43. // 请求唯一键
  44. type RequestKey = string | symbol
  45. /**
  46. * 获取url参数
  47. * @param url
  48. * @returns
  49. */
  50. export function getParams(url: string) {
  51. const paramsString = url.split('?')[1]
  52. const searchParams = new URLSearchParams(paramsString)
  53. return Object.fromEntries(searchParams.entries())
  54. }
  55. class HttpClient {
  56. private instance: AxiosInstance
  57. private requestInterceptorId?: number
  58. private responseInterceptorId?: number
  59. private abortControllers: Map<RequestKey, AbortController> = new Map()
  60. constructor(customConfig?: AxiosRequestConfig, interceptors?: InterceptorsConfig) {
  61. this.instance = Axios.create({ ...defaultConfig, ...customConfig })
  62. this.initInterceptors(interceptors)
  63. }
  64. private initInterceptors(interceptors?: InterceptorsConfig): void {
  65. this.initRequestInterceptor(
  66. interceptors?.requestInterceptor,
  67. interceptors?.requestErrorInterceptor
  68. )
  69. this.initResponseInterceptor(
  70. interceptors?.responseInterceptor,
  71. interceptors?.responseErrorInterceptor
  72. )
  73. }
  74. private initRequestInterceptor(
  75. customInterceptor?: InterceptorsConfig['requestInterceptor'],
  76. customErrorInterceptor?: InterceptorsConfig['requestErrorInterceptor']
  77. ): void {
  78. const defaultInterceptor = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
  79. // const search = getParams(window.location.href)
  80. // const enterpriseCode = search?.['enterpriseCode'] || 'a'
  81. const token =
  82. localStorage.getItem('oauth2token') ||
  83. document.cookie.match(new RegExp('(^| )' + 'x-sessionId_b' + '=([^;]*)(;|$)'))?.[2]
  84. // 添加token
  85. if (token) {
  86. if (!config.headers) {
  87. config.headers = {} as InternalAxiosRequestConfig['headers']
  88. }
  89. config.headers.Authorization = JSON.parse(token) || token
  90. }
  91. return config
  92. }
  93. // 默认请求错误拦截器
  94. const defaultErrorInterceptor = (error: AxiosError): Promise<any> => {
  95. // todo 错误处理
  96. return Promise.reject(error)
  97. }
  98. this.requestInterceptorId = this.instance.interceptors.request.use(
  99. customInterceptor || defaultInterceptor,
  100. customErrorInterceptor || defaultErrorInterceptor
  101. )
  102. }
  103. private initResponseInterceptor(
  104. customInterceptor?: InterceptorsConfig['responseInterceptor'],
  105. customErrorInterceptor?: InterceptorsConfig['responseErrorInterceptor']
  106. ): void {
  107. const defaultInterceptor = (
  108. response: AxiosResponse<ResponseData<any>>
  109. ): AxiosResponse<ResponseData<any>> => {
  110. // 会话丢失 重定向到/登录页
  111. if (response.data?.code === 9999) {
  112. window.location.href = '/'
  113. }
  114. return response
  115. }
  116. this.responseInterceptorId = this.instance.interceptors.response.use(
  117. customInterceptor || defaultInterceptor,
  118. customErrorInterceptor
  119. )
  120. }
  121. private getRequestKey(config: AxiosRequestConfig): RequestKey | undefined {
  122. if (!config.url) return undefined
  123. return `${config.method?.toUpperCase()}-${config.url}`
  124. }
  125. private setupCancelController(
  126. config: AxiosRequestConfig,
  127. requestKey?: RequestKey
  128. ): AxiosRequestConfig {
  129. const key = requestKey || this.getRequestKey(config)
  130. if (!key) return config
  131. // 如果已有相同key的请求,先取消它
  132. this.cancelRequest(key)
  133. const controller = new AbortController()
  134. this.abortControllers.set(key, controller)
  135. return {
  136. ...config,
  137. signal: controller.signal
  138. }
  139. }
  140. public removeRequestInterceptor(): void {
  141. if (this.requestInterceptorId !== undefined) {
  142. this.instance.interceptors.request.eject(this.requestInterceptorId)
  143. this.requestInterceptorId = undefined // 重置ID,避免重复移除
  144. }
  145. }
  146. public removeResponseInterceptor(): void {
  147. if (this.responseInterceptorId !== undefined) {
  148. this.instance.interceptors.response.eject(this.responseInterceptorId)
  149. this.responseInterceptorId = undefined // 重置ID,避免重复移除
  150. }
  151. }
  152. public setRequestInterceptor(
  153. customInterceptor?: InterceptorsConfig['requestInterceptor'],
  154. customErrorInterceptor?: InterceptorsConfig['requestErrorInterceptor']
  155. ): void {
  156. this.removeRequestInterceptor()
  157. this.initRequestInterceptor(customInterceptor, customErrorInterceptor)
  158. }
  159. public setResponseInterceptor(
  160. customInterceptor?: InterceptorsConfig['responseInterceptor'],
  161. customErrorInterceptor?: InterceptorsConfig['responseErrorInterceptor']
  162. ): void {
  163. this.removeResponseInterceptor()
  164. this.initResponseInterceptor(customInterceptor, customErrorInterceptor)
  165. }
  166. public getInstance(): AxiosInstance {
  167. return this.instance
  168. }
  169. public cancelRequest(key: RequestKey, message?: string): boolean {
  170. const controller = this.abortControllers.get(key)
  171. if (controller) {
  172. controller.abort(message || `取消请求: ${String(key)}`)
  173. this.abortControllers.delete(key)
  174. return true
  175. }
  176. return false
  177. }
  178. public cancelAllRequests(message?: string): void {
  179. this.abortControllers.forEach((controller, key) => {
  180. controller.abort(message || `取消所有请求: ${String(key)}`)
  181. })
  182. this.abortControllers.clear()
  183. }
  184. public static isCancel(error: unknown): boolean {
  185. return Axios.isCancel(error)
  186. }
  187. private sleep(ms: number): Promise<void> {
  188. return new Promise((resolve) => setTimeout(resolve, ms))
  189. }
  190. /**
  191. * 通用请求方法
  192. * @param url 请求地址
  193. * @param config 请求配置
  194. * @returns 响应数据
  195. */
  196. public async request<T = any>(
  197. url: string,
  198. config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
  199. ): Promise<ResponseData<T>> {
  200. const { requestKey, retry, method, ...restConfig } = config || {}
  201. const defaultRetryCondition = (error: AxiosError) => {
  202. // 默认只重试网络错误或5xx服务器错误
  203. return !error.response || (error.response.status >= 500 && error.response.status < 600)
  204. }
  205. const retryConfig = {
  206. retries: 0,
  207. retryDelay: 1000,
  208. retryCondition: defaultRetryCondition,
  209. ...retry
  210. }
  211. let lastError: any
  212. const key = requestKey || this.getRequestKey({ ...restConfig, method, url })
  213. for (let attempt = 0; attempt <= retryConfig.retries; attempt++) {
  214. try {
  215. if (attempt > 0 && key) {
  216. this.abortControllers.delete(key)
  217. }
  218. const requestConfig = this.setupCancelController({ ...restConfig, method, url }, requestKey)
  219. const response = await this.instance.request<ResponseData<T>>(requestConfig)
  220. return response.data
  221. } catch (error) {
  222. lastError = error
  223. if (
  224. attempt === retryConfig.retries ||
  225. !retryConfig.retryCondition(error as AxiosError) ||
  226. HttpClient.isCancel(error)
  227. ) {
  228. break
  229. }
  230. // 延迟后重试
  231. if (retryConfig.retryDelay > 0) {
  232. await this.sleep(retryConfig.retryDelay)
  233. }
  234. }
  235. }
  236. return Promise.reject(lastError)
  237. }
  238. /**
  239. * GET 请求
  240. * @param url 请求地址
  241. * @param config 请求配置
  242. * @returns 响应数据
  243. */
  244. public get<T = any>(
  245. url: string,
  246. config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
  247. ): Promise<ResponseData<T>> {
  248. return this.request<T>(url, { ...config, method: 'GET' })
  249. }
  250. /**
  251. * POST 请求
  252. * @param url 请求地址
  253. * @param data 请求数据
  254. * @param config 请求配置
  255. * @returns 响应数据
  256. */
  257. public post<T = any>(
  258. url: string,
  259. data?: any,
  260. config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
  261. ): Promise<ResponseData<T>> {
  262. return this.request<T>(url, { ...config, data, method: 'POST' })
  263. }
  264. /**
  265. * PUT 请求
  266. * @param url 请求地址
  267. * @param data 请求数据
  268. * @param config 请求配置
  269. * @returns 响应数据
  270. */
  271. public put<T = any>(
  272. url: string,
  273. data?: any,
  274. config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
  275. ): Promise<ResponseData<T>> {
  276. return this.request<T>(url, { ...config, data, method: 'PUT' })
  277. }
  278. /**
  279. * DELETE 请求
  280. * @param url 请求地址
  281. * @param config 请求配置
  282. * @returns 响应数据
  283. */
  284. public delete<T = any>(
  285. url: string,
  286. config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
  287. ): Promise<ResponseData<T>> {
  288. return this.request<T>(url, { ...config, method: 'DELETE' })
  289. }
  290. /**
  291. * PATCH 请求
  292. * @param url 请求地址
  293. * @param data 请求数据
  294. * @param config 请求配置
  295. * @returns 响应数据
  296. */
  297. public patch<T = any>(
  298. url: string,
  299. data?: any,
  300. config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
  301. ): Promise<ResponseData<T>> {
  302. return this.request<T>(url, { ...config, data, method: 'PATCH' })
  303. }
  304. }
  305. const http = new HttpClient()
  306. // 导出request供api-service使用
  307. export const request = http.request.bind(http)
  308. export default http