带上对上面答案的认知,我们开始说小程序页面 Page 方面的逻辑。分以下几点:
由于在 App.onLaunch() 中,用户登录态信息是异步的方式请求后台接口的,接口返回登录态信息并赋值给全局变量 app.gData 前,很大可能小程序页面已经执行完了 onLoad() 方法,这样直接对我们页面里后面的写逻辑造成了致命的错误(页面中获取到的登录态信息是错误的)。
还有就是用户授权方面的问题。对于那些业务逻辑要求必须有用户基本信息的页面,我们得在页面初始化时验证用户授权状态(在登录的时候我们为这一步做过准备),若未曾询问过或者用户拒绝授权,我们同意跳转到 /pages/auth/auth 页面进行用户授权步骤,同意后返回上一页并做相应的更新。
我们很容易会想到,上面的这两点都是多页面中调用到的,必然会考虑到灵活封装好,之后每个页面调用即可。
在 app.js 中先预定义全局控制字段,包括登录控制字段 logined, 授权列表 authsetting,以及用户信息(包含token,就是登录态信息):
// app.js
'gData': {
'logined': false, //用户是否登录
'authsetting': null, //用户授权结果
'userinfo': null, //用户信息(包含自定义登录态token)
},
针对上面的第一点,我们在 app.js 下封装 pageGetLoginInfo() 方法,该方法主要做的事情有一下几点:
判断登录控制字段 app.gData.logined,若已经登录——控制字段值为 true,直接把全局控制字段赋值给页面的控制字段;
若全局登录状态控制字段值为 false,则我们完全可认为是由于异步请求后台的原因导致的全局登录控制字段未赋值(因为上文提到登录失败都可以认为是系统的一个Bug)。所以若为 false, 则在 app 对象中定义一个新函数 loginedCb(),供 app.onLaunch() 中异步获取到登录态信息后回调(在本文第四点有特意提过)。而 loginedCb() 方法要做的也是把全局控制字段赋值给页面的控制字段;
代码中出现的 wxsetData() 方法是在 /utils/wxapi.js 定义的,这里我们导入进来
// app.js
import {
wxsetData
} from './utils/wxapi.js';
/**
* 获取小程序注册时返回的自定义登录态信息(小程序页面中调用)
* 主要是解决pageObj.onLoad 之后app.onLaunch()才返回数据的问题
*/
pageGetLoginInfo: function(pageObj) {
var _this = this;
return new Promise((resolve, reject) => {
// console.log(_this.gData.logined);
if (_this.gData.logined == true) {
wxsetData(pageObj, {
'logined': _this.gData.logined,
'authsetting': _this.gData.authsetting,
'userinfo': _this.gData.userinfo
}).then(function(data) {
//执行pageObj.onShow的回调方法
(pageObj.authorizedCb && typeof(pageObj.authorizedCb) === 'function') && pageObj.authorizedCb(data);
resolve(data);
});
} else {
/**
* 小程序注册时,登录并发起网络请求,请求可能会在 pageObj.onLoad 之后才返回数据
* 这里加入loginedCb回调函数来预防,回调方法会在接收到请求后台返回的数据后执行,详看app.onLaunch()
*/
_this.loginedCb = () => {
wxsetData(pageObj, {
'logined': _this.gData.logined,
'authsetting': _this.gData.authsetting,
'userinfo': _this.gData.userinfo
}).then(function(data) {
//执行pageObj.onShow的回调方法
(pageObj.authorizedCb && typeof(pageObj.authorizedCb) === 'function') && pageObj.authorizedCb(data);
resolve(data);
});
}
}
});
},
然后我们再封装一个 pageOnLoadInit() 方法,也简单说说方法的逻辑:
调用上一步 pageGetLoginInfo() 方法,保证页面拿到有效准确的登录态信息;
验证登录,同时通过参数来决定当前页面初始化时是否需要校验用户授权;
若用户没有授权,则从当前页面跳转到 /pages/auth/auth 页面,auth 页面就是一个授权按钮,用户点击后弹窗提示用户确认授权(小程序官方已修改只能通过点击按钮弹窗用户授权);
代码中若涉及到授权方面的我们放在后面讨论:
// app.js
/**
* 封装小程序页面的公共方法
* 在小程序页面onLoad里调用
* @param {Object} pageObj 小程序页面对象Page
* @param {Boolean} needAuth 是否检验用户授权(scope.userInfo)
* @return {Object} 返回Promise对象,resolve方法执行验证登录成功后且不检验授权(特指scope.userInfo)的回调函数,reject方法是验证登录失败后的回调
*/
pageOnLoadInit: function(pageObj, needAuth = false) {
var _this = this;
return new Promise((resolve, reject) => {
_this.pageGetLoginInfo(pageObj).then(function(res) {
// console.log(_this.gData.logined);
if (res.logined === true) {
//登录成功、无需授权
resolve(res);
if (needAuth) {
if (res.authsetting['scope.userInfo'] === false || typeof res.authsetting['scope.userInfo'] === 'undefined') {
common.navigateTo('/pages/auth/auth');
}
}
} else {
reject({
'errMsg': 'Fail to login.Please feedback to manager.'
});
}
});
});
},
现在问题基本解决了,剩下的就是在每个小程序页面中调用,只校验登录的逻辑在 Page.onLoad() 里面执行,下面以代码写在小程序页面 /pages/mine/index/index.js 中为例:
// /pages/mine/index/index.js
const app = getApp();
/**
* 生命周期函数--监听页面加载
*/
onLoad: function(options) {
var _this = this;
app.pageOnLoadInit(this).then(function(res) {
//这里写验证登录成功后且无需验证授权 需要执行的逻辑
//若还需验证授权成功才执行的逻辑需写在onShow方法里面,并且这里pageOnLoadInit()第二个参数要为 true
}, function(error) {
//登录失败
wx.showModal({
title: 'Error',
content: error.errMsg ? error.errMsg : 'Fail to login.Please feedback to manager.',
})
return false;
});
},
到此,针对第一点——页面登录已经完成。
针对第二点用户授权的。先看看在 app.js 中封装的 exeAuth() 方法,该方法就是统一授权与后台接口的交互
/**
* [exeAuth 执行用户授权流程]
* @param {[string]} loginKey 自定义登录态信息缓存的key
* @param {[Object]} data wx.getUserInfo接口返回的数据结构一致
* @return {[Promise]} 返回一个Promise对象
*/
exeAuth: function(loginKey, data) {
var _this = this;
return new Promise((resolve, reject) => {
wxapi('request', {
'method': 'POST',
'url': _this.gData.api.request + '/api/User/thirdauth',
'header': {
'Content-type': 'application/x-www-form-urlencoded',
},
'data': {
'platform': 'miniwechat',
'token': _this.gData.userinfo.token,
'encryptedData': data.encryptedData,
'iv': data.iv,
}
}).then(function(res) {
//当服务器内部错误500(或者其它目前我未知的情况)时,wx.request还是会执行success回调,所以这里还增加一层服务器返回的状态码的判断
if (res.statusCode === 200 && res.data.code === 1) {
//更新app.gData中的数据
_this.gData.authsetting['scope.userInfo'] = true;
_this.gData.userinfo = res.data.data.userinfo;
//更新自定义登录态的缓存数据,防止再次进入小程序时读取到旧的缓存数据,这里让它异步执行即可,
//倘若异步执行的结果失败,直接清除自定义登录态缓存,再次进入小程序时系统会自动重新登录生成新的
wxapi('setStorage', {
'key': loginKey,
'data': res.data.data.userinfo
}).catch(function(error) {
console.warn(error.errMsg);
wxapi('removeStorage', {
'key': loginKey
});
});
resolve(res.data.data.userinfo);
} else {
return Promise.reject({
'errMsg': res.data.msg ? 'ServerApi error:' + res.data.msg : 'Fail to network request!'
});
}
}).catch(function(error) {
reject(error);
});
});
},
要调用上述授权方法的地方必不可少的就是 /pages/auth/auth 统一授权页面了,对于其它可能用到的地方我们之后也可直接调用,我们来看看 /pages/auth/auth 的 bindGetUserinfo() type = getUserInfo 按钮的回调函数:
/**
* getUserinfo回调函数
*/
bindGetUserinfo: function(e) {
var data = e.detail;
if (data.errMsg === "getUserInfo:ok") {
app.exeAuth('loginInfo', data).then(function(res) {
var pages = getCurrentPages();
var prevPage = pages[pages.length - 2]; //上一个页面
prevPage.setData({
'userinfo': res,
'authsetting.scope\\.userInfo': true 这里请注意反斜杠转义,'scope.userInfo'被看做一个完整的键名
}, function() {
wx.navigateBack({
delta: 1
});
});
}).catch(function(error) {
console.error(error);
wx.showModal({
title: 'Error',
content: error.errMsg,
})
});
} else {
wx.showModal({
title: 'Warning',
content: 'Please permit to authorize.',
showCancel: false
})
return false;
}
},
最后像上面登录一样,在 app.js 里封装一个 pageOnShowInit() 供需要授权的页面调用:
/**
* 封装小程序页面的公共方法
* 在小程序页面onShow里调用
* @param {Object} pageObj 小程序页面对象Page
* @return {Object} 返回Promise对象,resolve方法执行验证授权(特指scope.userInfo)成功后的回调函数,reject方法是验证授权失败后的回调
*/
pageOnShowInit: function(pageObj) {
var _this = this;
return new Promise((resolve, reject) => {
/**
* 这里通过pageObj.data.authsetting == (null || undefined)
* 来区分pageObj.onLoad方法中是否已经执行完成设置页面授权列表(pageObj.data.authsetting)的方法,
*
* 因为如果已经执行完成设置页面授权列表(pageObj.data.authsetting)的方法,并且获取到的授权列表为空的话,会把pageObj.data.authsetting赋值为
* 空对象 pageObj.data.authsetting = {} ,所以pageObj.data.authsetting倘若要初始化时,请务必初始化为 null ,不能初始化为 {},切记!
*/
if (pageObj.data.authsetting === null || typeof pageObj.data.authsetting === 'undefined') {
/**
* pageObj.onLoad是异步获取用户授权信息的,很大可能会在 pageObj.onShow 之后才返回数据
* 这里加入authorizedCb回调函数预防,回调方法会在pageObj.onLoad拿到用户授权状态列表后调用,详看app.pageOnLoadInit()
*/
pageObj.authorizedCb = (res) => {
if (res.authsetting['scope.userInfo'] === true) {
//授权成功执行resolve
resolve();
} else {
reject();
}
}
} else {
if (res.authsetting['scope.userInfo'] === true) {
//授权成功执行resolve
resolve();
} else {
reject();
}
}
});
},
由于授权多数情况下是从授权页面跳转回来的,所以这个方法设计在小程序页面的 Page.onShow() 中调用,具体调用这里不贴代码了,类似校验登录一样。