让axios在vue(2.x)中丝滑起来(基本封装/请求取消,重试, 挂起等)
axios在vue中的使用手册(封装,取消请求,请求重试,请求挂起),涉及业务1. token过期刷新;2. 多个请求url;3. 请求失败重试,挂起;4.重复请求取消,...
Axios
- 易用、简洁且高效的http库
- 支持node端和浏览器端
- 支持 Promise
– 因为收到大量粉丝的私信叫我出个文章说说axios的日常使用,所以写了这篇文章。
– 😨😨😨😨???大量粉丝?!!!
– 咳咳…我…想…写…怎 !么 !了!!!
— 🙄🙄🙄🙄
想深入源码了解axios实现的,请参考我另一篇文章: Axios源码初探
axios
是我工作中用的比较多的一个库,基本离不开。这里把常用的一些东西整理出来以便日后复制粘贴。提升开发效率[手动狗头]🤣
小提示: win10
win
+.
,或者编辑状态右键 → 表情符号
,可以打表情哦
目录
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)
})
使用不变
🏈请求挂起
tokenExprieTime
与 token
是登录时拿到的, 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/') }
}
使用不变。
以上。
更多推荐
所有评论(0)