首页 > 编程语言 >小程序静默登录, 自定义Promise.all实现业务逻辑的封装

小程序静默登录, 自定义Promise.all实现业务逻辑的封装

时间:2023-02-17 16:24:37浏览次数:38  
标签:封装 自定义 登录 res Promise log reject console 请求

前言

做了一个小程序, 里面涉及到用户授权以及登录的情况, 初次登录需要获取用户信息, 然后再走登录流程, 后续就不需要用户授权了, 就可以直接走登录流程了
同时有的数据需要登录之后才能获取, 如果未登录则返回 401, 此时需要先登录, 然后再去获取数据
也就是说需要做静默登录的操作: 请求数据, 登录了则正常获取数据, 而如果未登录则需要先登录然后才能获取数据
当页面只有一个数据来源的时候, 只需要正常发起请求, 遇到 401 就去登录, 然后再执行这个获取数据的函数即可, 但当页面上有多个, 比如两个数据来源的时候, 遇到 401 然后重新请求数据, 此时就可能发生这么一个情况: 没登录, 两个接口都返回 401 然后都去登录, 就会登录两遍, 虽然登录两遍也只是除了第一次之外又刷新了一次登录的有效期

模拟获取数据的函数

不得不说, 后端返回的数据的格式各家公司有各家的规范, 有的公司在处理异常的时候会返回相应的 status code, 而有的则是全部按 200 处理然后通过响应体中的 code 字段来标识, code 为 0 表示成功, 为其他数字表示异常, 同时这些 code 和除了 200 之外的其他 status code 是一一对应的, 比如 401 表示未授权这样的

首先需要创建一个函数, 模拟请求发送, 登录了返回相应的数据, 未登录则返回 401 和失败原因这些信息, 同时, 由于需要控制请求的失败与否来测试的代码, 因此还需要在这个函数的外部定义一个变量, 让的这个函数可以根据这个外部变量来修改自己的返回结果, 为了便于测试, 这样的函数写两个:

let isFooSuccess = false;
let isFoo2Success = true;

//请求foo数据的函数
const foo = ({ delay }) =>
  new Promise((resolve, reject) => {
    console.log("foo被调用, isFooSuccess", isFooSuccess);

    const res = {
      code: isFooSuccess ? 0 : 401,
      msg: isFooSuccess ? "foo成功" : "foo失败, 需要登录",
      data: isFooSuccess
        ? [
            {
              a: 1,
            },
          ]
        : [],
    };

    setTimeout(() => {
      isFooSuccess ? resolve(res) : reject(res);
    }, delay);
  });

//请求foo2数据的函数
const foo2 = ({ delay }) =>
  new Promise((resolve, reject) => {
    console.log("foo2被调用, isFoo2Success", isFoo2Success);

    const res = {
      code: isFoo2Success ? 0 : 401,
      msg: isFoo2Success ? "foo2成功" : "foo2失败, 需要登录",
      data: isFooSuccess
        ? [
            {
              a: 1,
            },
          ]
        : [],
    };

    setTimeout(() => {
      isFoo2Success ? resolve(res) : reject(res);
    }, delay);
  });

两个函数都需要返回一个 promise, 参数的话是考虑到实际中会有多个参数的情况, 所以传递的是一个 object, 为了方便调试也添加了输出语句, 两个函数返回的结果的结构是一样的, 其实也就是一个封装过的 request, 它请求后端接口, 然后返回一个 promise

模拟登录的函数

接下来就是模拟登录的函数了:

//登录
const login = (isLoginSuccess, delay) =>
  new Promise((resolve, reject) => {
    console.log("login被调用, isLoginSuccess", isLoginSuccess);

    const res = {
      msg: isLoginSuccess ? "success" : "fail",
      code: isLoginSuccess ? 0 : 123,
      isLoginSuccess,
    };

    isFooSuccess = isLoginSuccess;
    isFoo2Success = isLoginSuccess;

    setTimeout(() => {
      isLoginSuccess ? resolve(res) : reject(res);
    }, delay);
  });

登录之后需要将上面提到的外部变量做一个修改, 从而当再次请求的时候, '后端接口'才知道的登录情况

处理接口 401 的函数

同时还需要一个处理 401 的函数, 同时也是这一整个需求的关键代码: 当响应体中的 code 的值为 401 的时候就做错误处理, 然后登录, 接着再再次请求数据, 以及如果遇到其他异常, 那么也要用 reject 抛出
也就是: 遇到请求返回的结果中 code 为 401, 则返回一个 reject 的 promise, 否则 resolve:

//遇到请求返回的结果中code为401, 则返回一个reject的promise, 否则resolve
const handlePromise401Reject = (promiseReq, promiseReqParams) => {
  const finalPromise = promiseReqParams
    ? promiseReq(promiseReqParams)
    : promiseReq();

  return new Promise((resolve, reject) => {
    finalPromise
      .then((res) => {
        const { code } = res;

        code === 401 ? reject(res) : resolve(res);
      })
      .catch((error) => {
        reject(error);
      });
  });
};

封装一个处理 401 的函数, 这个函数返回一个 promise, 同时它也接收一个返回 promise 的请求函数, 以及这个请求函数所需要的参数, 当然也要考虑没有参数的情况

有了请求函数, 也有了封装的处理 401 的函数, 现在使用一下:

handlePromise401Reject(foo, { delay: 1000 })
  .then((res) => {
    console.log("成功", res);
  })
  .catch((error) => {
    console.log("失败", error);
  });

可以看到, 接口返回 401, 此时 promise 的状态为 reject, 最终结果进到了 catch 回调中, 符合预期

结合起来

接着, 需要把上面提到的部分都结合起来, 也就是: 获取数据, 登录未过期则返回数据, 登录过期就先登录然后再去获取数据:

let isFooSuccess = false;
let isFoo2Success = true;

//请求foo数据的函数
const foo = ({ delay }) =>
  new Promise((resolve, reject) => {
    console.log("foo被调用, isFooSuccess", isFooSuccess);

    const res = {
      code: isFooSuccess ? 0 : 401,
      msg: isFooSuccess ? "foo成功" : "foo失败, 需要登录",
      data: isFooSuccess
        ? [
            {
              a: 1,
            },
          ]
        : [],
    };

    setTimeout(() => {
      isFooSuccess ? resolve(res) : reject(res);
    }, delay);
  });

//请求foo2数据的函数
const foo2 = ({ delay }) =>
  new Promise((resolve, reject) => {
    console.log("foo2被调用, isFoo2Success", isFoo2Success);

    const res = {
      code: isFoo2Success ? 0 : 401,
      msg: isFoo2Success ? "foo2成功" : "foo2失败, 需要登录",
      data: isFooSuccess
        ? [
            {
              a: 1,
            },
          ]
        : [],
    };

    setTimeout(() => {
      isFoo2Success ? resolve(res) : reject(res);
    }, delay);
  });

//登录
const login = (isLoginSuccess, delay) =>
  new Promise((resolve, reject) => {
    console.log("login被调用, isLoginSuccess", isLoginSuccess);

    const res = {
      msg: isLoginSuccess ? "success" : "fail",
      code: isLoginSuccess ? 0 : 123,
      isLoginSuccess,
    };

    isFooSuccess = isLoginSuccess;
    isFoo2Success = isLoginSuccess;

    setTimeout(() => {
      isLoginSuccess ? resolve(res) : reject(res);
    }, delay);
  });

//遇到请求返回的结果中code为401, 则返回一个reject的promise, 否则resolve
const handlePromise401Reject = (promiseReq, promiseReqParams) => {
  const finalPromise = promiseReqParams
    ? promiseReq(promiseReqParams)
    : promiseReq();

  return new Promise((resolve, reject) => {
    finalPromise
      .then((res) => {
        const { code } = res;

        code === 401 ? reject(res) : resolve(res);
      })
      .catch((error) => {
        reject(error);
      });
  });
};

//请求数据的函数
const getData = () => {
  handlePromise401Reject(foo, { delay: 1000 })
    .then((res) => {
      console.log("成功", res);
    })
    .catch((error) => {
      console.log("失败", error);

      login(true, 1000)
        .then((res) => {
          console.log("登录成功", res);

          getData();
        })
        .catch((error) => {
          console.log("登录失败", error);
        });
    });
};

getData();

请求数据, 首先请求 foo 数据, 返回 401, 此时登录, 登录成功之后再次请求, 请求成功, 没问题

多个请求

但实际情况是经常会需要处理多个请求的情形, 此时怎么办呢?
比如此时同时请求 foo 数据和 foo2 数据, 遇到任意一个返回 401, 那么就去登录, 登录成功再次做请求, 也就是: 多个 promise, 只要其中一个 reject 了, 那么最终的结果就是 reject, 这个场景是不是似曾相识? 对的, 这就是 Promise.all 的处理逻辑, 所以刚才提到的这个情形, 可以用 Promise.all 来处理
其余代码不变, 修改请求数据的函数 getData 如下:

//请求数据的函数
const getData = () => {
  Promise.all([
    handlePromise401Reject(foo, { delay: 1000 }),
    handlePromise401Reject(foo2, { delay: 1000 }),
  ])
    .then((res) => {
      console.log("成功", res);
    })
    .catch((error) => {
      console.log("失败", error);

      login(true, 1000)
        .then((res) => {
          console.log("登录成功", res);

          getData();
        })
        .catch((error) => {
          console.log("登录失败", error);
        });
    });
};

getData();

这样就解决了这个需求, 但仔细一看还是不够完美, 为什么呢? 比如会将 handlePromise401Reject 用作一个公共的 utils, 然后使用的时候 import, 但这里还需要登录, 还要导入 login, 毕竟 401 了需要做登录的操作, 也就是说在使用的时候需要导入 handlePromise401Reject 和 login, 这里能不能封装一个函数, 直接将 login 放进去, 用这个函数去请求数据, 401 了登录, 登录完毕之后将请求到的数据给 resolve 出来, 大概像这样:

const getData = () => {
  my401PromiseAll([
    handlePromise401Reject(foo, { delay: 1000 }),
    handlePromise401Reject(foo2, { delay: 1000 }),
  ])
    .then((res) => {
      console.log("数据获取成功:", res);
    })
    .catch((error) => {
      console.log("数据获取失败:", error);
    });
};

getData();

更完美的方案

这里还是熟悉的 Pormise.all, 只要有一个 reject 了那么它的结果就 reject, 符合的需求, 此时去登录就好了, 然后就 resolve 了, 这里写一个这样的函数:

const my401PromiseAll = (promiseReqList) => {
  return new Promise((resolve, reject) => {
    console.log("my401PromiseAll被调用");

    Promise.all(promiseReqList)
      .then((res) => {
        console.log("my401PromiseAll resolve", res);

        resolve(res);
      })
      .catch((error) => {
        console.log("my401PromiseAll reject", error);

        login(true, 1000)
          .then((res) => {
            console.log("登录成功", res);

            my401PromiseAll(promiseReqList);
          })
          .catch((error) => {
            console.log("登录失败", error);
          });

        reject(error);
      });
  });
};

const getData = () => {
  my401PromiseAll([
    handlePromise401Reject(foo, { delay: 1000 }),
    handlePromise401Reject(foo2, { delay: 1000 }),
  ])
    .then((res) => {
      console.log("数据获取成功:", res);
    })
    .catch((error) => {
      console.log("数据获取失败:", error);
    });
};

getData();

发现问题了: 死循环

当调用 getData 函数之后, 这段代码的执行是这样:
foo 函数被调用
foo2 函数被调用
my401PromiseAll 函数被调用
foo reject, my401PromiseAll 也 reject
进登录流程中, login 被调用
同时外部 getData 进到了 catch 中, 因为 my401PromiseAll reject 了
登录成功, 递归调用 my401PromiseAll 函数

此时就进入死循环了, 因为 my401PromiseAll 返回的 promise 的状态已经改变了, my401PromiseAll 内部再次修改它的 promise 状态将不起作用, foo reject 导致 my401PromiseAll 也 reject, 此时内部尝试修改 promise 的状态为 resolve(通过登录)则是徒劳:

图中有个 Uncaught (in promise) {code: 401, msg: 'foo 失败, 需要登录', data: Array(0)}, 推测是因为外部 promise 的状态已经改变了, 而在内部尝试再次修改外部 promise 状态导致的, 毕竟 getData 中是写了 catch 回调的, 不然这句也不会输出:
数据获取失败:

这里没有考虑到外部 promise 状态改变之后, 内部无法再次变更外部 promise 的状态, 那怎么办呢?
希望 foo 和 foo2 中任意一个或者两个都 401 就去登录, 登录成功再次执行 foo 和 foo2 然后再把结果返回给, 这个能力 Promise.all 能提供, 但是不够完美, 因此解决方案就是手动实现一个符合自己需求的 Promise.all 即可

最终方案: 自定义 Promise.all

这里自定义的 Promise.all 和原生的 Promise.all 有一些不同, 原生的它状态改变之后无法再次变更, 这里需要使得它的状态变更一次之后再次发生变更, 那么就是说在第一次变更之前, 需要保存一下请求操作, 流程大概如下:

  1. 保存请求
  2. 发送请求, 401, 此时内部状态第一次变更, 但不改变外部状态
  3. 登录, 登录成功之后再次发送请求, 此时需要发送保存的请求, 因为第一次的状态已经变更了, 无法再次变更, 只能产生新的状态
  4. 第二次发送请求, 请求成功, 此时再改变外部状态

