Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

小程序 Page 获取登录态,异步满天飞? #11

Open
ryancui92 opened this issue Nov 9, 2017 · 6 comments
Open

小程序 Page 获取登录态,异步满天飞? #11

ryancui92 opened this issue Nov 9, 2017 · 6 comments

Comments

@ryancui92
Copy link
Owner

ryancui92 commented Nov 9, 2017

登录逻辑与登录态

根据小程序文档与官方 Demo 的例子,微信登录逻辑在 app.jsApp.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.onLaunchPage.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 的某个钩子之后?

如果有更好的解决方法,请不吝赐教,谢谢。

@kala888
Copy link

kala888 commented Apr 4, 2018

都是异步惹的祸,看了几个开源项目,大体方案有三种: 1,用的eventbus,2,通过mixin,引入login到页面,3, mixin 一个封装的http。我的方案更简单,没有用mixin, 觉得侵入性太强了。
项目的架构,wepy+redux+saga, 我的所有请求都会到saga,后台请求都通过一个backend.js(service 方法),加一个sync的方法在这里就行.
"Talk is cheap. Show me the code"
image

@ryancui92
Copy link
Owner Author

@kala888

大致明白了,但在 session 过期时候的并发 request 会不会导致多次请求 wx.login 从而导致问题呢?

比如某个 Page 里有两个 request 请求,由于 session 过期都调用了 wx.login 和我们自己服务器换 openid 和 session_key 的接口(图片的 35 行),但这两个请求返回顺序不一致了,比如 1、2 的发送顺序,回调却是 2、1,那这样服务端记录的是 2 请求的 session_key,而前端却把 1 的 session 写到 storage 里了。

@MichelleChe
Copy link

@kala888 ,我想问一下wepy中怎么使用saga,我在saga中的函数都没有生效但是也不会报错

@ldwonday
Copy link

ldwonday commented Sep 6, 2018

@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

@jingchaocheng
Copy link

@ldwonday 想请问下用这个保存请求的方法, 页面 page 中有个 A 方法,请求后,服务器响应未登录。保存 A 请求,重新登录后 A 也重新执行了。 但是无法在 page 中获取到 A 重新执行返回的数据。这个要怎么解决 ?

@ldwonday
Copy link

@jingchaocheng 多输出些日志看看吧!

@ryancui92 ryancui92 removed the 前端 label Jan 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants