• fuse-line:熔断机制,如果短时间内多次调用,则停止响应一段时间,类似于 TCP 慢启动。用于解决refreshLoginlogin等方法的并发处理问题。

  • single-queue:单队列模式,同一时间,只允许一个正在过程中的网络请求。请求被锁定之后,同样的请求都会被推入队列,等待进行中的请求返回后,消费同一个结果。用于解决refreshLoginlogin等方法的并发处理问题。

4. 静默登录的调用时机

=============

4.1 小程序启动时调用


由于大部分情况都需要依赖登录态,在小程序启动的时候(app.onLaunch())调用静默登录是最常见的手段。这里我们封装一个login函数如下所示,首先调用wx.checkSession判断session_key是否过期,如果session_key未过期且本地存在auth_token自定义登录态,表示当前的静默登录态仍然有效,无需进行其它操作。否则,表示静默登录态失效或者新用户从未发起过静默登录,那么发起静默登录流程。

public async login(): Promise {

// 调用wx.checkSession判断session_key是否过期

const hasSession = await checkSession();

// 本地已有可用登录态且session_key未过期,resolve。

if (this.getAuthToken() && hasSession) return Promise.resolve();

// 否则,发起静默登录

await this.silentLogin();

}

但是由于原生的小程序启动流程中, App,Page,Component 的生命周期钩子函数,都不支持异步阻塞。所以很有可能出现小程序页面加载完成后,静默登录过程还没有执行完毕的情况,这会导致后续一些依赖登录态的操作(比如请求发起)出错

4.2 接口请求发起时调用


保险起见,如果某些接口需要携带自定义登录态进行鉴权,则需要在请求发起时进行拦截,校验登录态,并刷新登录。刷新登录代码如下所示:

public async refreshLogin(): Promise {

try {

// 清除 Session

this.clearSession();

// 发起静默登录

await this.silentLogin();

} catch (error) {

throw error;

}

}

整个流程如下图所示:

  • 拦截 request
  1. 判断是否需要鉴权:请求发起时,拦截请求,判断请求是否需要添加auth-token,如若不需要,直接发起请求。如若需要,执行第二步。

  2. 判断是否需要发起静默登录:判断 storage 中是否存在auth-token,如若不存在,发起「刷新登录」。

  3. 请求头部添加auth-token:添加auth-token,发起请求。

  • 与服务端通信:发起请求,服务端处理请求返回结果。

  • 拦截 response: 解析状态码

    1. 状态码为AUTH_FAIL:服务端返回code为“鉴权失败”,触发这种情景的原因有两个,一是接口需要鉴权,但是发起请求时未携带auth-token,二是auth-token过期。这时将上一次请求携带的auth-token与本地存储的auth-token比较,如果不一致,表示登录态已经刷新过了,那么就直接重新发起请求。如果一致,发起刷新登录,拿到新的auth-token后重新发起请求,这个动作对用户来说是无感知的
  1. 状态码为USER_WX_SESSIONKEY_EXPIRE:服务器返回code为“用户登录态过期”,这是针对用户授权手机号登录失败定制的状态码,如果登录态已过期,表示存储在服务端的session_key也是过期的,那么点击授权手机号获取的加密数据发送到服务端进行对称解密,由于session_key失效,无法解密出真正的手机号。因此需要重新发起静默登录,等待用户重新点击授权按钮获取新的加密数据,然后发起新的解密请求

  2. 状态码为其它:比如Success或者其他业务请求错误的情况,不进行拦截,返回 response 让业务代码解析。

4.3 wx.checkSession 罢工之谜


基于上述接口请求发起时调用的流程,很多人会有疑问,既然服务端会返回auth-token过期的状态码,为啥不在请求发送前进行拦截,使用wx.checkSession接口校验登录态是否过期(如下图所示,增加红框内的步骤)?

这是因为,我们通过实验发现,在 session_key 已过期的情况下,wx.checkSession 有一定的几率返回true。即增加wx.checkSession步骤并不能百分百保证登录态不会过期,后续仍然需要对不同的状态码进行处理。

社区也有相关的反馈未得到解决:

  • 小程序解密手机号,隔一小段时间后,checksession:ok,但是解密失败

  • wx.checkSession 有效,但是解密数据失败

  • checkSession 判断 session_key 未失效,但是解密手机号失败

所以结论是:wx.checkSession可靠性是不达 100% 的。

