github地址:github.com/Lstmxx/axio…
前言
前段时间在公司搞优化,看到很多之前自己写的移动端项目和桌面项目都用了无感刷新token,所以干脆把这个封装成一个npm包来使用吧。
无感刷新token
无感刷新token原理其实挺简单的,其实就是当接口返回401的时候再向后端申请一个全新的token就好了。
实现思路
通过在axios的response拦截器的rejected中检查返回的错误代码中是否是401,如果是的话则先刷新一次token再发一次请求。
- 封装token保存,保存到sessionStorage中。
// ./src/util/tokenHelp.js
const TOKEN_KEY = 'token'
const storage = window.sessionStorage
export function setToken (token, type) {
storage.setItem(TOKEN_KEY, token)
storage.setItem('type', type)
}
export function getToken () {
const token = storage.getItem(TOKEN_KEY)
if (token) return token
else return false
}
export function getType () {
const type = storage.getItem('type')
return type
}
- 封装axios
// /src/core/axios.js
import axios from 'axios'
import { merge } from '../util/util'
const baseConfig = {
baseUrl: '',
timeout: 6000,
responseType: 'application/json',
withCredentials: true,
headers: {
get: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
},
post: {
'Content-Type': 'application/json;charset=utf-8'
}
},
transformRequest: [function (data) {
data = JSON.stringify(data)
return data
}],
transformResponse: [function (data) {
// 对 data 进行任意转换处理
if (typeof data === 'string' && data.startsWith('{')) {
data = JSON.parse(data)
}
return data
}]
}
export function initAxios (config) {
const _config = merge(baseConfig, config)
const _axios = axios.create(_config)
return _axios
}
merge函数是参考了axios的merge来编写,主要是来合并config的。当然啦,Object.assign也不是不行,但是毕竟目的是要封装成npm包的,兼容性还是得看一下的。
- 封装抛出service对象。这里注意我在response的error接受函数加了async,配合await就可以编写同步代码了,就不用都嵌在then、catch里面了。
// ./src/index.js
import { setToken, getToken } from './util/tokenHelp'
import { isFunction } from './util/util'
import { initAxios } from './core/axios'
const Service = function Service (options) {
this.isReflash = false
this.reTryReqeustList = []
const config = options.config ? options.config : {}
const responseInterceptors = options.responseInterceptors
const requestInterceptors = options.requestInterceptors
const reflashTokenConfig = options.refleshTokenConfig || null
const getTokenFn = options.getTokenFn
const axios = initAxios(config)
function _refleshToken () {
return new Promise((resolve, reject) => {
axios.request(reflashTokenConfig).then((response) => {
const isResolve = getTokenFn(response, setToken)
if (isResolve) resolve()
else reject(new Error('get token error'))
}).catch((err) => {
reject(err)
})
})
}
axios.interceptors.request.use((_config) => {
const c = isFunction(requestInterceptors) ? requestInterceptors(_config, getToken) : _config
return c
}, (error) => {
error.data = {}
error.data.msg = '服务器异常,请联系管理员!'
return Promise.resolve(error)
})
axios.interceptors.response.use((response) => {
if (isFunction(responseInterceptors)) {
return responseInterceptors(response)
}
return response
}, async (error) => {
if (error.response && error.response.status === 401) {
try {
refleshTokenConfig && await _refleshToken()
return axios.request(error.response.config)
} catch (err) {
return Promise.reject(err)
}
}
return Promise.reject(error)
})
this.axios = axios
}
Service.prototype.request = function request (options) {
return this.axios.request(options)
}
export default Service
封装完毕后,引入到业务层调用测试一下看看先~
import Service from './axios-reflash-token/src/index.js'
const showStatus = (status) => {
let message = ''
// 这一坨代码可以使用策略模式进行优化
switch (status) {
case 400:
message = '请求错误(400)'
break
case 401:
message = '未授权,请重新登录(401)'
break
case 403:
message = '拒绝访问(403)'
break
case 404:
message = '请求出错(404)'
break
case 408:
message = '请求超时(408)'
break
case 500:
message = '服务器错误(500)'
break
case 501:
message = '服务未实现(501)'
break
case 502:
message = '网络错误(502)'
break
case 503:
message = '服务不可用(503)'
break
case 504:
message = '网络超时(504)'
break
case 505:
message = 'HTTP版本不受支持(505)'
break
case 1001:
message = '验证失败'
break
default:
message = `连接出错(${status})!`
}
return `${message},请检查网络或联系管理员!`
}
const options = {
config: {
baseURL: 'http://test.platform.xbei.pro/api',
timeout: 6000
},
requestInterceptors: function (config, getToken) {
config.headers.Authorization = 'Bearer ' + getToken()
return config
},
responseInterceptors: function (response) {
const data = response.data
const res = {
status: data.code || response.status,
data,
msg: ''
}
res.msg = data.message || showStatus(res.status)
return res
},
refreshTokenConfig: {
url: '/platform/login',
method: 'POST',
data: {}
},
getTokenFn: function (response, setToken) {
if (response.data.code === 200) {
setToken(response.data.data.token, response.data.data.tokenType)
return true
}
return false
}
}
const service = new Service(options)
...
结果也如预期一样,大功告成啦。。。等等先看看同时多个请求时会怎么样
啊这不行啊,调了好几次token获取。这是因为多请求的时候,各个请求都是异步的,token还没设置自然会调用好几次reflashToken来刷新token。
解决多次刷新token问题
其实解决也很简单,因为axios.interceptors.response里无论是fulfilled亦或者是rejected都是返回Promise(axios的request里面的链式调用),而Promise在进入pending后会一直等待resolve或reject才会到达fulfilled或者rejected,所以只要我们返回promise的时候把对应的resolve用数组给存起来,那么等到token刷新完之后再把数组里面的请求一一执行就完事了。
- 改进一下
const Service = function Service (options) {
this.isReflesh = false
this.reTryReqeustList = []
...
axios.interceptors.response.use((response) => {
if (isFunction(responseInterceptors)) {
return responseInterceptors(response)
}
return response
}, async (error) => {
if (error.response && error.response.status === 401) {
if (!this.isReflash) {
this.isReflash = true
try {
reflashTokenConfig && await _reflashToken()
this.isReflash = false
while (this.reTryReqeustList.length > 0) {
const cb = this.reTryReqeustList.shift()
cb()
}
return axios.request(error.response.config)
} catch (err) {
return Promise.reject(err)
}
} else {
return new Promise((resolve) => {
this.reTryReqeustList.push(
() => resolve(axios.request(error.response.config))
)
})
}
}
return Promise.reject(error)
})
...
}
再来测试一下
OK!没有问题,那么接下来就是打包成npm包了。
打包
使用rollup来打包,先初始化包
mkdir axios-refresh-token
cd axios-refresh-token
npm init
修改package.json
"main": "dist/axios-refresh-token.cjs.js",
"module": "dist/axios-refresh-token.esm.js",
"browser": "dist/axios-refresh-token.umd.js",
...
"dependencies": {
"axios": "^0.21.0"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/preset-env": "^7.12.1",
"@rollup/plugin-babel": "^5.2.1",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^10.0.0",
"@types/babel__core": "^7.1.9",
"rollup": "^2.0.0",
"rollup-plugin-terser": "^7.0.2"
},
"files": [
"dist",
"src"
]
...
修改完毕后,npm install安装依赖。这次打包用到了5个插件。
- @rollup/plugin-json 将json文件转换为es6模块
- @rollup/plugin-node-resolve 解析node_modules文件
- @rollup/plugin-commonjs 将CommonJS模块转换为es6模块给rollup来处理
- @rollup/plugin-babel 集成babel
- rollup-plugin-terser 压缩代码
在项目根目录下新建一个rollup.cofnig.js文件。
import { nodeResolve } from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json';
import babel from '@rollup/plugin-babel';
import { terser } from 'rollup-plugin-terser'
// const isDev = process.env.NODE_ENV !== 'production'
export default {
input: 'src/index.js',
output: [
{
file: 'dist/axios-reflash-token.cjs.js',
format: 'cjs',
name: 'cjs'
},
{
file: 'dist/axios-reflash-token.es.js',
format: 'es',
name: 'es'
},
{
file: 'dist/axios-reflash-token.umd.js',
format: 'umd',
name: 'umd'
}
],
plugins: [
json(),
nodeResolve(),
commonjs(),
babel({
babelHelpers: 'runtime',
exclude: 'node_modules/**',
plugins: [
'@babel/plugin-transform-runtime'
]
}),
terser()
]
}
打包~
rollup -c ./rollup.config.js
如果要打包到npm上,还需要使用npm adduser命令来添加用户。添加完之后就可以上传了。
npm publish --access public // 如果打包的前缀带@xxx/的话得带上 --access public
打包完就可以安装引入啦
npm install @lstmxx/axios-refresh-token --save
import Service from '@lstmxx/axios-refresh-token'
···