注册
web

无感刷新,我想说说这三种方案

现在当你想去找一个无感刷新的方案的时候,搜出来的大多都是教你在aioxs的相应拦截器里面去截取当前请求的config。然后当token刷新后再去请求失败的接口。首先声明,这个方案完全没有任何问题。只是有可以优化的地方,这个优化的地方可以在我将要写的第二种方案中得到解决。


准备工作


接口服务


在实行方案之前需要准备好相关的接口服务,我会用node写一些登录刷新和正常的业务接口, 点这里查看


简单介绍下准备的接口及其作用



  • /login: 模拟登录并返回tokenrefreshToken
  • /refreshToken: 当token过期,请求这个接口会获得新的tokenrefreshToken,接口需要传入通过/login接口或者/refreshToken接口返回的refreshToken。当refreshToken也判断过期,就只能去登陆页。
  • /test1: 模拟正常的业务请求接口,会验证token是否有效及是否过期,过期返回401
  • /test2: 模拟正常的业务请求接口,会验证token是否有效及是否过期,过期返回401
  • /test3: 模拟正常的业务请求接口,会验证token是否有效及是否过期,过期返回401

token的过期时间设置为5秒。


axios的封装


我们使用axios都会进行二次封装,都会在拦截器里面处理一些逻辑。这里给出一个最基本的封装,后面都会用到。


import axios from "axios";

export const service = axios.create({
  timeout: 1000 * 30,
  baseURL: "http://192.168.0.102:9001",
});

service.interceptors.request.use(
  (config) => {
    let token = localStorage.getItem("token");
    config.headers["Authorization"] = token;
    return config;
  },
  (err) => Promise.reject(err)
);

service.interceptors.response.use(
  (res) => {
    return res.data;
  },
  (err) => {
    return Promise.reject(err);
  }
);

export const get = (url, params) => {
  return new Promise((resolve, reject) => {
    service
      .get(url, { params })
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      });
  });
};

export const post = (url, data = {}) => {
  return new Promise((resolve, reject) => {
    service
      .post(url, data)
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      });
  });
};

首先还是介绍下最广泛的使用方案


在axios的响应拦截器里面处理


这种方案的工作流程都是在相应拦截器里面处理的,当判断到接口401,则请求刷新token接口,并改变一个状态来表明当前正在执行刷新token,这样可以避免多个请求同时401的时候所导致的一次401就会请求一次刷新token接口。接着将紧随着报401的接口保存起来,等到刷新token接口成功后再去执行这些失败的接口。完整代码,在上文的二次封装的axios基础上进行更改。


import axios from "axios";
----------------------------------------------------------------新增
import { refreshToken } from "@/api/login";
----------------------------------------------------------------新增

export const service = axios.create({
  timeout: 1000 * 30,
  baseURL: "http://192.168.0.102:9001",
});

service.interceptors.request.use(
  (config) => {
    let token = localStorage.getItem("token");
    config.headers["Authorization"] = token;
    return config;
  },
  (err) => Promise.reject(err)
);

----------------------------------------------------------------新增
let inRefreshing = false; // 当前是否正在请求刷新token
let wating = []; // 报401的接口 加入等待列表 刷新接口成功后统一请求
----------------------------------------------------------------新增

service.interceptors.response.use(
  (res) => {
    return res.data;
  },
  (err) => {
----------------------------------------------------------------新增
    let { config } = err.response;

    if (inRefreshing) { // 刷新token正在请求,把其他的接口加入等待数组
      return new Promise((resolve) => {
        wating.push({
          config,
          resolve,
        });
      });
    }

    if (err?.response?.status === 401) {
      inRefreshing = true;

      refreshToken({ refreshToken: localStorage.getItem("refreshToken") }).then(
        (res) => {
          const { success, response } = res;
          if (success) {
            inRefreshing = false;

            const { token, refreshToken } = response;
            localStorage.setItem("token", token);
            localStorage.setItem("refreshToken", refreshToken);

// 刷新token请求成功,等待数据的失败接口重新发起请求
            wating.map(({ config, resolve }) => {
              resolve(service(config));
            });
            wating = []; // 请求完之后清空等待请求的数组

            return service(config); // 当前接口重新发起请求
          } else {
            // 刷新token失败  重新登录
          }
        }
      );
    }
----------------------------------------------------------------新增
    return Promise.reject(err);
  }
);

export const get = (url, params) => {
  return new Promise((resolve, reject) => {
    service
      .get(url, { params })
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      });
  });
};

