一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情。
响应拦截器的功能
axios中提供了响应拦截器功能:所有从后端回来的响应都会先进入响应拦截器,包括出错的请求中。所以,我们可以在响应拦截器中去写代码来统一解决。
添加响应拦截器
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
return response
}, async function (error) {
// 如果发生了错误,判断是否是401
console.dir(error)
// 开始处理
return Promise.reject(error)
})
一般在开发后台管理项目的时候,如果报401我们可以直接在响应拦截器里拦截,然后强行跳转到登录页面,让员工重新登录
但是在开发前台项目,也就是面向用户的项目就不能这样做,因为会让用户的体验感很差,后台项目的话面向公司内部的员工,可以用这种直接跳转到登录页的方法,面向用户的项目我们一般是用双Token的方法,让用户对因token过期产生的401错误无感知
在request的响应拦截器中:
- 对于某次请求A,如果是401错误 (2)
-
有refresh_token,用refresh_token去请求回新的token (3)
-
新token请求成功 (4)
- 更新本地token (5)
- 再发一次请求A (6)
-
新token请求失败
- 携带请求地址,跳转到登陆页
-
-
没有refresh_token,说明没有登陆
- 携带请求地址,跳转到登陆页 在src\utils\request.ts中,补充响应拦截器。 由于这里涉及到非组件内路由跳转,所以需要提前封装独立的history
-
1、创建文件 src\utils\history.ts
import { createBrowserHistory } from 'history' //导入history然后暴露出去
const history = createBrowserHistory()
export default history
2、在app.tsx中使用,
import { Router, Route, Redirect } from 'react-router-dom' //react-router-dom里自带有Router
import history from '@/utils/history'
function App () {
return (
<div className="app">
<Router history={history}>
组件
</Router>
</div>
3、在对应的ts文件中引入history,然后直接history.push()等进行页面跳转
在request.ts中,添加响应拦截器(核心代码)
// 封装axios
import { Toast } from 'antd-mobile'
import axios, { AxiosError } from 'axios' //导入axios及对应的类型AxiosError
import { getToken, setToken } from './storage' //导入获取token的方法
import history from '@/utils/history' //到日history用来进行页面跳转
import store from '@/store' //导入store
const baseURL = '######' //这里把跟地址放到外面,因为我们等下发请求用的是axios,
如果用instance实例,会在请求拦截器上自动加上token,当token已经过期了我们正要去获取新的token时,
是不需要在请求头上加token的,所以要把根路径放到全局,用axios发请求
const instance = axios.create({ //这里创建axios实例
baseURL,
timeout: 5000
})
// 添加请求拦截器
instance.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
const token = getToken().token
if (token) {
config.headers!.Authorization = 'Bearer ' + token
}
return config
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error)
}
)
// 添加响应拦截器
instance.interceptors.response.use(
function (response) {
// 对响应数据做点什么
return response
},
async function (error) {
// 对响应错误做点什么
const err = error as AxiosError
// 没有网络
if (!err.response) {
Toast.show({ content: '网络错误', icon: 'fail' })
return Promise.reject(error)
}
// 401错误
if (err.response.status === 401) {
const { refresh_token } = getToken()
if (!refresh_token) {
history.push('/login', { from: history.location.pathname })
return Promise.reject(error)
}
try {
// 获取新的token
const res = await axios.put(baseURL + 'authorizations', null, {
headers: {
Authorization: 'Bearer ' + refresh_token
}
})
const newToken = { token: res.data.data.token, refresh_token }
// 保存新tokn
setToken(newToken)
// redux中保存新token
store.dispatch({ type: 'login/token', payload: newToken })
// 重新发请求
return instance(err.config)
} catch (error) {
// catch获取新token失败,也说明refresh_token过期了
Toast.show({ content: 'refresh_token过期了', icon: 'fail' })
history.push('/login', { from: history.location.pathname })
return Promise.reject(error)
}
}
return Promise.reject(error)
}
)
export default instance
因为在响应拦截器里的错误是分很多种的比如400、403、404、500等, 一般要对具体的错误进行处理,我们这里针对的是401错误,所以主要对401错误进行分析
我们先用断言指定error的类型,并定义变量接受,这样后面写会有提示语法,TypeScript也不会报错
const err = error as AxiosError
首先是没有网络的情况
用户是一定会发生使用的时候突然断网这个情况的,所以我们先要处理这个,因为后面的都是基于err.response存在的前提下进行的,如果网络断了就发不出去请求,也就没有err.response
//响应拦截器里每一每一种情况都要return,不然因为都是唯一的,后面的逻辑还需要return出去的内容
if (!err.response) {
Toast.show({ content: '网络错误', icon: 'fail' })
return Promise.reject(error) //如果没有网络,直接return Promise.reject(error),也就是抛出去一个错误
}
判断401错误的情况
if (err.response.status === 401) {
const { refresh_token } = getToken() //从# localStorage取出refresh_token
if (!refresh_token) {
history.push('/login', { from: history.location.pathname })
return Promise.reject(error)
}
这里做判断,当报的状态码是401时,说明是token已经过期了,此时我们取出我们的依据refresh_token,它的有效时长一般是14天**,利用它来作为我们获取token的依据,从localStorage取出refresh_token,如果是false,说明refresh_token也过期了,此时要跳转到登录页面重新登录,并记录下地址,用于登陆成功后回跳,然后return一个错误出去,保证代码不往下走
当确定refresh_token没有过期时,我们就要通过refresh_token获取新的token
// 获取新的token
const res = await axios.put(baseURL + 'authorizations', null, {
headers: {
Authorization: 'Bearer ' + refresh_token
}
})
const newToken = { token: res.data.data.token, refresh_token }
// 保存新tokn
setToken(newToken)
// redux中保存新token
store.dispatch({ type: 'login/token', payload: newToken })
// 重新发请求
return instance(err.config)
确定refresh_token没有过期后,发送请求获取新的token,put()里面传了三个参数,第一个是请求地址,第二个null代表没有参数,第三个对象是在请求头里加上Authorization: 'Bearer ' + refresh_token,这个是要根据后端的接口文档来决定的!然后把获取来的新token和refresh_token放在一个对象里,传到本地localStorage和redux中,更新token!
return instance(err.config) 这句代码是从新发送当时因为token过期而请求失败的请求,这样极大地增强用户的体验,instance(err.config)是axios的固定语法,这里return的就不是错误,而是从新发送请求
还有一种情况就是,token过期了refresh_token没有过期,但是在通过refresh_token获取新token的过程中出现错误,因为我们发请求时用了async和await,所以这里我们需要通过try catch处理这种情况
try {
// 获取新的token
const res = await axios.put(baseURL + 'authorizations', null, {
headers: {
Authorization: 'Bearer ' + refresh_token
}
})
const newToken = { token: res.data.data.token, refresh_token }
// 保存新tokn
setToken(newToken)
// redux中保存新token
store.dispatch({ type: 'login/token', payload: newToken })
// 重新发请求
return instance(err.config)
} catch (error) {
// catch获取新token失败,也说明refresh_token过期了
Toast.show({ content: 'refresh_token过期了', icon: 'fail' })
history.push('/login', { from: history.location.pathname })
return Promise.reject(error)
}
}
return Promise.reject(error)
}
当在通过refresh_token获取新token的过程中出现错误,使用catch捕获到错误,然后跳转到登录页面,并且给用户提示,然后抛出一个错误