基于以上,我们需要对 session_key 的过期做一些容错处理:

  1. 发起需要使用 session_key 的请求前,做一次 wx.checkSession 操作,如果失败了刷新登录态。

  2. 后端使用session_key解密开放数据失败之后,返回特定错误码(如:USER_WX_SESSIONKEY_EXPIRE),前端刷新登录态。

4.4 并发处理


我们知道,当启动小程序时,各种监控、埋点数据上报都需要获取用户的个人信息,这些信息都得「静默登录」后才能获取,因此会同时发起多个login请求。另一种情况下,假设一个新用户进入一个业务复杂的页面,同时发起五个不同的业务请求,恰巧这五个请求都需要鉴权,那么五个请求都会被拦截并发起refreshLogin请求。显然,这样的并发是不合理的。

基于此,我们设计了如下方案:

  • 单队列模式
  1. 请求锁:同一时间,只允许一个正在过程中的网络请求。

  2. 等待队列:请求被锁定之后,同样的请求都会被推入队列,等待进行中的请求返回后,消费同一个结果。

  • 熔断机制:如果短时间内多次调用,则停止响应一段时间,类似于 TCP 慢启动。

如上图所示,首先refreshLogin请求入队,队列中只有一个请求,发送该请求,同时保险丝计入次数 1,服务端返回请求结果,消费结果。接着又发起一个refreshLogin请求,队列中只有一个请求,发送该请求,同时保险丝计入次数 2。然后又连续发起三个请求,由于上一个请求还没有执行完成,将这三个请求入队,等待上一个请求结果返回,队列中的四个请求消费同一个结果。由于触发自动冷却阈值,保险丝重置。

以上两种方案通过装饰器模式引入,代码如下所示,refreshLogin函数其实是slientLogin函数的一层封装,用于接口发起时调用。而前面提到的login函数也是slientLogin函数的一层封装,用户小程序启动时调用。

@singleQueue({ name: ‘refreshLogin’ })

@fuseLine({ name: ‘refreshLogin’ })

public async refreshLogin(): Promise {

try {

// 清除 Session

this.clearSession();

await this.silentLogin();

} catch (error) {

throw error;

}

}

到此,很多读者可能对熔断机制还不甚理解,熔断的目的是为一个函数提供保险丝保障,短时间内多次调用,会熔断一段时间,这段时间内拒绝所有请求。如果在自动冷却阈值内,没有请求通过,则重置保险丝。代码如下所示:

export default function fuseLine({

// 一次熔断前重试次数

tryTimes = 3,

// 重试间隔,单位 ms

restoreTime = 5000,

// 自动冷却阈值,单位 ms

coolDownThreshold = 1000,

// 名称

name = ‘unnamed’,

}: {

tryTimes?: number;

restoreTime?: number;

name?: string;

coolDownThreshold?: number;

} = {}) {

// 请求锁

let fuseLocked = false;

// 当前重试次数

let fuseTryTimes = tryTimes;

// 自动冷却

let coolDownTimer;

// 重置保险丝

const reset = () => {

fuseLocked = false;

fuseTryTimes = tryTimes;

logger.info(${name}-保险丝重置);

};

const request = async () => {

if (fuseLocked) throw new Error(${name}-保险丝已熔断,请稍后重试);

// 已达最大重试次数

if (fuseTryTimes <= 0) {

fuseLocked = true;

// 重置保险丝

setTimeout(() => reset(), restoreTime);

throw new Error(${name}-保险丝熔断!!);

}

// 自动冷却系统

if (coolDownTimer) clearTimeout(coolDownTimer);

coolDownTimer = setTimeout(() => reset(), coolDownThreshold);

// 允许当前请求通过保险丝,记录 +1

fuseTryTimes = fuseTryTimes - 1;

logger.info(${name}-通过保险丝(${tryTimes - fuseTryTimes}/${tryTimes}));

return Promise.resolve();

};

return function(

_target: Record<string, any>,

_propertyName: string,

descriptor: TypedPropertyDescriptor<(…args: any[]) => any>,

) {

const method = descriptor.value;

descriptor.value = async function(…args: any[]) {

await request();

if (method) return method.apply(this, args);

};

};

}

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注:前端)
img

最后

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

最后写上我自己一直喜欢的一句名言:世界上只有一种真正的英雄主义就是在认清生活真相之后仍然热爱它

下面V无偿领取!(备注:前端)**
[外链图片转存中…(img-6RN4TbRd-1711033502566)]

最后

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

最后写上我自己一直喜欢的一句名言:世界上只有一种真正的英雄主义就是在认清生活真相之后仍然热爱它

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