让axios在vue(2.x)中丝滑起来(基本封装/请求取消,重试, 挂起等)

Axios

  1. 易用、简洁且高效的http库
  2. 支持node端和浏览器端
  3. 支持 Promise

– 因为收到大量粉丝的私信叫我出个文章说说axios的日常使用,所以写了这篇文章。
– 😨😨😨😨???大量粉丝?!!!
– 咳咳…我…想…写…怎 !么 !了!!!
— 🙄🙄🙄🙄

想深入源码了解axios实现的,请参考我另一篇文章: Axios源码初探


🎈官网地址
🎈axios看云(常规使用参考)

axios 是我工作中用的比较多的一个库,基本离不开。这里把常用的一些东西整理出来以便日后复制粘贴。提升开发效率[手动狗头]🤣

小提示: win10 win + .,或者编辑状态 右键 → 表情符号,可以打表情哦

  • 如果的你项目很迷你,Api 10+,项目不复杂,建议参考 axios简单封装
  • 如果你想尝试类(class)的方式封装(本文是函数),推荐 封装axios

目录
http
api.js 所有请求的封装
request.js axios 实例,拦截器配置以及错误处理
originAxios.js 不需要实例那些配置的公共请求如 退出,token刷新

推荐大家一个 vscode 主题 Tiny Light,对眼睛 针不戳 🤪


后端返回的数据结构:

{flag: 0, code: 123, data: {..}}

🏈基本封装

⚾常规封装

[💪支持]

  • axios与业务低耦合
  • token 统一添加
  • 错误全局拦截

需要安装 axios, element-ui(错误提示)

cnpm install axios element-ui -S

http/request.js

// 请求API
import axios from 'axios';
import { Message } from 'element-ui';
import store from "../store/index";

// 定义URL与环境判断
const reqUrl = ['https://xxx.xxx.com/api', 'http://xxx:5080'];
const isProd = ['production', 'prod'].includes(process.env.NODE_ENV); 

//表示跨域请求时是否需要使用凭证(cookie),设置后cookie会出现在请求头
//axios.defaults.withCredentials = true;
const instance = axios.create({
  baseURL: isProd ? `${reqUrl[0]}` : `${reqUrl[1]}`,
  timeout: 100 * 1000, // 请求时间超过100s就报超时错误(来自axios)
  headers: { 'Content-Type': 'application/json'},
  // withCredentials: true, //防止上面设置不上
})

//错误处理函数,
function errorHandle({data, status}){
	// 与后台约定403为登录过期
	if(data.code == 403) logout() // 走退出接口,清除一些登录信息 
	else Message.error(`${data.msg}---code: ${data.code} status: ${status}`)
}

// 请求拦截器
instance.interceptors.request.use(config => {
  const { access_token } = store.getters.userInfo;
  if (!/login/g.test(config.url) && access_token) { // 非登录请求增加token
    config.headers.Authorization = 'JWT ' + access_token;
  }
  return config;
}, error => { return Promise.reject(error) });

// 响应拦截器
instance.interceptors.response.use(res => {
  const { data } = res
  // flag为请求成功标志 0为失败
  if (res.status === 200 && data.flag == 0) errorHandle(res)
  return data;
},err => { //这里的error.response 和 res 格式一样,只是 status 不是200
	err.response && errorHandle(err.response)
    return Promise.reject(error.response)
})

export default instance;

http/api.js

// API接口汇总
import instance from './request.js';
export let user = {
  userGet(params = {}){ // params为查询参数 page, name 等等
	return instance.get(`/user/`, { params })
 },
  userAdd(data){
    return instance.post('/user/',data)
  },
}
export const login = {
	logout(){
		return instance.delete('/logout/')
	}
}

http/originAxios

//退出登录
/**
 * @params force: number  是否强制退出 1强制 0非强制
*/
import router from '../router/index.js';
import { Message } from 'element-ui';
import { reqUrl,isProd } from "./request.js";
import { login } from "./api.js";