这里的关键在于等待第二次发送的请求, 只有在第二次请求结束之后才能改变外部的状态, 而第一次请求发送完成的时候是不能, 相当于让一个任务挂起, 完成之后再继续执行后面的代码
而让任务挂起的操作, 想到了 await, 在 await 语句没有执行完成的时候后面的代码是不会执行的, 要用 await 自然少不了 async, async 函数返回一个 promise, 只需要在这个 async 函数里面做请求, 当第二次请求结束之后将结果 return 出去即可
使用 async/await 随之而来就是遇到 error 的时候没法 catch 了, 此时需要用到 try catch 语句, 从而能让可以 catch 到抛出的 error, 具体代码如下:

const my401PromiseAll = async (promiseReqList) => {
  const reservedPromiseReqList = promiseReqList;

  let res = null;

  console.log("my401PromiseAll被调用");

  try {
    res = await Promise.all(promiseReqList.map((promiseReq) => promiseReq()));
    console.log("my401PromiseAll resolve", res);
  } catch (error) {
    console.log("my401PromiseAll reject", error);

    try {
      const loginRes = await login(true, 1000);
      console.log("登录成功", loginRes);
      res = await Promise.all(
        reservedPromiseReqList.map((promiseReq) => promiseReq())
      );
    } catch (error2) {
      console.log("登录失败", error2);
      res = error2;
    }
  }

  return res;
};

同时使用的时候需要这样:

const getData = () => {
  my401PromiseAll([
    () => handlePromise401Reject(foo, { delay: 1000 }),
    () => handlePromise401Reject(foo2, { delay: 1000 }),
  ])
    .then((res) => {
      console.log("数据获取成功:", res);
    })
    .catch((error) => {
      console.log("数据获取失败:", error);
    });
};

getData();

由于需要保存请求, 那么就不能像原生的 Promise.all 那样传递 promise 实例, 而是传递函数, 这样才可以保存之后再次进行调用
同时需要注意的是 try catch 语句中 try 代码块部分, 当遇到第一个抛出错误的代码之后, try 中抛出错误的代码之后的代码就不会运行了, 会进到 catch 代码块中, 比如:

let res = null;

try {
  res = await foo({ delay: 1000 });
  res = await foo2({ delay: 1000 });
  console.log("try成功", res);
} catch (error) {
  console.log("try没成功", error);
}

当 foo 抛出错误了, 那么它后面的 foo2 就不会执行了, foo2 后面的输出语句也不会执行, 会进到 catch 代码块中执行 catch 代码块中的代码

后续更新

发现了一个可以优化的点: 需要请求多个接口, 当知道需要重新登录的时候这几个请求已经都发出去了, 能不能当第一个接口告诉超时, 就跳过后续所有请求直接去登录, 登录完之后再请求所有全部接口呢?
当然是可以的, 于是优化了一下代码, 具体如下:

const my401PromiseAll = async (promiseReqList) => {
  const reservedPromiseReqList = promiseReqList;

  const isMoreThanOne = promiseReqList.length > 1;

  let res = null;

  try {
    if (isMoreThanOne) {
      res = await Promise.all([promiseReqList[0]()]);
      const restReq = promiseReqList.filter((_, idx) => idx !== 0);
      res = [
        ...res,
        ...(await Promise.all(restReq.map((promiseReq) => promiseReq()))),
      ];
    } else {
      res = await Promise.all(promiseReqList.map((promiseReq) => promiseReq()));
    }
  } catch (error) {
    try {
      await login(true, 1000);
      res = await Promise.all(
        reservedPromiseReqList.map((promiseReq) => promiseReq())
      );
    } catch (error2) {
      res = error2;
    }
  }

  return res;
};

需要请求的接口数量如果大于 1 个, 那么先请求第一个:
第一个接口请求成功, 那么继续请求后续的接口
第一个接口请求失败, 那么跳过后续剩余请求直接登录, 登录完再次请求全部所有接口
倘若接口数量就只有 1 个, 那就走正常的请求逻辑, 成功则 resolve, 否则登录之后再次请求
这个优化有点空间换时间的意味, 毕竟多个并发请求, 拆成了先一个, 再其他, 改成了继发的请求了, 私以为这个优化在用户量比较少的时候不明显, 但用户量一多, 那服务器的压力就可想而知了, 优化的效果就相对明显了

标签:封装,自定义,登录,res,Promise,log,reject,console,请求
From: https://www.cnblogs.com/wp-leonard/p/17130543.html

相关文章