export const post = (url, data = {}) => {
  return new Promise((resolve, reject) => {
    service
      .post(url, data)
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      });
  });
};

有些同学可能对上面代码有些疑问,感觉wating里面的数据不对,比如同时发送三个接口报401,wating只会加入两条接口数据,还有一条在哪儿。其实就在第60行返回了。


然后再对接口进行简单的封装


import { post } from "./index";

export const login = () => post("/login");
export const refreshToken = (data) => post("/refreshToken", data);

export const test1 = () => post("/test1");
export const test2 = () => post("/test2");
export const test3 = () => post("/test3");

接下来看结果。点击按钮会发送三条接口请求。

第一次点击发送请求token还未过期,第二次点击发送请求token已过期。
1.gif


完美,没啥问题!因为上面给大家展示的是最简单的axios封装,所以在响应拦截器里面加上一些接口重发的逻辑好像没啥问题。但是如果还有对接口数据的加密解密过程呢?还有控制加载的时候loading窗完美展示的问题呢?这样一看在二次封装的axios里面加上接口重发就有点儿太多了。再比如说我用的不是axios怎么办,是不是又得根据请求的插件去改一些东西。


那么与axios拦截器解耦就成了需要做的事了。


与axios拦截器解耦


我们需要创建一个构造函数,这个构造函数会将报401的接口收集起来,等到刷新token接口成功后再去请求


import { refreshToken } from "@/api/login";

function requestCollection() {
  let inRefreshing = false; // 当前是否正在请求刷新token
  let wating = []; // 报401的接口 加入等待列表 刷新接口成功后统一请求

  return (request) => {
    return new Promise((resolve) =>
      request()
        .then((res) => {
          resolve(res);
        })
        .catch(async (err) => {
          if (err.response.status == 401) {
            if (inRefreshing) {
              // 加入等待执行的数组
              return wating.push({
                request,
                resolve,
              });
            }

            inRefreshing = true;

            await RT();

            wating.map(({ resolve, request }) => {
              resolve(request());
            });
            wating = [];

            return resolve(request());
          }
        })
    );
  };
}

const RT = () => {
  return new Promise((resolve) =>
    refreshToken({
      refreshToken: localStorage.getItem("refreshToken"),
    }).then((res) => {
      const { success, response } = res;
      if (success) {
        const { token, refreshToken } = response;
        localStorage.setItem("token", token);
        localStorage.setItem("refreshToken", refreshToken);

        resolve();
      }
    })
  );
};

export default requestCollection;

其实内在的处理逻辑与第一种大同小异,主要在于将这一部分逻辑抽离出来。


使用


import { post } from "./index2";
import requestCollection from "./RequestCollection";

const check = new requestCollection();

export const login = () => check(() => post("/login"));
export const refreshToken = (data) => check(() => post("/refreshToken", data));

export const test1 = () => check(() => post("/test1"));
export const test2 = () => check(() => post("/test2"));
export const test3 = () => check(() => post("/test3"));

效果演示


2.gif


这种方案存在的一个小问题
因为需要对接口进行一层包裹,所以如果你的项目已经运行有一段时间了突然来个需求说想要个无感刷新token,那其实这个方案会随着你的项目的大小而加大你的工作量。


上述两种方案都存在的不足
当我们都在讨论无感刷新token的方案都是如何接口重发的时候,大家都忽略了一个问题,那就是接口重发的过程是发送了原本两倍+1的接口数量。如果这个接口本来就慢,恰好碰上了401,那用户就得花约两倍时间去等一个结果,相信你不愿意等,我也不愿意等。


那么被大家诟病会造成一定性能浪费的定时任务刷新就可以解决这个问题,并且定时任务刷新也是解耦于二次封装的axios。 其实都到了2023年,一个定时任务会对最终用户所使用的页面造成卡顿的影响可以说完全感知不到。但是我也不推荐大家无意识的随便使用定时器或者闭包一类能通过一点点累加所造成的内存泄露的性能问题。


定时任务刷新


这个就不给代码了,主要注意几个点就行



  • 后端需要配合返回一个token的过期时间
  • 定时任务全局存在
  • 刷新token成功后需要更新过期时间重新计算

总结


其实上面三种方案都有自己的好处和缺点,无论你使用哪一种方案,都没有问题。这些需要你结合自己的项目来选择适合你的方案。


作者:谁是克里斯
来源:juejin.cn/post/7302404170412802074

0 个评论

要回复文章请先登录注册