// 退出单独拎出来处理是为了错误单独处理; 请求失败时token不需要刷新
export function logout(force = 0) {
    if(force == 0){
        login.logout.then(resp => {
            const res = resp.data
            if (res.flag == 1) toLogin() 
            else Message.error(res.msg + ' 退出失败!---' + res.code)
        }).catch(e => {
            const res = e.response || null
            if(res) Message.error(res.msg + '退出失败! ---' + res.code)
        })
    }else toLogin();
    
    function toLogin(){
        store.commit("user/CLEAR_INFO"); // 清除登录信息
        sessionStorage.clear(); // 清除vuex,我选择的是sessionStorage存储
        router.replace({name: 'login'})
    }
}

axios 全局挂载
main.js

import * as api "./http/api.js";
Vue.prototype.$api= api

使用
xxx.vue

methods: {
  getUser(){
	this.$api.user.userGet({ page: 1, name: '李' }).then(res => {
		if(res.flag == 1) // ....
		else  //...
	})
  },
  addUser(){
	this.$api.user.userAdd({ name: '李红', age: 23, sex: '女' }).then(res => {
		if(res.flag == 1) // ....
		else  //...
	})
  },
}

整体看下来是不是也不简单了,但是更利于项目的扩展

=======================================

⚾扩展

考虑到登录的错误提示需要在页面里显示,而不是全局弹窗;且有些请求是表单请求,上面封装无法达到。于是扩展一下
[💪支持]

  • 错误全局拦截可定制
  • 请求格式可定制

http/request.js

// 请求拦截器
instance.interceptors.request.use(config => {
  + if(config.headers.isForm) {
  +    config.headers['Content-Type'] = 'multipart/form-data';
  + }
  return config;
},err => {/*...*/ });

// 响应拦截器
instance.interceptors.response.use(res => {
  const { data } = res
  + const globalErr = res.headers.globalErr || true
  // 增加一个判断条件
  + if (res.status === 200 && data.flag == 0 && globalErr) errorHandle(res)
  return data;
},err => {
  	+ const globalErr = res.headers.globalErr || true
	+ globalErr && errorHandle(err.response)
    return Promise.reject(error.response)
})

http/api.js

import instance from './request.js';
export let user = {
  //...
  userAdd(data){ return instance.post('/user/', data, { headers: { isForm: true } }) },
}
export const login = {
	login(){ return instance.post('/login/', { headers: { globalErr: false } }) }
}

使用同上。其实利用的就是axios headers自定义属性

补充: axios自定义属性也可以,但有的版本不支持,但是后续版本应该修复了,我以前在这踩过坑。


🏈请求取消,重试

⚾请求取消

场景: 多次重复请求时,只执行最新的请求
推荐 axios源码分析-取消请求
借鉴的是原始XHR

let xhr
if (window.XMLHttpRequest) {
  xhr = new XMLHttpRequest();
} else {
  xhr = new ActiveXObject('Microsoft.XMLHTTP')
}
xhr= new XMLHttpRequest()
xhr.open('GET', 'https://api')
xhr.send()
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.status === 200) { /* success */ } 
  else { /* error */ }
}
// 取消ajax请求 readyState = 0
xhr.abort()

单个 axios 取消有两种方法官网支持两种方法

  • 使用 CancelToken.source 工厂方法创建 cancel token
var CancelToken = axios.CancelToken;
var source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // 处理错误
  }
});

// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
  • 通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token
var CancelToken = axios.CancelToken;
var cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    cancel = c;
  })
});

// 取消请求
cancel();

稍微封装一下(页面所有重复请求都取消)
http.js

//限于篇幅,省略一些导入和axios实例化代码,
//...

const awaitRequets = [];// 保存重复请求队列

