对于登录/注册的设计如此精雕细琢的目的,当然是想让这个作为应用的基础能力,有足够的健壮性,避免出现全站性的阻塞。
同时要充分考虑如何解耦和封装,在开展新的小程序的时候,能更快的去复用能力,避免重复采坑。
登录注册这模块,就像个冰山,我们以为它就是「输入账号密码,就完成登录了」,但实际下面还有各种需要考虑的问题。
基于状态机,我们就可以编写这样的代码:
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; });
当自定义登录态过期的时候,后端需要返回特定的状态码,例如:AUTH_EXPIRED
、 AUTH_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()
做一些特殊的容错处理:
示例代码:
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; }); }); // ...}
我们从上面的「静默登录」之后,微信服务器端会下发一个 session_key
给后端,而这个会在需要获取微信开放数据的时候会用到。
而 session_key
是有时效性的,以下摘自微信官方描述:
会话密钥 session_key 有效性
开发者如果遇到因为 session_key 不正确而校验签名失败或解密失败,请关注下面几个与 session_key 有关的注意事项。
- wx.login 调用时,用户的 session_key 可能会被更新而致使旧 session_key 失效(刷新机制存在最短周期,如果同一个用户短时间内多次调用 wx.login,并非每次调用都导致 session_key 刷新)。开发者应该在明确需要重新登录时才调用 wx.login,及时通过 auth.code2Session 接口更新服务器存储的 session_key。
- 微信不会把 session_key 的有效期告知开发者。我们会根据用户使用小程序的行为对 session_key 进行续期。用户越频繁使用小程序,session_key 有效期越长。
- 开发者在 session_key 失效时,可以通过重新执行登录流程获取有效的 session_key。使用接口 wx.checkSession可以校验 session_key 是否有效,从而避免小程序反复执行登录流程。
- 当开发者在实现自定义登录态时,可以考虑以 session_key 有效期作为自身登录态有效期,也可以实现自定义的时效性策略。
翻译成简单的两句话:
session_key
时效性由微信控制,开发者不可预测。wx.login
可能会导致 session_key
过期,可以在使用接口之前用 wx.checkSession
检查一下。而对于第二点,我们通过实验发现,偶发性的在 session_key
已过期的情况下,wx.checkSession
会概率性返回 true
社区也有相关的反馈未得到解决:
所以结论是:wx.checkSession
可靠性是不达 100% 的。
基于以上,我们需要对 session_key
的过期做一些容错处理:
session_key
的请求前,做一次 wx.checkSession
操作,如果失败了刷新登录态。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' }); } } )
在用户信息和手机号获取的方式上,微信是以 的方式,让用户主动点击授权的。
那么为了让代码更解耦,我们设计这样三个组件:
: 包装点击交互,通过
支持点击区域的自定义UI。
: 与
同理。
: 根据业务需要,组合
、
组合来定义不同的授权流程。以开头的业务场景的流程为例,它有这样的要求:
那么授权的阶段可以分三层:
// 用户登录的阶段export enum AuthStep { // 阶段一:只有登录态,没有用户信息,没有手机号 OnE= 1, // 阶段二:有用户信息,没有手机号 TWO = 2, // 阶段三:有用户信息,有手机号 THREE = 3, }
AuthStep
的推进过程是不可逆的,我们可以定义一个 nextStep
函数来封装 AuthStep 更新的逻辑。外部使用的话,只要无脑调用 nextStep
方法,等待回调结果就行。
示例伪代码:
// auth-flow componentComponent({ // ... data: { // 默认情况下,只需要到达阶段二。 mustAuthStep: AuthStep.TWO }, // 允许临时更改组件的需要达到的阶段。 setMustAuthStep(mustAuthStep: AuthStep) { this.setData({ mustAuthStep }); }, // 根据用户当前的信息,计算用户处在授权的阶段 getAuthStep() { let currAuthStep; // 没有用户信息,尚在第一步 if (!session.hasUser() || !session.hasUnionId()) { currAuthStep = AuthStepType.ONE; } // 没有手机号,尚在第二步 if (!session.hasPhone()) { currAuthStep = AuthStepType.TWO; } // 都有,尚在第三步 currAuthStep = AuthStepType.THREE; return currAuthStep; } // 发起下一步授权,如果都已经完成,就直接返回成功。 nextStep(e) { const { mustAuthStep } = this.data; const currAuthStep = this.updateAuthStep(); // 已完成授权 if (currAuthStep >= mustAuthStep || currAuthStep === AuthStepType.THREE) { // 更新全局的授权状态机,广播消息给订阅者。 return getApp().status.auth.success(); } // 第一步:更新用户信息 if (currAuthStep === AuthStepType.ONE) { // 已有密文信息,更新用户信息 if (e) session.updateUser(e); // 更新到视图层,展示对应UI,等待获取用户信息 else this.setData({ currAuthStep }); return; } // 第二步:更新手机信息 if (currAuthStep === AuthStepType.TWO) { // 已有密文信息,更新手机号 if (e) this.bindPhone(e); // 未有密文信息,弹出获取窗口 else this.setData({ currAuthStep }); return; } console.warn('auth.nextStep 错误', { currAuthStep, mustAuthStep }); }, // ...});
那么我们的
中就可以根据 currAuthStep
和 mustAuthStep
来去做不同的 UI 展示。需要注意的是使用
、
的时候连接上 nextStep(e)
函数。
示例伪代码:
已完成授权 授权用户信息 授权手机号
到这里,我们制作好了用来承载授权流程的组件
,那么接下来就是决定要使用它的时机了。
我们梳理需要授权的场景:
点击某个按钮,例如:购买某个商品。
对于这种场景,常见的是通过弹窗完成授权,用户可以选择关闭。
那么我们定义一个枚举变量:
// 授权的展示形式export enum AuthDisplayMode { // 以弹窗形式 POPUP = 'button', // 以页面形式 PAGE = 'page', }
我们可以设计一个 mustAuth
方法,在点击某个按钮,或者页面加载的时候,进行授权控制。
伪代码示例:
class Session { // ... mustAuth({ mustAuthStep = AuthStepType.TWO, // 需要授权的LEVEL,默认需要获取用户资料 popupCompName = 'auth-popup', // 授权弹窗组件的 id mode = AuthDisplayMode.POPUP, // 默认以弹窗模式 } = {}): Promise{ // 如果当前的授权步骤已经达标,则返回成功 if (this.currentAuthStep() >= mustAuthStep) return Promise.resolve(); // 尝试获取当前页面的 组件实例 const pages = getCurrentPages(); const curPage = pages[pages.length - 1]; const popupComp = curPage.selectComponent(`#${popupCompName}`); // 组件不存在或者显示指定页面,跳转到授权页面 if (!popupComp || mode === AuthDisplayMode.PAGE) { const curRoute = curPage.route; // 跳转到授权页面,带上当前页面路由,授权完成之后,回到当前页面。 wx.redirectTo({ url: `authPage?backTo=${encodeURIComponent(curRoute)}` }); return Promise.resolve(); } // 设置授权 LEVEL,然后调用 的 nextStep 方法,进行进一步的授权。 popupComp.setMustAuthStep(mustAuthStep); popupComp.nextStep(); // 等待成功回调或者失败回调 return new Promise((resolve, reject) => { const authStatus = getApp().status.auth; authStatus.onceSuccess(resolve); authStatus.onceFail(reject); }); } // ...}
那么我们就能在按钮点击,或者页面加载的时候进行授权拦截:
Page({ onLoad() { session.mustAuth().then(() => { // 开始初始化页面... }); } onClick(e) { session.mustAuth().then(() => { // 开始处理回调逻辑... }); } })
当然,如果项目使用了 TS 的话,或者支持 ES7 Decorator 特性的话,我们可以为 mustAuth
提供一个装饰器版本:
export function mustAuth(option = {}) { return function( _target, _propertyName, descriptor, ) { // 劫持目标方法 const method = descriptor.value; // 重写目标方法 descriptor.value = function(...args: any[]) { return session.mustAuth(option).then(() => { // 登录完成之后,重放原来方法 if (method) return method.apply(this, args); }); }; }; }
那么使用方式就简单一些了:
Page({ @mustAuth(); onLoad() { // 开始初始化页面... } @mustAuth(); onClick(e) { // 开始处理回调逻辑... } });
作为一套可复用的小程序登录方案,当然需要去定义好前后端的交互协议。
那么整套登录流程下来,需要的接口有这么几个:
由「登录服务」和「底层建设」组合提供的通用服务,业务层只需要去根据产品需求,定制授权的流程
,就能满足大部分场景了。
本篇文章通过一些常见的登录授权场景来展开来描述细节点。
整理了「登录」、「授权」的概念。
然后分别针对「登录」介绍了一些关键的技术实现:
session_key
过期的容错处理而对于「授权」,会有设计UI部分的逻辑,还需要涉及到组件的拆分:
然后,梳理了这套登录授权方案所依赖的后端接口,和给出最简单的参考协议。
最后,站在「秉着沉淀一套通用的小程序登录方案和服务为目标」的角度,梳理了一下架构层面上的分层。
相关免费学习推荐:微信小程序开发
以上就是了解微信小程序登录的前端设计与实现的详细内容,更多请关注其它相关文章!