唯品秀前端博客

在实际项目开发过程中,我们可能会遇到这么一种情况,假设现在有一个tab,tab下对应一个表格,切换tab时候请求下面的表格对应数据,假设tab>a页签点击后未等数据返回用户切换到了tab>b页签下,这时候很可能造成下面表格渲染错误(因为ajax请求是异步的,很有可能上面一个接口请求到的数据最终渲染到当前页签下),面对这个问题,就需要用到axios取消请求了

AbortController

关于axios取消请求目前网上流传的方法主要都是基于老版本CancelToken去实现的参考,此 API 从 v0.22.0 开始已被弃用,不应在新项目中使用,这里不再详谈。从v0.22.0开始,Axios 支持以 fetch API 方式 — AbortController取消请求,官方示例:

1
2
3
4
5
6
7
8
9
const controller = new AbortController();

axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});
// 取消请求
controller.abort()

取消请求封装

单纯看上面官方demo就是看个寂寞,上面方式阻止了所有请求,无法满足我们项目实际需求,实际开发中我们还需要进行额外的封装,我们要达到的目的是当用户频繁请求的过程中,当有新的请求时候把前面还没返回的请求中止掉,只保留最后一次请求,确保最终页面数据渲染正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import type { AxiosRequestConfig } from 'axios';
import axios from 'axios';
import { ElMessage } from 'element-plus';
import { globalStore } from '@/store';
const GLOBAL_STORE = globalStore();
const { requests } = storeToRefs(GLOBAL_STORE);

const reload = () => {
  if (window.parent && window.parent !== window.self) {
    window.parent.location?.reload();
  }
};

const blob2json = (data: Blob): Promise<Record<string, any>> =>
  new Promise((resolve) => {
    const reader = new FileReader();
    reader.readAsText(data, 'utf-8');
    reader.onload = function () {
      const res = JSON.parse(reader.result as string);
      resolve(res);
    };
  });

// 处理接口内层 code
const resolveData = (data: any) => {
  if (data.code && data.code !== '200') {
    const msg = data.entity || '接口返回异常';
    ElMessage.error(msg);
    return Promise.reject(msg);
  }
  return data;
};

const service = axios.create({
  // baseURL: import.meta.env.VITE_APP_BASE_API as string,
  withCredentials: true,
  timeout: 100000,
  headers: { 'Content-Type': 'application/json;charset=utf-8' },
});

