• 1

  • 444

封装axios支持无感刷新token(含打包)

6天前

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.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'
···
复制代码

参考连接

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

ios

444

相关文章推荐

未登录头像

暂无评论