1、登录系统设计
我们要有 1个认知,登录系统设计是一个很大的问题。
从业务上来讲,涉及几个方面
- 用户的账户类型,如,手机用户,证件用户(身份证,军官证等),其他社交帐号登录,用户有分vip等级
- 自家app登录交互,如免登,强登录,密码登录,otp登录,微信帐号登录,支付宝帐号登录,验证后,说不定还有指纹、人脸……
- 第3方app登录,如开发页面投放到微信,我们是否可以打通微信免登
- ……
是不是很多,我还没说技术方面,从技术架构上来说,稍微大或者成体系的公司,登录系统有专门的平台团队研发和维护,涉及很多工种,前端、后端、ios、安卓,RN……
作为业务前端开发,面对这1个庞大体系,我们可能从技术上来说,没有亲身亲历过,掌握得肯定是不那么透彻。但是从业务场景上,我们是实实在在的使用者(接入方),需要掌握,如怎么在微信免登、怎么接入人脸登录等,了解每个场景下的登录负责人,遇到登录问题,及时联系对应负责人,协助帮忙看问题。
有兴趣,也可以看看知乎上这个问题大型网站的用户登录系统是如何设计的?
在技术上来讲,也不是一点技术含量没有?业务方前端开发,也是需要进行二次封装的。
2、业务方登录
我目前接触移动端的项目有 csr,ssr,RN,三种类型项目,但这三种类型的项目接口请求和登录方法设计思路是一样。
按照场景来说,业务登录大多数更接口挂钩,接口调用分为2种:单个请求和多个请求并发。实际上我们的项目一定是按照多个请求来设计,没有哪个团队会说每个页面调用1个接口。
2.1 请求登录设计
假设有4个接口请求,3个请求要登录。这种情况登录交互设计如下:
首先,这4接口请求并行,其中3个请求,只要其中1个跳蹬路,另外2个响应不再处理登录。登录成功后,这个时候怎么处理了,即登录成功后回调,或者登录成后的钩子怎么执行。我想到的有3种处理方案
2.2 登录成功后处理
方案1,登录成功后,直接刷新
优点:逻辑处理简单。
缺点:体验不好,性能是大问题,很浪费流量(cdn费用有可能浪费百万元以上,不要觉得这个流量不起眼)。
适用场景:流量极少、不想花太大人力的项目
方案2,登录成功后,重新调用接口
优点:处理逻辑比方案1稍微复杂一些,但逻辑处理还是简单。
缺点:相对方案1流量少很多,但是用户体验不是最优。如request4不需要登录,登录成功后是否还需调用了。
适用场景:流量大,但不想登录处理逻辑过于复杂,后续接入新开发,能快速上手。我们的大多数项目可能就这类。
方案3,登录成功后,只调用需要登录接口
这个方案实际就是方案1,只不过做了更精准的处理。
优点:如果做到极致,那么这个方案的性能和用户体验,是最佳的。
缺点:
- 处理逻辑非常复杂,要对业务非常熟悉,哪些接口需要登录,哪些接口不需要登录,哪些接口虽然不需要登录,但是在有登录态的时候,返回出参是不一样的。
- 不需要重新调用接口,那么这些接口的数据该怎么缓存。
- 怎么保证登录成功后每个接口响应依赖处理逻辑,和没有跳登录是一样的。
适用场景:这个方案,常见于RN项目。但h5 app混合开发,虽然不多。如: 我们在电商买了一个东西,加入到购物车后,准备点击结算,但没有登录,这个时候就会让你登录,登录成功回来后,显然是不会让用户重复点击结算按钮,而是登录成功后执行点击结算按钮的逻辑。
总结一下
这3个方案的,不是孰优孰劣,一定要看情况使用。
3、请求登录代码封装
请求登录封装思路见下图
登录代码封装思路
const globalLoginInfo = {
// 判断是否正在登录,true,不跳登录;false,跳登录
isLogining: false,
handleAfterloginSuccess: () => {},
}
export function setGlobalLoginInfo (otps) {
// 这么处理,防止 globalLoginInfo 被污染
Object.assign(globalLoginInfo)
}
export function getGlobalLoginInfo () {
// 这么处理,防止 globalLoginInfo 被污染
return cloneDeep(globalLoginInfo)
}
// 判断是否登录
export function getIsLogin() {
// ...
}
// 登出
export function loginOut() {
// ...
}
// 登录
export function login () {
const {
isLogining,
handleAfterloginSuccess
} = getGlobalLogin()
// 正在登录中,跳出函数
if (isLogining) {
return
}
// 跳登录,设置正在登录中
setGlobalLoginInfo({ isLogining: true })
// 登出,清除之前登录
loginOut()
// 封装一个事件,用来执行登录成功后的回调
event.on('loginSuccess', () => {
setGlobalLoginInfo({ isLogining: false })
// 在自家app内
if (isInApp) {
handleAfterloginSuccess
? handleAfterloginSuccess()
: window.location.reload()
return
}
window.location.reload()
})
// 跳转登录页面
sdk.login()
}
接口请求的登录封装
// 接口请求
async function request ({ params, url, method, handleAfterLoginSuccess, isNeedLogin }) {
// ...
setGlobalLoginInfo({ handleAfterLoginSuccess })
// 如果使用axios,那就使用 beforeReuqest 和 afterResponse
const res = await fetch(url, method, params)
// ...
const isLogin = getIsLogin(res)
if (isNeedLogin && !isLogin ) {
login()
}
// ...
}
业务页面调用
// 页面初始化
function pageInit () {
// 设置当前页面的登录成功后的回调
setGlobalLoginInfo({ handleAfterLoginSuccess: () => pageInit()})
Promise.all([
request1(),
request2(),
request3(),
request4()
])
}
4、补充:ios 的 wkView 的 cookie 问题
现在移动端项目,大多数是在自己公司的app webView运行,涉及到native层。直白的说, 调用接口请求方法,不再是xmlHttpRequest 或者w3c的fetch,有可能是自己公司native封装。我们简单取名叫 native 的 http request。
那这有什么问题?问题在于ios的 wkWebView(ios app 的主流webView) native 层 cookie 和 web 层 cookie 是独立、不共享的。导致即使拥web层请求又使用native请求,那就存在一个cookie同步过程。常见的项目是ssr项目。
ssr 项目有服务端请求、客户端请求,客户端请求是使用 native 的 http request,能够拿到native 层cookie,服务端请求,使用的web层的请求,只能拿到 web层 cookie。那如何使服务端拿到cookie呢?显然是将native层 cookie 同步到 web 层 cookie就可以了,也就是下图,黄色方块内容,native 单独封装了一个page reload,执行页面刷新,就会同步cookie
ps: 严格来讲, ssr项目不应该通过执行native page reload 来同步cookie,这样会导致静态资源重复加载。但是在我的团队项目实践中,ssr 项目的服务接口请求,比客户端请求响应过,原因是我司服务端接口请求网络是内部网络,不需要执行https速度快,当然速度快还有其他原因。刷新页面比客户端重新调用接口后,页面渲染更快。所以,为了体验好,团队ssr项目登录成功使用上面方案1,即登录成功后刷新页面。 所以,很多时候,问题的解决方案,往往根据实践情况变更的,方案不是永层不变得。
(完)