起个头吧
嘻嘻,想到好东西就是想跟大家一起分享(本文适合会使用vue和axios的码友),顺便装(找)逼(骂),没错,就是皮痒了;
就喜欢你看不惯我又干不掉我的样子

本文主要想分享以下几个点:
- axios的二次封装
- axios处理token过期并自动刷新token,不用跳转到登录页重新登录
- axios中断页面跳转后当前正在进行还没响应的请求
- api的自动化管理
axios的二次封装
我看很多人都在问,axios的封装都已经够好了,为什么还要进行二次封装?
举个现实生活中的栗子:
在付现金的时代,去饭店吃个小面后结账,服务员都是上来给你算多少钱(8块),然后你给服务员10块,服务员跑去前台给你找2元补给你;
然后老板觉得这样太浪费时间了,就让服务员身上装一些零钱,下次你再去的时候,就直接补给你了;
你考虑得越周到,你的工作效率就会越高;axios考虑的是满足所有用户,而你应该考虑的是满足你自己,怎样才能更高、更快、更狠的完成项目。
回归正题,先直接看代码吧~
let promiseArr = {};
const CancelToken = axios.CancelToken;
//状态
const statusMap = {
400: '请求参数有误',
401: '未授权,请重新登录',
403: '访问被拒绝',
404: '地址不存在,找不到对应资源',
405: '请求方法不允许',
500: '服务端错误',
501: '网络未实现',
502: '网络错误',
503: '服务不可用',
504: '网络超时',
505: 'http版本不支持该请求'
}
function request(options) {
let _url = options.url;
//防止重复请求
if(_url in promiseMap){
promiseMap[_url]();
}
let defaultOptions = {
tip:errorAutoTip,
cancelToken: new CancelToken(c => {
promiseMap[_url] = c;
}),
url:getGateWayName(options.gateway)+_url,
};
delete options.gateway;
delete options.url;
defaultOptions = Object.assign(defaultOptions,options);
let tip = defaultOptions.tip;
return new Promise((resolve, reject) => {
axios(defaultOptions).then(res => {
delete promiseMap[_url];
//请求成功,需要通过state来判断,不同的后台使用插件可能不同,注意此处需要更正,否则全是reject
if (res.data.state) {
resolve(res.data.data);
}
//请求失败,由于后台封装的问题,他们抛出的错误与http的状态不符,会出现在then里面
else {
reject(res.data);
handleCatchError(res, tip);
}
}).catch(err => {
delete promiseMap[_url];
reject(err.response);
handleCatchError(err.response, tip);
})
})
}
//捕获错误处理
function handleCatchError(obj, tip) {
if (!isObject(obj)) return;
let data = obj.data || {};
//根据后台返回数据提示
let code = data.code || obj.status;
let errorInfo = ['message:' + (data.message)];
code && errorInfo.push(['code:', code, '(', statusMap[code], ')'].join(''));
tip ? (showMessage.show({ messages: errorInfo })) : (console.log(errorInfo.join('\n')));
}
//网关服务
function getGateWayName(name) {
const gatewayMap = {
auth: '/phjr-manager-service'
}
return baseUrl + ((name in gatewayMap) ? gatewayMap[name] : '');
}
export const FINAL_SERVICE = {
// 序列化的方式,参数跟地址栏一起
get: async function (options) {
options.url = options.url + stringifyObj(options.data);
options.data = {};
return await request(options);
},
// 参数实体的方式
post: async function (options) {
// 如果不是表单格式的话,就不JSON序列化
if (options.headers!==null) {
options.data = JSON.stringify(options.data);
}
return await request(options);
}
}
get、post两种方法,实际意义上应该是两种方式,一种是参数在地址栏上,一种是参数为独立实体,这个根据后端的接口定义有关,所以get、post对于get/post/put/delete都适用,一般情况来说get/delete为一组,post/put为一组;
封装axios为request的目的有4个:
- 在开发环境请求接口错误自动提示(可配置),方便调试(showMessage为自定义调用组件的方法,可以使用其他ui插件替换,如iview的$Message,vant的Toast等)
- 处理网关服务,后台可能存在多个网关服务,需要配置并添加到url地址(这个功能看自己公司的需求,一般小项目是不需要的,到时候不配置就可以)
- 为每个接口配置中断请求项-->cancelToken(请求完成后删除),然后在请求拦截以及路由切换时取消没有响应的请求(下面有讲)
- 根据后台返回的状态来判断是否为成功(直接通过axios的.then.catch是有问题的)
axios无痛刷新
大概理一下思路,就是用户正常访问时,token正好过期了,后台返回一个状态(401),然后就去请求后台提供的一个可以刷新token的接口(refreshToken),请求成功之后后台返回最新的token信息,重新存入到storage或者cookie中(此时用户还没有得到反馈,这一步是在axios拦截中做的),通过axios重新请求用户访问的接口最后响应给用户;

