小程序 Page 获取登录态,异步满天飞?
登录逻辑与登录态
根据小程序文档与官方 Demo 的例子,微信登录逻辑在 app.js 的 App.onLaunch 中实现。另外还需要自行与第三方服务器独立进行一套授权机制,这里我使用了 JWT,在换取 openid 的接口中返回。
整个 app.js 的登录逻辑大致如下,进入小程序后检查微信登录态是否过期,过期了重新调起微信登录并换取新的 JWT 作为与第三方服务器通信的凭证;未过期则从 localStorage 中拉取 token 与用户信息。
App({
onLaunch: function () {
const that = this;
// 检查用户登录态是否过期,过期了重新登录
wx.checkSession({
success: () => {
//session 未过期,并且在本生命周期一直有效
that.globalData.token = wx.getStorageSync('token');
that.globalData.userInfo = wx.getStorageSync('userInfo');
},
fail: () => {
that.initLoginState();
}
});
},
globalData: {
token: '',
userInfo: null
},
// 重新登录
initLoginState: function () {
const that = this;
// 登录
wx.login({
success: res => {
// 调用获取用户信息接口
wx.getUserInfo({
success: fullUserInfo => {
// 发送 res.code 到后台换取 openId, sessionKey, unionId
wx.request({
url: `${host}/api/auth/loginwx`,
method: 'POST',
data: {
code: res.code,
userInfo: fullUserInfo
},
success: ({data}) => {
wx.setStorageSync('token', data.data.token);
wx.setStorageSync('userInfo', data.data.userInfo);
that.globalData.token = data.data.token;
that.globalData.userInfo = data.data.userInfo;
}
});
}
});
}
});
}
});
业务页面获取数据
在 Page 中,不可避免地需要调用第三方服务器的接口获取数据,此时需要使用 JWT 进行授权。当进入页面时就需要拉数据时,请求放在了 Page.onLoad 方法中。
const app = getApp();
const host = require('../../config').host;
Page({
data: {
groups: []
},
onLoad: function () {
this.listGroups();
},
// 获取订单团信息
listGroups: function () {
wx.request({
url: `${host}/api/group/list`,
header: {
'authorization': app.globalData.token
},
success: ({data}) => {
this.setData({
groups: data.data
})
}
});
}
})
测试时却发现有时候页面有数据,有时候页面无数据。进一步查看请求便发现有时 token 有值,有时 token 拿不到值。
全特么是异步
仔细想想就能够明白,原因在于 App.globalData.token 的设置是异步,虽然 App.onLaunch 与 Page.onLoad 有明确的时序性(文档上没有说明,通过测试可发现 Page.onLoad 总在 App.onLaunch 后执行),但「设置 token」与「请求业务接口」两个步骤不能保证其时序性,因此会出现偶尔请求失败的情况。
既然无法保证时序,第一个想法就是 EventBus 了,这种很 free 的东西,用起来功能强大但也很危险。
给 App 添加了一个自己实现的简单全局 EventBus ,并在 checkSession.success 和重新登录设置好 token 后都广播一个登陆成功事件。这时候 Page 就变成这样了。
loggingSubs: null,
onLoad: function () {
if (app.globalData.token) {
this.listGroups();
} else {
this.loggingSubs = app.eventBus.on('LOGGING-SUCCESS', this.listGroups.bind(this));
}
},
onUnload: function () {
this.loggingSubs.off();
},
由于不能保证时序性,如果在 onLoad 方法只订阅事件就会出现:
- 登录成功了,广播登陆成功事件(没有人订阅,消息被丢弃)
- 执行
Page.onLoad方法,订阅事件
结果没有调用业务接口,因此需要处理两种情况。
其实 RxJS 提供了
ReplaySubject这样的Observable来使后订阅的观察者也能收到之前的所有通知,自行实现一个ReplaySubject就能去掉这种判断,详见 #12
每个页面都要这样?
如果每个业务 Page 都需要在初始化时调用业务接口,都需要写一套这样的逻辑?有没有什么更好的解决办法?
更甚之,就算是一些事件绑定,理论上也不能保证事件调用时已经完成登录,难道每次调用业务接口都要写一套这样的判断?
这个问题的出现根本在于登录态的获取、token 的设置是异步的,而这个异步与业务接口调用需要同步(必须先有 token 才能调用),能否把这个过程通过小程序框架固定成同步?如 Page.onLoad 方法的调用必须在 App 的某个钩子之后?
如果有更好的解决方法,请不吝赐教,谢谢。
都是异步惹的祸,看了几个开源项目,大体方案有三种: 1,用的eventbus,2,通过mixin,引入login到页面,3, mixin 一个封装的http。我的方案更简单,没有用mixin, 觉得侵入性太强了。
项目的架构,wepy+redux+saga, 我的所有请求都会到saga,后台请求都通过一个backend.js(service 方法),加一个sync的方法在这里就行.
"Talk is cheap. Show me the code"

