热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

了解微信小程序登录的前端设计与实现

今天微信小程序开发栏目介绍小程序登录的前端设计与实现。

今天微信小程序开发栏目介绍小程序登录的前端设计与实现。

一. 前言

对于登录/注册的设计如此精雕细琢的目的,当然是想让这个作为应用的基础能力,有足够的健壮性,避免出现全站性的阻塞。

同时要充分考虑如何解耦和封装,在开展新的小程序的时候,能更快的去复用能力,避免重复采坑。

登录注册这模块,就像个冰山,我们以为它就是「输入账号密码,就完成登录了」,但实际下面还有各种需要考虑的问题。

基于状态机,我们就可以编写这样的代码:

import { Status } from '@beautywe/plugin-status';// on app.jsApp({    status: {       login: new Status('login');
    },    onLaunch() {
        session            // 发起静默登录调用
            .login()            // 把状态机设置为 success
            .then(() => this.status.login.success())      
            // 把状态机设置为 fail
            .catch(() => this.status.login.fail());
    },
});// on page.jsPage({    onLoad() {      const loginStatus = getApp().status.login;      
      // must 里面会进行状态的判断,例如登录中就等待,登录成功就直接返回,登录失败抛出等。
      loginStatus().status.login.must(() => {        // 进行一些需要登录态的操作...
      });
    },
});

2. 在「第一个需要登录态接口」被调用的时候去发起登录

更进一步,我们会发现,需要登录态的更深层次的节点是在发起的「需要登录态的后端 API 」的时候。

那么我们可以在调用「需要登录态的后端 API」的时候再去发起「静默登录」,对于并发的场景,让其他请求等待一下就好了。

以 fly.js 作为 wx.request() 封装的「网络请求层」,做一个简单的例子:

// 发起请求,并表明该请求是需要登录态的fly.post('https://...', params, { needLogin: true });// 在 fly 拦截器中处理逻辑fly.interceptors.request.use(async (req)=>{  // 在请求需要登录态的时候
  if (req.needLogin !== false) {    // ensureLogin 核心逻辑是:判断是否已登录,如否发起登录调用,如果正在登录,则进入队列等待回调。
    await session.ensureLogin();    
    // 登录成功后,获取 token,通过 headers 传递给后端。
    const token = await session.getToken();    Object.assign(req.headers, { [AUTH_KEY_NAME]: token });
  }  
  return req;
});

3.1.3 自定义登录态过期的容错处理

当自定义登录态过期的时候,后端需要返回特定的状态码,例如:AUTH_EXPIREDAUTH_INVALID 等。

前端可以在「网络请求层」去监听所有请求的这个状态码,然后发起刷新登录态,再去重放失败的请求:

// 添加响应拦截器fly.interceptors.response.use(    (response) => {      const code = res.data;        
      // 登录态过期或失效
      if ( ['AUTH_EXPIRED', 'AUTH_INVALID'].includes(code) ) {      
        // 刷新登录态
        await session.refreshLogin();        
        // 然后重新发起请求
        return fly.request(request);
      }
    }
)

那么如果并发的发起多个请求,都返回了登录态失效的状态码,上述代码就会被执行多次。

我们需要对 session.refreshLogin() 做一些特殊的容错处理:

  1. 请求锁:同一时间,只允许一个正在过程中的网络请求。
  2. 等待队列:请求被锁定之后,调用该方法的所有调用,都推入一个队列中,等待网络请求完成之后共用返回结果。
  3. 熔断机制:如果短时间内多次调用,则停止响应一段时间,类似于 TCP 慢启动。

示例代码:

class Session {  // ....
  