service.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    const controller = new AbortController(); // 每个请求时都新生成一个AbortController实例
    config.signal = controller.signal; // 设置请求的signal字段为new AbortController()的signal
    GLOBAL_STORE.$patch((state) => { // requests:请求列表存到状态管理pania的state中
      state.requests.push(controller);
    });

    const token = localStorage.getItem('UTOKEN');
    if (token && config.headers) {
      config.headers['Authorization'] = token;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

service.interceptors.response.use(
  async (response) => {
    console.log('requests.value', requests.value);
    requests.value.slice(0, -1).forEach((controller) => controller.abort()); // 通过遍历终止所有未完成的请求
    GLOBAL_STORE.$patch((state) => {
      state.requests = requests.value.slice(-1); // 执行完清空,从而不影响新页面的请求列表
    });

    const res: any = response.data;

    let { code } = res;
    if (response.request.responseType === 'blob') {
      const _res = await blob2json(res);
      if (_res && _res.code) {
        code = _res.code;
      }
    }

    if (code === '2002') {
      ElMessage.error('登录过期,请重新登录');
      reload();
    } else if (code !== '1001' && !Array.isArray(res)) {
      const msg = res.msg || '接口请求出错';
      ElMessage.error(msg);
      return Promise.reject(msg);
    } else {
      ElMessage({
        message: '请求成功',
        type: 'success',
      });
      return resolveData(res.data || res);
    }
  },
  (error) => {
    const msg = error && error.message ? error.message : error;
    ElMessage.error(msg);
    if (axios.isCancel(error)) { //中止后的请求会走这里
      console.error('请求被取消', error);
    }
    return Promise.reject('错误信息' + msg);
  }
);

export default service;

新的问题

上面方式虽然简单,但又显得过于粗暴,因为它每次有新的请求就会把前面没请求完成的全部中止,所以每次并发请求池中只能有最后一个请求成功,事实上我们进入一个页面请求可能多则数十个,这个时候上面方式肯定是不行的。因此我们还需要把每条接口存储起来

例如:进入页面后请求a、b、c、d四个接口,同时并发,于是我们将所有的接口以及状态对应存储,这时候就通过map方式无疑是最好的选择,当存储好了后,例如后面有新的请求a,我们会去找map中存储的状态值,如果发现前面的接口a还没请求完成,就把它中止,请求我们本次最新发出的请求a,从而完美解决上面问题

创建方法函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 新建abortController.js封装文件
export default class abortController {
  // 声明一个 Map 用于存储每个请求的标识 和 取消函数
  static pending = new Map();
  // 白名单, 写入接口名称
  static whiteRequest = [];

  /**
   * 得到该格式的url
   * @param {AxiosRequestConfig} config
   * @returns
   */

  static getUrl(config) {
    return [config.method, config.url].join('&');
  }

  /**
   * 添加请求
   * @param {AxiosRequestConfig} config
   */

  static addPending(config) {
    const url = this.getUrl(config);
    const controller = new AbortController(); // 每个请求时都新生成一个AbortController实例
    config.signal = controller.signal; // 设置请求的signal字段为new AbortController()的signal
    if (!this.pending.has(url)) {
      // 如果 pending 中不存在当前请求,则添加进去
      this.pending.set(url, controller);
    }
    console.log('添加请求', this.pending);
  }

  /**
   * 移除请求
   * @param {AxiosRequestConfig} config
   */

  static removePending(config) {
    const url = this.getUrl(config);
    const method = url.split('&')[1];
    if (this.pending.has(url) && !this.whiteRequest.includes(method)) {
      // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
      const controller = this.pending.get(url);
      controller.abort(); // 取消请求
      this.pending.delete(url);
    }
  }
  /**
   * 清空 pending 中的请求(在路由跳转时调用)
   */

  static clearPending() {
    for (const [url, controller] of this.pending) {
      controller.abort(); // 取消请求
    }
    this.pending.clear();
  }
}

加入到请求钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import type { AxiosRequestConfig } from 'axios';
import axios from 'axios';
import abortController from './abortController';
import { ElMessage } from 'element-plus';

const reload = () => {
  if (window.parent && window.parent !== window.self) {
    window.parent.location?.reload();
  }
};

const blob2json = (data: Blob): Promise<Record<string, any>> =>
  new Promise((resolve) => {
    const reader = new FileReader();
    reader.readAsText(data, 'utf-8');
    reader.onload = function () {
      const res = JSON.parse(reader.result as string);
      resolve(res);
    };
  });

// 处理接口内层 code
const resolveData = (data: any) => {
  if (data.code && data.code !== '200') {
    const msg = data.entity || '接口返回异常';
    ElMessage.error(msg);
    return Promise.reject(msg);
  }
  return data;
};

const service = axios.create({
  // baseURL: import.meta.env.VITE_APP_BASE_API as string,
  withCredentials: true,
  timeout: 100000,
  headers: { 'Content-Type': 'application/json;charset=utf-8' },
});

service.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    // 这里的判断用于处理白名单不参与取消请求
    if (!abortController.whiteRequest.includes(`${config.url}`)) {
      // 请求开始前,检查一下是否已经有该请求了,有则取消掉该请求
      abortController.removePending(config);
      // 把当前请求添加进去
      abortController.addPending(config);
    }
    console.log('config', config);

    const token = localStorage.getItem('UTOKEN');
    if (token && config.headers) {
      config.headers['Authorization'] = token;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

service.interceptors.response.use(
  async (response) => {
    // 接口响应之后把这次请求清除
    abortController.removePending(response.config);
    const res: any = response.data;

    let { code } = res;
    if (response.request.responseType === 'blob') {
      const _res = await blob2json(res);
      if (_res && _res.code) {
        code = _res.code;
      }
    }

    if (code === '2002') {
      ElMessage.error('登录过期,请重新登录');
      reload();
    } else if (code !== '1001' && !Array.isArray(res)) {
      const msg = res.msg || '接口请求出错';
      ElMessage.error(msg);
      return Promise.reject(msg);
    } else {
      ElMessage({
        message: '请求成功',
        type: 'success',
      });
      return resolveData(res.data || res);
    }
  },
  (error) => {
    const msg = error && error.message ? error.message : error;
    ElMessage.error(msg);
    if (axios.isCancel(error)) {
      console.error('请求被取消', error);
    }
    return Promise.reject('错误信息' + msg);
  }
);

export default service;
本站所有文章、图片、资源等如无特殊说明或标注,均为来自互联网或者站长原创,版权归原作者所有;仅作为个人学习、研究以及欣赏!如若本站内容侵犯了原著者的合法权益,可联系我们进行处理,邮箱:343049466@qq.com
赞(0) 打赏

上一篇:

下一篇:

相关推荐

0 条评论关于"Axios.js通过AbortController中止控制器方法取消请求"

表情

最新评论

    暂无留言哦~~
谢谢你请我吃鸡腿*^_^*

支付宝扫一扫打赏

微信扫一扫打赏