@kala888
大致明白了,但在 session 过期时候的并发 request 会不会导致多次请求 wx.login 从而导致问题呢?
比如某个 Page 里有两个 request 请求,由于 session 过期都调用了 wx.login 和我们自己服务器换 openid 和 session_key 的接口(图片的 35 行),但这两个请求返回顺序不一致了,比如 1、2 的发送顺序,回调却是 2、1,那这样服务端记录的是 2 请求的 session_key,而前端却把 1 的 session 写到 storage 里了。
@kala888 ,我想问一下wepy中怎么使用saga,我在saga中的函数都没有生效但是也不会报错
@kala888 这样子并不能解决问题,正如@ryancui- 的问题一样,说下我的解决方案: 对服务器返回结果进行判断,如果服务器告诉你token失效了,这个时候保存所有请求然后登陆,登陆完成后再多保存的请求进行再次请求就没问题了。
import wx from './wx.js'
import { hideWxLoading, showModal, getStorageSyncLoginResult } from './index'
const makeOptions = (url, options) => {
const defaultoptions = {
url: undefined,
method: 'GET',
qs: undefined,
body: undefined,
headers: undefined,
type: 'json',
contentType: 'application/json',
crossOrigin: true,
credentials: undefined,
customToken: false,
showFailMsg: true,
}
let thisoptions = {}
if (!options) {
thisoptions = { url }
} else {
thisoptions = options
if (url) {
thisoptions.url = url
}
}
thisoptions = Object.assign({}, defaultoptions, thisoptions)
return thisoptions
}
const addQs = (url, qs) => {
let queryString = ''
let newUrl = url
if (qs && typeof qs === 'object') {
/* eslint no-restricted-syntax: 0 */
for (const k of Object.keys(qs)) {
queryString += `&${k}=${qs[k]}`
}
if (queryString.length > 0) {
if (url.split('?').length < 2) {
queryString = queryString.substring(1)
} else if (url.split('?')[1].length === 0) {
queryString = queryString.substring(1)
}
}
if (url.indexOf('?') === -1) {
newUrl = `${url}?${queryString}`
} else {
newUrl = `${url}${queryString}`
}
}
return newUrl
}
let isRefreshing = false
/*存储请求的数组*/
let refreshSubscribers = []
/*将所有的请求都push到数组中,其实数组是[function(token){}, function(token){},...]*/
function subscribeTokenRefresh(cb) {
refreshSubscribers.push(cb);
}
/*数组中的请求得到新的token之后自执行,用新的token去请求数据*/
function onRrefreshed() {
console.log(refreshSubscribers)
refreshSubscribers.map(cb => cb());
}
const request = (url, options) => {
const opts = makeOptions(url, options)
const { method, body, headers, qs, type, contentType } = opts
let requestUrl = opts.url
if (qs) requestUrl = addQs(requestUrl, qs)
let header = headers
if ((!headers || !headers['content-type']) && contentType) {
header = Object.assign({}, headers, { 'content-type': contentType })
}
if (opts.customToken) {
const res = getStorageSyncLoginResult()
header = {
...header,
'X-Custom-Token': res && res.token
}
}
return new Promise((resolve, reject) => {
wx.request({
url: requestUrl,
method,
data: body,
header,
dataType: type
})
.then(response => {
// getApp().log(JSON.stringify(response));
// 业务数据异常
if (response.statusCode < 200 || response.statusCode >= 300 || (response.data.code !== 0 && (response.data.code < 200 || response.data.code >= 300))) {
let errors = {
error: -1,
request: url,
errorMessage: '系统异常,请查看response',
response
}
if (response.data && typeof response.data === 'object') {
errors = Object.assign({}, errors, response.data)
}
if (response.data.code === 401) {
subscribeTokenRefresh(() => {
resolve(request(url, options))
})
if (!isRefreshing) {
isRefreshing = true
//showModal('登录已过期,请重新尝试')
wx.app.login().then(async userInfo => {
if (userInfo) {
try {
isRefreshing = false
onRrefreshed()
} catch (e) {
reject(e)
}
} else {
reject()
}
}).catch(e => {
console.log(222, e)
reject(e)
})
}
} else {
opts.showFailMsg && showModal(errors.detail || '数据加载失败')
hideWxLoading()
reject(errors)
}
} else {
// 正确返回
resolve(response.data)
}
})
.catch(err => {
// getApp().log(JSON.stringify(err));
reject({
error: -1,
message: '系统异常,请查看response',
err,
request: url
})
})
})
}
export default request
@ldwonday 想请问下用这个保存请求的方法, 页面 page 中有个 A 方法,请求后,服务器响应未登录。保存 A 请求,重新登录后 A 也重新执行了。 但是无法在 page 中获取到 A 重新执行返回的数据。这个要怎么解决 ?
@jingchaocheng 多输出些日志看看吧!