request.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import axios from 'axios';
  2. class RequestHttp {
  3. constructor(config) {
  4. this.instance = axios.create(config);
  5. this.abortControllers = new Map();
  6. this._setupInterceptors();
  7. }
  8. _generateRequestKey(config) {
  9. const { method, url, params, data } = config;
  10. return `${method}-${url}-${JSON.stringify(params || {})}-${JSON.stringify(data || {})}`;
  11. }
  12. _addPendingRequest(config) {
  13. const requestKey = this._generateRequestKey(config);
  14. const controller = new AbortController();
  15. config.signal = controller.signal;
  16. this.abortControllers.set(requestKey, controller);
  17. }
  18. _removePendingRequest(config, cancel = false) {
  19. const requestKey = this._generateRequestKey(config);
  20. if (this.abortControllers.has(requestKey)) {
  21. if (cancel) {
  22. this.abortControllers.get(requestKey).abort();
  23. }
  24. this.abortControllers.delete(requestKey);
  25. }
  26. }
  27. _setupInterceptors() {
  28. // 请求拦截器
  29. this.instance.interceptors.request.use(
  30. (config) => {
  31. // 防重复请求
  32. if (config.cancelDuplicate !== false) {
  33. this._removePendingRequest(config, true);
  34. this._addPendingRequest(config);
  35. }
  36. // 注入 Token
  37. if (config.withToken !== false) {
  38. const token = localStorage.getItem('token');
  39. if (token && config.headers) {
  40. config.headers.Authorization = `Bearer ${token}`;
  41. }
  42. }
  43. return config;
  44. },
  45. (error) => {
  46. return Promise.reject(error);
  47. }
  48. );
  49. // 响应拦截器
  50. this.instance.interceptors.response.use(
  51. (response) => {
  52. this._removePendingRequest(response.config);
  53. const { data, config } = response;
  54. // 业务成功:脱壳返回
  55. if (data.code === 200) {
  56. return data.data;
  57. }
  58. // 业务错误
  59. if (!config.skipGlobalError) {
  60. console.error('Business Error:', data.message);
  61. }
  62. // Token 过期:仅在非 skipGlobalError 的请求上触发整站跳登录页,
  63. // 否则像登录接口(凭证错也是 401)会被强制 reload,覆盖掉本地 hint。
  64. if (data.code === 401 && !config.skipGlobalError) {
  65. console.warn('Token已过期,请重新登录');
  66. localStorage.removeItem('token');
  67. window.location.href = '/login';
  68. }
  69. return Promise.reject(new Error(data.message || 'Error'));
  70. },
  71. (error) => {
  72. if (axios.isCancel(error)) {
  73. return Promise.reject('Request Canceled by Duplicate Prevention');
  74. }
  75. if (error.config) {
  76. this._removePendingRequest(error.config);
  77. }
  78. const status = error.response?.status;
  79. let errorMsg = error.message;
  80. switch (status) {
  81. case 400: errorMsg = '请求参数错误'; break;
  82. case 401: errorMsg = '未授权,请重新登录'; break;
  83. case 403: errorMsg = '拒绝访问'; break;
  84. case 404: errorMsg = '请求地址不存在'; break;
  85. case 500: errorMsg = '服务器内部错误'; break;
  86. case 502: errorMsg = '网关错误'; break;
  87. case 503: errorMsg = '服务不可用'; break;
  88. case 504: errorMsg = '网关超时'; break;
  89. default: errorMsg = '网络连接异常';
  90. }
  91. console.error('HTTP Error:', errorMsg);
  92. return Promise.reject(error);
  93. }
  94. );
  95. }
  96. get(url, config) {
  97. return this.instance.get(url, config);
  98. }
  99. post(url, data, config) {
  100. return this.instance.post(url, data, config);
  101. }
  102. put(url, data, config) {
  103. return this.instance.put(url, data, config);
  104. }
  105. delete(url, config) {
  106. return this.instance.delete(url, config);
  107. }
  108. }
  109. export const http = new RequestHttp({
  110. baseURL: process.env.VUE_APP_API_BASE || '',
  111. timeout: Number(process.env.VUE_APP_REQUEST_TIMEOUT) || 15000,
  112. cancelDuplicate: true,
  113. });