// 请求拦截器
instance.interceptors.request.use(config => {
    // 区别请求的唯一标识,用方法名+请求路径
    const requestMark = `${config.method}-${config.url}`;
    const markIndex = awaitRequets.findIndex(item => {
      return item.name === requestMark;
    });
    if (markIndex > -1) {
      // 取消上个重复的请求
      console.log('取消', awaitRequets, markIndex);
      awaitRequets[markIndex].cancel('还就那个无情取消~~~~');
      // 删掉在awaitRequets中的请求标识
      awaitRequets.splice(markIndex, 1);
    }
    const CancelToken = axios.CancelToken;
    const source = CancelToken.source();

    config.cancelToken = source.token;
    // 设置自定义配置requestMark项,主要用于响应拦截中
    config.requestMark = requestMark;
    // 记录本次请求的标识
    awaitRequets.push({ name: requestMark, cancel: source.cancel });
    return config;
  }, error =>  { return Promise.reject(error) });

// 响应拦截器
instance.interceptors.response.use(async res => {
    console.log('s 响应拦截器: ', res);
    const { data } = res
    // 根据请求拦截里设置的requestMark配置来寻找对应awaitRequets里对应的请求标识,
    // 如果状态码为非2xx,在错误拦截函数里也需要做响应的处理
    const markIndex = awaitRequets.findIndex(item => {
      return item.name === res.config.requestMark;
    });
    // 删除以更新 awatRequests
    markIndex > -1 && awaitRequets.splice(markIndex, 1);

    return data;
  }, async error => {
	    const config = error.message? error.message : error.config;
	    const response = error.response;
	    console.log('e响应拦截器: ', error, config, response);
	    
	    if(response) {// 如果被取消了,response 不存在的,下面会报错
	      const markIndex = awaitRequets.findIndex(item => {
	        return item.name === response.config.requestMark;
	      });
	      // 删除以更新 awatRequests
	      markIndex > -1 && awaitRequets.splice(markIndex, 1);
	      // 如果是主动取消了请求,做个标识
	      if (axios.isCancel(error)) {
	        console.log('主动取消!'); // 利用这个
	      }
	    }
	    // 下面error.responce也就是axios.then().catch(data => xxx)里的data,
	    return Promise.reject(error.response) 
  })

因为这个接口反应太快了,这里我为了实现效果,搞成了slow 3g网络🤣🤣
在这里插入图片描述


— 有大量粉丝反应没有好的录屏工具,让我推荐一个
— 😨😨😨??你又来了?
— 咳咳…,你懂什么说不定观众老爷们看了我这篇文章就关注❤我了。

哈哈,就是想给大家安利一款录屏工具 LICECap,


上面我的思路是参考这篇文章的,大家可以去看看哟。
【掘金】axios切换路由取消指定请求与取消重复请求并存方案

⚾token过期重试

场景: 在token过期的时候,可能需要刷新token,再重新请求
与后端约定 code === 401 就是token过期。然后需要将过期的请求重新发出。

方案优劣
① 在响应拦截器里将所有过期请求保存起来刷新token后再重新请求:由服务器判断请求是否过期,无需自己判断;: 需要多请求一次
② 在请求拦截器里将过期请求挂起,刷新token后再重新请求: 省流,过期的请求不会发出;:需要根据时间戳来判断token是否过期,存在不正确性

为确保准确性,我使用的是第一种,关于第二种后面会有介绍。
🔑关键点

  • 响应拦截器里 return instance(config) 就会重试请求,在错误处理函数里也是如此
  • 响应拦截器里 return new Promise(resolve => {}) 就会将该请求挂起,不会有返回值(不会触发使用时 .then 里的回调)。此时请求已经发出并得到响应。

为了确保当前过期请求可重新执行,改为微任务执行。更新于 2021年6月21日

http/request.js

+ import { refreshToken } from "./originAxios.js";

