request.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  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']
  81. const token =
  82. localStorage.getItem('token_' + enterpriseCode) ||
  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 = 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. this.responseInterceptorId = this.instance.interceptors.response.use(
  108. customInterceptor,
  109. customErrorInterceptor
  110. )
  111. }
  112. private getRequestKey(config: AxiosRequestConfig): RequestKey | undefined {
  113. if (!config.url) return undefined
  114. return `${config.method?.toUpperCase()}-${config.url}`
  115. }
  116. private setupCancelController(
  117. config: AxiosRequestConfig,
  118. requestKey?: RequestKey
  119. ): AxiosRequestConfig {
  120. const key = requestKey || this.getRequestKey(config)
  121. if (!key) return config
  122. // 如果已有相同key的请求,先取消它
  123. this.cancelRequest(key)
  124. const controller = new AbortController()
  125. this.abortControllers.set(key, controller)
  126. return {
  127. ...config,
  128. signal: controller.signal
  129. }
  130. }
  131. public removeRequestInterceptor(): void {
  132. if (this.requestInterceptorId !== undefined) {
  133. this.instance.interceptors.request.eject(this.requestInterceptorId)
  134. this.requestInterceptorId = undefined // 重置ID,避免重复移除
  135. }
  136. }
  137. public removeResponseInterceptor(): void {
  138. if (this.responseInterceptorId !== undefined) {
  139. this.instance.interceptors.response.eject(this.responseInterceptorId)
  140. this.responseInterceptorId = undefined // 重置ID,避免重复移除
  141. }
  142. }
  143. public setRequestInterceptor(
  144. customInterceptor?: InterceptorsConfig['requestInterceptor'],
  145. customErrorInterceptor?: InterceptorsConfig['requestErrorInterceptor']
  146. ): void {
  147. this.removeRequestInterceptor()
  148. this.initRequestInterceptor(customInterceptor, customErrorInterceptor)
  149. }
  150. public setResponseInterceptor(
  151. customInterceptor?: InterceptorsConfig['responseInterceptor'],
  152. customErrorInterceptor?: InterceptorsConfig['responseErrorInterceptor']
  153. ): void {
  154. this.removeResponseInterceptor()
  155. this.initResponseInterceptor(customInterceptor, customErrorInterceptor)
  156. }
  157. public getInstance(): AxiosInstance {
  158. return this.instance
  159. }
  160. public cancelRequest(key: RequestKey, message?: string): boolean {
  161. const controller = this.abortControllers.get(key)
  162. if (controller) {
  163. controller.abort(message || `取消请求: ${String(key)}`)
  164. this.abortControllers.delete(key)
  165. return true
  166. }
  167. return false
  168. }
  169. public cancelAllRequests(message?: string): void {
  170. this.abortControllers.forEach((controller, key) => {
  171. controller.abort(message || `取消所有请求: ${String(key)}`)
  172. })
  173. this.abortControllers.clear()
  174. }
  175. public static isCancel(error: unknown): boolean {
  176. return Axios.isCancel(error)
  177. }
  178. private sleep(ms: number): Promise<void> {
  179. return new Promise((resolve) => setTimeout(resolve, ms))
  180. }
  181. /**
  182. * 通用请求方法
  183. * @param url 请求地址
  184. * @param config 请求配置
  185. * @returns 响应数据
  186. */
  187. public async request<T = any>(
  188. url: string,
  189. config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
  190. ): Promise<ResponseData<T>> {
  191. const { requestKey, retry, method, ...restConfig } = config || {}
  192. const defaultRetryCondition = (error: AxiosError) => {
  193. // 默认只重试网络错误或5xx服务器错误
  194. return !error.response || (error.response.status >= 500 && error.response.status < 600)
  195. }
  196. const retryConfig = {
  197. retries: 0,
  198. retryDelay: 1000,
  199. retryCondition: defaultRetryCondition,
  200. ...retry
  201. }
  202. let lastError: any
  203. const key = requestKey || this.getRequestKey({ ...restConfig, method, url })
  204. for (let attempt = 0; attempt <= retryConfig.retries; attempt++) {
  205. try {
  206. if (attempt > 0 && key) {
  207. this.abortControllers.delete(key)
  208. }
  209. const requestConfig = this.setupCancelController({ ...restConfig, method, url }, requestKey)
  210. const response = await this.instance.request<ResponseData<T>>(requestConfig)
  211. return response.data
  212. } catch (error) {
  213. lastError = error
  214. if (
  215. attempt === retryConfig.retries ||
  216. !retryConfig.retryCondition(error as AxiosError) ||
  217. HttpClient.isCancel(error)
  218. ) {
  219. break
  220. }
  221. // 延迟后重试
  222. if (retryConfig.retryDelay > 0) {
  223. await this.sleep(retryConfig.retryDelay)
  224. }
  225. }
  226. }
  227. return Promise.reject(lastError)
  228. }
  229. /**
  230. * GET 请求
  231. * @param url 请求地址
  232. * @param config 请求配置
  233. * @returns 响应数据
  234. */
  235. public get<T = any>(
  236. url: string,
  237. config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
  238. ): Promise<ResponseData<T>> {
  239. return this.request<T>(url, { ...config, method: 'GET' })
  240. }
  241. /**
  242. * POST 请求
  243. * @param url 请求地址
  244. * @param data 请求数据
  245. * @param config 请求配置
  246. * @returns 响应数据
  247. */
  248. public post<T = any>(
  249. url: string,
  250. data?: any,
  251. config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
  252. ): Promise<ResponseData<T>> {
  253. return this.request<T>(url, { ...config, data, method: 'POST' })
  254. }
  255. /**
  256. * PUT 请求
  257. * @param url 请求地址
  258. * @param data 请求数据
  259. * @param config 请求配置
  260. * @returns 响应数据
  261. */
  262. public put<T = any>(
  263. url: string,
  264. data?: any,
  265. config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
  266. ): Promise<ResponseData<T>> {
  267. return this.request<T>(url, { ...config, data, method: 'PUT' })
  268. }
  269. /**
  270. * DELETE 请求
  271. * @param url 请求地址
  272. * @param config 请求配置
  273. * @returns 响应数据
  274. */
  275. public delete<T = any>(
  276. url: string,
  277. config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
  278. ): Promise<ResponseData<T>> {
  279. return this.request<T>(url, { ...config, method: 'DELETE' })
  280. }
  281. /**
  282. * PATCH 请求
  283. * @param url 请求地址
  284. * @param data 请求数据
  285. * @param config 请求配置
  286. * @returns 响应数据
  287. */
  288. public patch<T = any>(
  289. url: string,
  290. data?: any,
  291. config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
  292. ): Promise<ResponseData<T>> {
  293. return this.request<T>(url, { ...config, data, method: 'PATCH' })
  294. }
  295. }
  296. const http = new HttpClient()
  297. // 导出request供api-service使用
  298. export const request = http.request.bind(http)
  299. export default http