废话不多说,直接上代码~
// axios 响应拦截器
axios.interceptors.response.use(
response => {
if (response.data && response.data.code === 401) {
// handleError();
return handle401(response);
}
// 对响应数据做点什么
return Promise.resolve(response);
},
error => {
if (error.response && error.response.status === 401) {
// handleError();
return handle401(error);
}
// 对响应错误做点什么
return Promise.reject(error);
}
)
function handle401(error) {
// 如果是刷新token的接口报错401就直接退出
if (error.config.url.includes('refreshToken')) {
handleError();
return Promise.reject(error);
}
//如果已经在登录页面了,就不用继续往下了
if (router.currentRoute.name !== 'login') {
//那么去请求新 token并返回重新请求的接口
return doRequest(error);
}
}
//accessToken过期,通过refreshToken 重置accessToken和refreshToken
async function handleRefreshToken() {
return await axios.get(getGateWayName('auth') + '/refreshToken');
}
//捕获异步后返回数据{err,data}
async function asyncCapture(asyncFn,params){
let err='',data={};
try{
data = await asyncFn(params);
}catch(e){
err = e
}
return {err,data};
}
// 处理刷新token后重新获取接口数据,并返回到页面
async function doRequest(error) {
const { err, data } = await asyncCapture(handleRefreshToken);//请求刷新token的接口
if (err) {
handleError();
return false;
}
let { accessToken, refreshToken, expire } = data;
setToken(accessToken, expire);
setRefToken(refreshToken, expire);
let config = error.response.config;
config.headers.token = accessToken;
//将请求失败的config重新请求一次
return await axios.request(config);
}
//过滤失败(token未通过验证)直接退出
function handleError() {
showMessage.show('登录信息已失效,请重新登录');
store.dispatch('logout', false);
}
注释都写的很明显,就不废话了,要注意以下几点:
- 重新请求refreshToken有可能会失败,要捕获到这个异常机制,否则就容易陷入死循环,要根据实际项目的接口做判断
- 正常响应response和异常响应error都要做判断,因为后台返回的状态和http状态会不一致
中断axios请求
上文提到中断请求应该有2种情况,一是用户请求同一个接口,由于响应速度慢,上一个接口还没响应,同一个接口又发起了请求;二是同样是接口响应慢导致,用户在当前页面请求的接口都没响应,结果就跳转了页面,当前页面的接口应该全部中断。
实现方式就可以针对这2种情况来开展:
- 通过路由拦截,遍历当前的所有请求并中断
//router 路由拦截
router.beforeEach((from,to,next)=>{
let item;
for(item in promiseMap){
promiseMap[item]();
}
promiseMap = {};
next();
})
- 请求中判断上次请求是否存在,存在则中断
function request(options) {
let _url = options.url;
//防止重复请求
if(_url in promiseMap){
promiseMap[_url]();
}
......
}
api的自动化管理
接着上个故事继续讲
随着科技的进步,以前饭店还需要收银员去收钱,然后补钱,但是两个马爸爸就觉得,不行呀,就算你准备了零钱也要多个步骤啊,我还要腾出手来收钱补钱,那我给你一个二维码吧,你自己付,我就只管给你们做吃的就可以了。
自动化管理就是如此,我看网上的方式其实都差不多,都是封装axios,然后根据需求定义api文件,再挂载到vue实例或者window全局上,方式都差不多,不过我还是喜欢按照自己的思路去封装,怎么简单怎么来,怎么好维护怎么来,怎么能偷懒怎么来;