//token刷新标识与重试队列(函数)
+ let isRefreshing = false, requests = [];
// 响应拦截器
instance.interceptors.response.use( async res => {
  const { data } = res;
  if (res.status === 200 && res.data.flag == 0){
   // 以下为改动的地方,方便复制粘贴,这里就不加 + 了
   if(res.data.code === 401) {
      if(!isRefreshing){
        isRefreshing = true;
        const res1  = await refreshToken();
        if(res1.flag)  { // 将config的url与baseUrl重新配置就可以具体配置取消哪个请求
        	//为了确保当前过期请求可重新执行,改为微任务执行。 2021年6月21日14:40:26
        	Promise.resolve().then(() => {
				requests.forEach(cb => cb()); // 执行那些 刷新tokens时 被挂起的请求
          		requests = [];
			})
      	  // 为了保证401请求顺序执行,删除下面。2021年4月20日10:54:28
          // return instance(res.config)
        }
        else errorHandle(res1) // token刷新失败
        isRefreshing = false;
      }
      // 为了保证401请求顺序执行,略作修改。2021年4月20日10:54:28
      return new Promise(resolve => {
        requests.push(() => { resolve(instance(res.config)) })
      })
    }else errorHandle(res)
   } 
  return data;
},async error => {
    const config = error.config;
    const response = error.response;
    if(response.data.code === 401) { // 上面的刷新token代码 }
    else errorHandle(response);//其他错误进入错误处理
    return Promise.reject(error.response)
});

http/originAxios.js

// token刷新 单写是为了重刷token时不重复执行,且不进入拦截器的处理。
// 以上可以通过在拦截器匹配url来实现。但我这里涉及到vuex的数据更新,太多判断索性就拎出来了!
export function refreshToken(token = "") {
    const authUrl = isProd ? reqUrl[0] : reqUrl[1];
    const rToken = token || store.getters.userInfo.refresh_token;
    const aToken = store.getters.userInfo.access_token;
    return new Promise(resolve => {
        axios.post('/refresh-token/', null, {
            baseURL: authUrl,
            params: { token: rToken },
            headers: { Authorization: 'JWT ' + aToken }
        }).then(res => {
            const data = res.data;
            if (data.flag == 1) {
                store.commit("user/UPDATE_USER_INFO", data.data.data);
                resolve({ flag: 1, ...res })
            } else resolve({ flag: 0, ...res })
        }).catch(e => {
            const resp = e.response;
            resolve({ flag: 0, ...resp })
        })
    })
}
// 退出函数

以上代码就可以实现,接口返回401,后面的请求被全部收集起来,之后被收集的请求列表一个个重新请求。
使用方式不变。


🏈请求(失败后)重试n次

场景:假定一般status ≠ 2xx时就需要重新请求四次,一旦请求成功不再请求(即使请求次数<4)
前面介绍过只要在响应拦截器里 return instance(config) 该请求就会重试。所以只需稍作改变🛠.
http/request.js

const instance = axios.create({
 //全局配置,假设重试4次, 1.2s后重新请求
 headers: { _retry: 4, _retryDelay: 1200 }
})
// 响应拦截器
instance.interceptors.response.use( async res => {
  // 其他逻辑代码
},async error => { 
    const config = error.config;
    const response = error.response;
    errorHandle(response);
    
    if(!config.headers || !config.headers._retry) return Promise.reject(error);
    // 获取重试请求次数
    config.headers._retryCount = config.headers._retryCount || 0;
    if(config.headers._retryCount >= config.headers._retry) {
      return Promise.reject(error);
    }
    config.headers._retryCount += 1;
    const backOff = new Promise((resolve) => {
      setTimeout(() => { resolve() }, config.headers.retryDelay || 1500)
    })
    return backOff.then(() => {return instance(config)});
    return Promise.reject(error.response)
  })

使用不变


🏈请求挂起

tokenExprieTimetoken 是登录时拿到的, tokenExprieTime 是一个单位为秒的时间段。如 3600即代表一个小时,在登录的时候产生一个 expireTime:格林威治时间1970年01月01日00时00分00秒起至过期时间的总秒数

🔑关键点

  • 请求拦截器里 return new Promise(resolve => {}) 就会将该请求挂起,除非有返回值
  • 请求拦截器里在什么的 resolve => {} 函数中return config 就会重试请求(准确的说是发起请求,而非重试,因为请求并没有开始)。

http/request.js

import { logout, refreshToken } from "./originAxios.js";
import store from "../store/index";
instance.interceptors.request.use((config) => {
   // ...	
  const tokenExpireTime = store.getters.userInfo.expireTime
  const now = Date.now()
  // 当前时间大于过期时间,说明已经过期了,返回一个Promise,执行refreshToken后再return当前的config
  if (expireTime && now >= expireTime) {
     return new Promise(resolve => {
 		const res1  = await refreshToken();
 		if(res1.flag === 1){
 			store.commit('UPDATE_INFO', res1.data) //更新vuex token
			return config;
		} else logout(1); //更新失败后强制退出
     })
  }
  }
  return config
}, (error) => {
  // Do something with request error
  return Promise.reject(error)
})