  // 刷新登录保险丝,最多重复 3 次,然后熔断,5s 后恢复
  refreshLoginFuseLine = REFRESH_LOGIN_FUSELINE_DEFAULT;
  refreshLoginFuseLocked = false;
  refreshLoginFuseRestoreTime = 5000;  // 熔断控制
  refreshLoginFuse(): Promise {    if (this.refreshLoginFuseLocked) {      return Promise.reject('刷新登录-保险丝已熔断,请稍后');
    }    if (this.refreshLoginFuseLine > 0) {      this.refreshLoginFuseLine = this.refreshLoginFuseLine - 1;      return Promise.resolve();
    } else {      this.refreshLoginFuseLocked = true;      setTimeout(() => {        this.refreshLoginFuseLocked = false;        this.refreshLoginFuseLine = REFRESH_LOGIN_FUSELINE_DEFAULT;
        logger.info('刷新登录-保险丝熔断解除');
      }, this.refreshLoginFuseRestoreTime);      return Promise.reject('刷新登录-保险丝熔断!!');
    }
  }  // 并发回调队列
  refreshLoginQueueMaxLength = 100;
  refreshLoginQueue: any[] = [];
  refreshLoginLocked = false;  // 刷新登录态
  refreshLogin(): Promise {    return Promise.resolve()    
      // 回调队列 + 熔断 控制
      .then(() => this.refreshLoginFuse())
      .then(() => {        if (this.refreshLoginLocked) {          const maxLength = this.refreshLoginQueueMaxLength;          if (this.refreshLoginQueue.length >= maxLength) {            return Promise.reject(`refreshLoginQueue 超出容量:${maxLength}`);
          }          return new Promise((resolve, reject) => {            this.refreshLoginQueue.push([resolve, reject]);
          });
        }        this.refreshLoginLocked = true;
      })      // 通过前置控制之后,发起登录过程
      .then(() => {        this.clearSession();
        wx.showLoading({ title: '刷新登录态中', mask: true });        return this.login()
          .then(() => {
            wx.hideLoading();
            wx.showToast({ icon: 'none', title: '登录成功' });            this.refreshLoginQueue.forEach(([resolve]) => resolve());            this.refreshLoginLocked = false;
          })
          .catch(err => {
            wx.hideLoading();
            wx.showToast({ icon: 'none', title: '登录失败' });            this.refreshLoginQueue.forEach(([, reject]) => reject());            this.refreshLoginLocked = false;            throw err;
          });
      });  // ...}

3.1.4 微信 session_key 过期的容错处理

我们从上面的「静默登录」之后,微信服务器端会下发一个 session_key 给后端,而这个会在需要获取微信开放数据的时候会用到。

session_key 是有时效性的,以下摘自微信官方描述:

会话密钥 session_key 有效性

开发者如果遇到因为 session_key 不正确而校验签名失败或解密失败,请关注下面几个与 session_key 有关的注意事项。

  1. wx.login 调用时,用户的 session_key 可能会被更新而致使旧 session_key 失效(刷新机制存在最短周期,如果同一个用户短时间内多次调用 wx.login,并非每次调用都导致 session_key 刷新)。开发者应该在明确需要重新登录时才调用 wx.login,及时通过 auth.code2Session 接口更新服务器存储的 session_key。
  2. 微信不会把 session_key 的有效期告知开发者。我们会根据用户使用小程序的行为对 session_key 进行续期。用户越频繁使用小程序,session_key 有效期越长。
  3. 开发者在 session_key 失效时,可以通过重新执行登录流程获取有效的 session_key。使用接口 wx.checkSession可以校验 session_key 是否有效,从而避免小程序反复执行登录流程。
  4. 当开发者在实现自定义登录态时,可以考虑以 session_key 有效期作为自身登录态有效期,也可以实现自定义的时效性策略。

翻译成简单的两句话:

  1. session_key 时效性由微信控制,开发者不可预测。
  2. wx.login 可能会导致 session_key 过期,可以在使用接口之前用 wx.checkSession 检查一下。

而对于第二点,我们通过实验发现,偶发性的在 session_key 已过期的情况下,wx.checkSession 会概率性返回 true

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

  • 小程序解密手机号,隔一小段时间后,checksession:ok,但是解密失败
  • wx.checkSession有效,但是解密数据失败
  • checkSession判断session_key未失效,但是解密手机号失败

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

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

  1. 发起需要使用 session_key 的请求前,做一次 wx.checkSession 操作,如果失败了刷新登录态。
  2. 后端使用 session_key 解密开放数据失败之后,返回特定错误码(如:DECRYPT_WX_OPEN_DATA_FAIL),前端刷新登录态。

示例代码:

// 定义检查 session_key 有效性的操作const ensureSessiOnKey= async () => {  const hasSession = await new Promise(resolve => {
    wx.checkSession({      success: () => resolve(true),      fail: () => resolve(false),
    });
  });  
  if (!hasSession) {
    logger.info('sessionKey 已过期,刷新登录态');    // 接上面提到的刷新登录逻辑
    return session.refreshLogin();
  }  return Promise.resolve();
}// 在发起请求的时候,先做一次确保 session_key 最新的操作(以 fly.js 作为网络请求层为例)const updatePhOne= async (params) => {  await ensureSessionKey();  const res = await fly.post('https://xxx', params);
}// 添加响应拦截器, 监听网络请求返回fly.interceptors.response.use(    (response) => {      const code = res.data;        
      // 登录态过期或失效
      if ( ['DECRYPT_WX_OPEN_DATA_FAIL'].includes(code)) {        // 刷新登录态
        await session.refreshLogin();        
        // 由于加密场景的加密数据由用户点击产生,session_key 可能已经更改,需要用户重新点击一遍。
        wx.showToast({ title: '网络出小差了,请稍后重试', icon: 'none' });
      }
    }
)

3.2 授权的实现

3.2.1 组件拆分与设计

在用户信息和手机号获取的方式上,微信是以

推荐阅读
author-avatar
日月星辰淡定鍀好男孩_933
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有