当前位置:   article > 正文

axios封装终极版实现token无感刷新及全局loading_axios 无感刷新

axios 无感刷新

前言

关于axios全局loading的封装博主已经发过一次了,这次是在其基础上增加了token的无感刷新。

token无感刷新流程

  • 首次登录的时候会获取到两个token(AccessToken,RefreshToken)
  • 持久化保存起来(localStorage方案)
  • 正常请求业务接口的时候携带AccessToken
  • 当接口口返回401权限错误时,使用RefreshToken请求接口获取新的AccessToken
  • 替换原有旧的AccessToken,并保存
  • 继续未完成的请求,携带AccessToken
  • RefreshToken也过期了,跳转回登录页面,重新登录

后端设计

这里采用node简单实现的后台接口服务

  • 后端存有两个字段,分别保存长短token,并且每一段时间更新他们
  • 短token过期,返回 returncode:104;长token过期,返回 returncode: 108;请求成功返回returncode: 0;
  • 请求头中pass用来接收客户端长token,请求头中authorization用来接收客户端短token

1、创建一个新文件夹,通过vscode打开,运行:

npm init -y 
  • 1

2、安装koa

npm i koa -s 
  • 1

3、安装nodemon

npm i nodemon -g 
  • 1

4、使用路由中间件

npm i koa-router -S 
  • 1

5、跨域处理

npm i koa2-cors 
  • 1

6、新建routes/index.js

const router = require("koa-router")();
let accessToken = "init_s_token"; //短token
let refreshToken = "init_l_token"; //长token

/* 5s刷新一次短token */
setInterval(() => {accessToken = "s_tk" + Math.random();
}, 5000);

/* 一小时刷新一次长token */
setInterval(() => {refreshToken = "l_tk" + Math.random();
}, 600000);

/* 登录接口获取长短token */
router.get("/login", async (ctx) => {ctx.body = {returncode: 0,accessToken,refreshToken,};
});

/* 获取短token */
router.get("/refresh", async (ctx) => {//接收的请求头字段都是小写的let { pass } = ctx.headers;if (pass !== refreshToken) {ctx.body = {returncode: 108,info: "长token过期,重新登录",};} else {ctx.body = {returncode: 0,accessToken,};}
});

/* 获取应用数据1 */
router.get("/getData", async (ctx) => {let { authorization } = ctx.headers;if (authorization !== accessToken) {ctx.body = {returncode: 104,info: "token过期",};} else {ctx.body = {code: 200,returncode: 0,data: { id: Math.random() },};}
});

/* 获取应用数据2 */
router.get("/getData2", async (ctx) => {let { authorization } = ctx.headers;if (authorization !== accessToken) {ctx.body = {returncode: 104,info: "token过期",};} else {ctx.body = {code: 200,returncode: 0,data: { id: Math.random() },};}
});

module.exports = router; 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

7、创建index.js文件

const Koa = require('koa')
const app = new Koa();
const index = require('./routes/index')

const cors = require('koa2-cors');

app.use(cors());

app.use(index.routes(),index.allowedMethods())

app.listen(4000,() => {console.log('server is listening on port 4000')
}) 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