1. index.js
/**
* 使用require.context自动导入api结尾的文件,并全部注入到APIS一个对象中,方便全局导入
*/
const ctx = require.context('.',false,/.api.js$/);
const APIS = {};
ctx.keys().forEach((item)=>{
let name = item.split('.')[1].slice(1);
let config = ctx(item);
let dft = config.default || config;
APIS[name] = dft;
})
export default APIS;
/*
user.api.js order.api.js
{
user:{...很多方法},
order:{...很多方法}
}
*/
自动读取api文件夹下的.api.js后缀的文件,并自动归类名称,例如我新建一个cart.api.js,然后就会新生成一个属性cart,并且不会出现命名冲突的问题,如果需要注册到vue或window,直接引入即可:
//main.js
import api from './api/index.js';//引入所有的对象
/*
{
user:{
get:f()
...
},
order:{...},
cart:{
get:f(),
...
}
}
*/
vue.prototype.$api = api;//vue
// or window.$api = api;//window
2. urlHandler.js
// 引入全局请求方法
import { FINAL_SERVICE } from './request';
//异步处理函数
async asyncCapture(asyncFn,params){
let err='',data={};
try{
data = await asyncFn(params);
}catch(e){
err = e
}
return {err,data};
}
/**
* 获取数据类型
*/
const getType = (function () {
return function (val) {
let str = Object.prototype.toString.call(val);
return str.slice(8, str.length - 1).toLowerCase();
}
})();
export function urlHandler(urls){
const finalExport = {};
urls.forEach(item => {
let name = item.name
if (!name || (name in finalExport)) {
throw new Error(name+' should be required and unique,but be multiple or undefined!')
}
finalExport[item.name] = function (params) {
//如果url是个方法的话,单独处理
if(getType(item.url)==='function'){
item.url = item.url(params);
}else{
item.data = params;
}
//返回异步数据{err,data}
return asyncCapture(FINAL_SERVICE[item.way],item);
}
})
// console.log(finalExport)
return finalExport;
}
3. user.api.js
// 引入全局请求方法
import {urlHandler} from './urlHandler';
const urls = [
{
name: 'login', //函数名称
url: '/auth/login', //请求地址
method:'post', //请求方法
gateway: 'auth', //请求网关服务
way: 'post', //请求处理方式,post为实体方式,get为序列化方式,
tip:false,//请求失败是否自动提示,默认是可配置(config.js)
},
{
name: 'getMe',
url: '/out/me/get',
method: 'get',
gateway: 'auth',
way: 'get',
},
{
name: 'updateMe',
url: '/out/me/update',
method: 'put',
gateway: 'auth',
way: 'post'
},
{
name: 'deleteMe',
url:({id})=>{
return '/out/me/delete/'+id
},
method: 'delete',
gateway: 'auth',
way: 'get'
},
]
export default urlHandler(urls);
/*
{
login:f(),
getMe:f(),
updateMe:f(),
deleteMe:f()
}
*/
这种方式,好处真的很多
-
可以添加自定义字段进行单独处理(只要不和axios的字段冲突)
-
配置与axios冲突的字段,对axios的进行覆盖,例如method,url,headers,timeout,data等等
-
可以自定义字段的类型,String,Function等,只需要在公用方法上进行处理即可,可维护性更高
上面对url进行了自动处理,可以给url配置string和function,在接口访问时,除了序列化方式(interface?a=1&b=2),还有直接在后面加参数的情况(/interface/:id/:name),这种就需要进行单独处理
-
重复代码很少,就相当于定义一个数组对象
使用试试?
<template>
<div class="width-limit">
<h1>请求测试</h1>
<me-button class="margin-r-base" @click="request('get')">get请求</me-button>
<me-button class="margin-r-base" @click="request('post')">post请求</me-button>
<me-button class="margin-r-base" @click="request('put')">put请求</me-button>
<me-button @click="request('delete')">delete请求</me-button>
</div>
</template>
<script>
import userApi from '../api/user.api';//按需引入,前提是没有全局引入(为了测试,同时也注册到vue上)
export default {
methods: {
request(method) {
switch (method) {
case "get": {
const {err,data} = await userApi.getOrder({ page:1,rows:10 });
if(!err){
console.log(data)
}
break;
}
case "post": {
userApi.login({account:'test',password: "123456",kaptcha:'de56' })
.then(({err,data})=>{
if(err) throw err;
console.log(data);
});
break;
}
case "put": {
this.$api.user.updateMe({ id:1,count:2 });
break;
}
case "delete": {
this.$api.user.deleteMe({ id:1 });
}
}
}
}
};
</script>
处理请求结果有2种处理方式:
- 配合async await:
aysnc ...
const {err,data} = await userApi.getMe({ page:1,rows:10 });
if(!err){
console.log(data)
}
- promise.then链式模式:
userApi.login({account:'test',password: "123456",kaptcha:'de56' })
.then(({err,data})=>{
//err和data封装在同一个对象返回,这样在处理相同情况时就省去一个步骤
/*
例如:点击触发请求后使loading=true,当返回结果时,无论是成功还是失败,都应该将loading=false
*/
if(err) throw err;
console.log(data);
});
看看效果
页面:





这里可以发现,请求响应明明是返回的成功200,而后台却返回的错误500,所以当我对axios进行二次封装后,我已经捕获到这是一个错误的请求,并抛出;
delete请求:

结个尾吧
本文所叙述的都是实际工作中遇到的问题,然后通过各种途径想办法进行解决,不管是请教同事、看博客、看书还是看视频,最后还是的总结成自己的一套思路,满足公司的实际需求。长路漫漫,其修远兮!技术的沉淀是来源于实际运用过程中对遇到问题的思考和总结。
最后给大家奉上我的源码吧~github地址,如果有什么不好的地方,请大家多多指正,大家共同进步。
