request.ts 9.8 KB

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