8、`配置package.json

"dev":"nodemon index.js", 
  • 1

9、运行 npm run dev,这时服务端已准备好

npm run dev
  • 1

前端源码

interceptors.ts

/** axios封装
 * 请求拦截、相应拦截、错误统一处理
 */
import Axios from "axios";
import { ElMessage, ElLoading } from "element-plus";
import _ from "lodash";
import router from "@/router";
import BaseRequest from "@/request/request";
const axios = Axios.create({
  //baseURL: localStorage.getItem("address")?.toString(), // url = base url + request url
  // timeout: 50000 // request timeout
});
// loading对象
let loadingInstance: { close: () => void } | null;
// 变量isRefreshing
let isRefreshing = false;
// 后续的请求队列
let requestList: ((newToken: any) => void)[] = [];
// 请求合并只出现一次loading
// 当前正在请求的数量
let loadingRequestCount = 0;
// post请求头
axios.defaults.headers.post["Content-Type"] = "application/json;charset=UTF-8";
// request interceptor

axios.interceptors.request.use(
  (config: any) => {
    let loadingTarget = "body";
    if (config.headers.loadingTarget) {
      loadingTarget = config.headers.loadingTarget;
    }
    const isShowLoading = config.headers.isShowLoading;
    const target = document.querySelector(loadingTarget);
    if (target && !isShowLoading) {
      // 请求拦截进来调用显示loading效果
      showLoading(loadingTarget);
    }
    // do something before request is sent
    // if (sessionStorage.getItem("token")) {
    //   config.headers.Authorization =
    //     "Bearer " + sessionStorage.getItem("token"); // 让每个请求携带自定义 token 请根据实际情况自行修改
    // }
    if (config.url) {
      // 此处为 Refresh Token 专用接口,请求头使用 Refresh Token
      if (config.url.indexOf("/refresh") >= 0) {
        config.headers.Authorization = localStorage.getItem("RefreshToken");
      } else if (!(config.url.indexOf("/login") !== -1)) {
        // 其他接口,请求头使用 Access Token
        config.headers.Authorization = localStorage.getItem("accessToken");
      }
    }

    return config;
  },
  (error) => {
    // do something with request error
    console.log(error); // for debug
    return Promise.reject(error);
  }
);
// http response 拦截器
axios.interceptors.response.use(
  async (response) => {
    setTimeout(() => {
      hideLoading();
    }, 200);
    const data = response.data;
    if (data.code == "401") {
      // 控制是否在刷新token的状态
      if (!isRefreshing) {
        // 修改isRefreshing状态
        isRefreshing = true;
        // 这里是获取新token的接口,方法在这里省略了。
        const url = `/refresh`;
        const BaseRequestFun = new BaseRequest(url, "");
        BaseRequestFun.get().then(async (res) => {
          if (res && res.accessToken) {
            console.log("a");
            // 新token
            const newToken = res.accessToken;
            // 保存新的accessToken
            localStorage.setItem("accessToken", newToken);
            // 替换新accessToken
            response.config.headers.Authorization = newToken;
            // token 刷新后将数组里的请求队列方法重新执行
            requestList.forEach((cb) => cb(newToken));
            // 重新请求完清空
            requestList = [];

            // 继续未完成的请求
            const resp = await axios.request(response.config);
            // 重置状态
            isRefreshing = false;
            // 返回请求结果
            return resp;
          } else {
            // 清除token
            localStorage.clear();
            // 重置状态
            isRefreshing = false;
            // 跳转到登录页
            router.replace("/");
          }
        });
      } else {
        // 后面的请求走这里排队
        // 返回未执行 resolve 的 Promise
        return new Promise((resolve) => {
          // 用函数形式将 resolve 存入,等待获取新token后再执行
          requestList.push((newToken) => {
            response.config.headers.Authorization = newToken;
            resolve(axios(response.config));
          });
        });
      }
    }
    return data;
  },
  (err) => {
    setTimeout(() => {
      hideLoading();
    }, 200);
    // 返回状态码不为200时候的错误处理
    ElMessage({
      message: err.toString(),
      type: "error",
      duration: 5 * 1000,
    });
    return Promise.resolve(err);
  }
);
// 显示loading的函数 并且记录请求次数 ++
const showLoading = (target: any) => {
  if (loadingRequestCount === 0) {
    loadingInstance = ElLoading.service({
      lock: true,
      text: "加载中...",
      target: target,
      background: "rgba(255,255,255,0.5)",
    });
  }
  loadingRequestCount++;
};

// 隐藏loading的函数,并且记录请求次数
const hideLoading = () => {
  if (loadingRequestCount <= 0) return;
  loadingRequestCount--;
  if (loadingRequestCount === 0) {
    toHideLoading();
  }
};

// 防抖:将 300ms 间隔内的关闭 loading 便合并为一次. 防止连续请求时, loading闪烁的问题。
const toHideLoading = _.debounce(() => {
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  loadingInstance.close();
  loadingInstance = null;
}, 300);

export default axios;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163

request.ts

import instance from "./interceptors";
import { ElMessage } from "element-plus";

export default class baseRequest {
  private url: any;
  private params: any;

  constructor(url: any, params: any) {
    this.url = url;
    this.params = typeof params === "undefined" ? {} : params;
  }

  get(...params: any[]) {
    return instance
      .get(this.url, {
        params: this.params,
        headers: {
          loadingTarget: params[0],
          isShowLoading: params[1] === undefined ? true : params[1],
        },
      })
      .then((res: any) => {
        if (res.code === 200) {
          return Promise.resolve(res);
        } else {
          ElMessage({
            message: res.entitys[Object.keys(res.entitys)[0]],
            type: "error",
            duration: 5 * 1000,
          });
          return Promise.resolve(false);
        }
      })
      .catch((e) => {
        ElMessage({
          message: e,
          type: "error",
          duration: 5 * 1000,
        });
        Promise.resolve(false);
      });
  }

  post(...params: any[]) {
    return instance
      .post(this.url, this.params, {
        headers: {
          loadingTarget: params[0],
          isShowLoading: params[1] === undefined ? true : params[1],
        },
      })
      .then((res: any) => {
        if (res.code === "200") {
          return Promise.resolve(res.entitys);
        } else {
          ElMessage({
            message: res.entitys[Object.keys(res.entitys)[0]],
            type: "error",
            duration: 5 * 1000,
          });
          Promise.resolve(false);
        }
      })
      .catch((e) => {
        ElMessage({
          message: e,
          type: "error",
          duration: 5 * 1000,
        });
        Promise.resolve(false);
      });
  }

  put(...params: any[]) {
    return instance
      .put(this.url, this.params, {
        headers: {
          loadingTarget: params[0],
          isShowLoading: params[1] === undefined ? true : params[1],
        },
      })
      .then((res: any) => {
        if (res.code === "200") {
          return Promise.resolve(res.entitys);
        } else {
          ElMessage({
            message: res.entitys[Object.keys(res.entitys)[0]],
            type: "error",
            duration: 5 * 1000,
          });
          Promise.resolve(false);
        }
      })
      .catch((e) => {
        ElMessage({
          message: e,
          type: "error",
          duration: 5 * 1000,
        });
        Promise.resolve(false);
      });
  }

  delete(...params: any[]) {
    return instance
      .delete(this.url, {
        params: this.params,
        headers: {
          loadingTarget: params[0],
          isShowLoading: params[1] === undefined ? true : params[1],
        },
      })
      .then((res: any) => {
        if (res.code === "200") {
          return Promise.resolve(res.entitys);
        } else {
          ElMessage({
            message: res.entitys[Object.keys(res.entitys)[0]],
            type: "error",
            duration: 5 * 1000,
          });
          Promise.resolve(false);
        }
      })
      .catch((e) => {
        ElMessage({
          message: e,
          type: "error",
          duration: 5 * 1000,
        });
        Promise.resolve(false);
      });
  }

  upfile(...params: any[]) {
    return instance
      .post(this.url, this.params, {
        headers: {
          "Content-Type": "multipart/form-data",
          "X-Requested-With": "XMLHttpRequest",
          loadingTarget: params[0],
          isShowLoading: params[1] === undefined ? true : params[1],
        },
      })
      .then((res: any) => {
        if (res.code === "200") {
          return Promise.resolve(res.entitys);
        } else {
          ElMessage({
            message: res.entitys[Object.keys(res.entitys)[0]],
            type: "error",
            duration: 5 * 1000,
          });
          Promise.resolve(false);
        }
      })
      .catch((e) => {
        ElMessage({
          message: e,
          type: "error",
          duration: 5 * 1000,
        });
        Promise.resolve(false);
      });
  }

  downfile(...params: any[]) {
    return instance
      .post(this.url, this.params, { responseType: "blob" })
      .then((res: any) => {
        const fileReader = new FileReader();
        fileReader.onload = function (e: any) {
          try {
            const jsonData = JSON.parse(e.target.result); // 说明是普通对象数据,后台转换失败
            if (jsonData.code) {
              ElMessage({
                message: jsonData.message,
                type: "error",
                duration: 5 * 1000,
              });
              Promise.resolve(false);
            }
          } catch (err) {
            // 解析成对象失败,说明是正常的文件流
            const url = window.URL.createObjectURL(res);
            const eleLink = document.createElement("a");
            eleLink.href = url;
            eleLink.download = params[2];
            // eleLink.download = "1.xls";
            document.body.appendChild(eleLink);
            eleLink.click();
            window.URL.revokeObjectURL(url);
          }
        };
        fileReader.readAsText(res);
      })
      .catch((e) => {
        ElMessage({
          message: e,
          type: "error",
          duration: 5 * 1000,
        });
        Promise.resolve(false);
      });
  }
  icd9Export() {
    return instance
      .post(this.url, this.params, { responseType: "blob" })
      .then((res: any) => {
        const fileReader = new FileReader();
        fileReader.onload = function (e: any) {
          try {
            const jsonData = JSON.parse(e.target.result); // 说明是普通对象数据,后台转换失败
            if (jsonData.code) {
              ElMessage({
                message: jsonData.message,
                type: "error",
                duration: 5 * 1000,
              });
              Promise.resolve(false);
            }
          } catch (err) {
            // 解析成对象失败,说明是正常的文件流
            const url = window.URL.createObjectURL(res);
            const eleLink = document.createElement("a");
            eleLink.href = url;
            eleLink.download = "icd9.xls";
            document.body.appendChild(eleLink);
            eleLink.click();
            window.URL.revokeObjectURL(url);
          }
        };
        fileReader.readAsText(res);
      })
      .catch((e) => {
        ElMessage({
          message: e,
          type: "error",
          duration: 5 * 1000,
        });
        Promise.resolve(false);
      });
  }
  icd10Export() {
    return instance
      .post(this.url, this.params, { responseType: "blob" })
      .then((res: any) => {
        const fileReader = new FileReader();
        fileReader.onload = function (e: any) {
          try {
            const jsonData = JSON.parse(e.target.result); // 说明是普通对象数据,后台转换失败
            if (jsonData.code) {
              ElMessage({
                message: jsonData.message,
                type: "error",
                duration: 5 * 1000,
              });
              Promise.resolve(false);
            }
          } catch (err) {
            // 解析成对象失败,说明是正常的文件流
            const url = window.URL.createObjectURL(res);
            const eleLink = document.createElement("a");
            eleLink.href = url;
            eleLink.download = "icd10.xls";
            document.body.appendChild(eleLink);
            eleLink.click();
            window.URL.revokeObjectURL(url);
          }
        };
        fileReader.readAsText(res);
      })
      .catch((e) => {
        ElMessage({
          message: e,
          type: "error",
          duration: 5 * 1000,
        });
        Promise.resolve(false);
      });
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283

测试vue

<template>
  <div>
    <el-button type="primary" @click="login()">登录</el-button>
    <el-button type="primary" @click="getData()">接口一</el-button>
    <el-button type="primary" @click="getData2()">接口二</el-button>
  </div>
</template>

<script lang="ts" setup>
import BaseRequest from "@/request/request";
const login = () => {
  const url = `/login`;
  const BaseRequestFun = new BaseRequest(url, "");
  BaseRequestFun.get().then((res) => {
    if (res) {
      console.log();
      localStorage.setItem("accessToken", res.accessToken);
      localStorage.setItem("RefreshToken", res.refreshToken);
    }
  });
};
const getData = () => {
  const url = `/getData`;
  const BaseRequestFun = new BaseRequest(url, "");
  BaseRequestFun.get().then((res) => {
    if (res) {
      console.log(res);
    }
  });
};
const getData2 = () => {
  const url = `/getData2`;
  const BaseRequestFun = new BaseRequest(url, "");
  BaseRequestFun.get().then((res) => {
    if (res) {
      console.log(res);
    }
  });
};
</script>

<style lang="scss"></style>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/正经夜光杯/article/detail/886523
推荐阅读
相关标签
  

闽ICP备14008679号