setTimeout模拟异步请求

instance.interceptors.request.use(config => { 
   const p = function pro(){
        return new Promise(resolve => {
            setTimeout((p) => {
                console.log('我闪闪闪: ', p);
                resolve(config)
            }, 2000, 444)
        })
    }
    return p().then( config => {
        return config;
    })
}

在这里插入图片描述
具体参考:
【掘金】axios如何利用promise无痛刷新token
【掘金】axios如何利用promise无痛刷新token(二)


🏈多个URL

场景: 一个项目里,权限、基本数据、用户数据 都是不同的请求地址

[💪支持]:

  • n个请求地址

http/request.js

// 鉴权URL & 其他URL
const reqUrl = {
  authentication: ['https://xxx/api', 'http://xxx:5080']  
  , other: ['https://xxx/api/p/', 'http://xxx:5001/p/']
  //其它地址
}

// 拦截器配置
//....

// 动态生成实例
+ function newAxios(isAuth = 0) {
+  if (isAuth == 1) {
+    instance.defaults.baseURL = isProd ?
+      reqUrl.authentication[0] : reqUrl.authentication[1];
+  } else if(isAuth == 0 ) {
+    instance.defaults.baseURL = isProd ?
+      reqUrl.other[0] : reqUrl.other[1];
+  }
+  return instance
+}

export { newAxios }

http/api.js

// API接口汇总
import { newAxios }from './request.js';
export let user = {
  userGet(params = {}){ // params查询参数 page, name 等等
	return newAxios(1).get(`/user/`, { params })
 }
 //...
}
export const login = {
	logout(){ return newAxios().delete('/logout/') }
}

使用不变。

以上。

Axios源码初探

Vue 2中封装axios进行HTTP请求时,处理取消请求通常会涉及到`axios.CancelToken`。你可以创建一个`cancelTokenSource`,然后返回这个源的`token`,当需要取消请求时,通过调用`cancelTokenSource.cancel()`来停止请求。下面是一个简单的封装示例: ```javascript import axios from 'axios'; const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, // api 的基础地址 timeout: 5000, // 请求超时时间 cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c; // 将cancel函数绑定到组件实例上 }), }); service.interceptors.request.use( config => { if (!config.cancelToken) { config.cancelToken = this.cancelRequest; // 如果没有提供,则默认使用全局取消请求 } return config; }, error => Promise.reject(error) ); export default service; // 使用示例 this.$http.get('your-url').then(response => { // 请求成功... }).catch(error => { if (error.response && error.response.status === 401) { // 检查是否是因为取消请求导致的状态码 // 登录过期,重新登录... } else { console.error('Error:', error); } }).finally(() => { // 请求结束后的清理操作... }); // 要取消请求时,只需调用cancel方法 if (this.cancelRequest && !this.cancelled) { this.cancelRequest(); this.cancelled = true; } ``` 在这个例子中,当你需要取消请求时,可以在组件内调用`cancelRequest`。注意要保存一个标志`cancelled`来确认请求是否已经取消过,避免重复取消
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值