登入是一項核心基礎功能,通過登入對使用者進行唯一標識,繼而才可以提供各種跟蹤服務,如收藏、下單、留言、訊息、釋出、個性化推薦等。小程式功能的方方面面大多會直接/間接涉及登入,因而,登入功能健壯與否高效與否是值得重點關注與保障的。
登入涉及的面比較多:觸發場景上,各種頁面各種互動路徑都可能觸發登入;互動過程上,既需要使用者提供/證明id,也需要後端記錄維護,還需要保證安全性;複用場景上,既是通用功能,需要多場景多頁面甚至多小程式複用,又是定製功能,需要各場景/頁面/小程式區分處理。要做到各種情形下都有良好的互動體驗,且健壯、高效、可複用、可擴充套件、可維護,還是相對比較複雜的。
本文將探討小程式登入過程中的一些主要需求和問題,以漸進迭代的方式提出並實現一個健壯、高效的登入方案。
順帶一提,es6語法中的async/await、Promise、decorator等特性對於複雜時序處理相當有增益,在本文中也會有所體現。
基礎流程
如上圖所示,基礎登入流程為:
- 呼叫微信登入介面wx.login獲取微信登入態
- 呼叫微信使用者資訊介面wx.getUserInfo獲取微信使用者資訊
- 呼叫後端登入介面,根據微信使用者標識及資訊,記錄維護自己的使用者體系
該流程主要基於以下考慮:
- 互動上,使用者只需在微信的授權彈窗上點選確認,不需要輸入賬號密碼等複雜操作;
- 體驗上,可以直接獲取微信暱稱頭像等作為初始使用者資訊,使用起來更親切,傳播時好友辨識度也更高;
- 開發上,可以直接使用或對映微信使用者標識,無需自己進行標識的生成和驗證;
- 安全上,微信已經在使用者資訊的獲取、傳輸、解密等環節做了許多處理,安全性相對有保障。
健壯流程
拒絕授權問題
問題:
獲取微信使用者資訊時,會出現一個授權彈窗,需要使用者點選“允許”才能正常獲取;
若使用者點選“拒絕”,不僅當次登入會失敗,一定時間內後續登入也會失敗,因為短期內再次呼叫微信使用者資訊介面時,微信不會再向使用者展示授權彈窗,而是直接按失敗返回。
這樣導致使用者只要拒絕過一次,即使後來對小程式感興趣了願意授權了,也難以再次操作。
方案:
如上圖所示,增加以下流程以處理拒絕授權問題:
- 獲取微信使用者資訊失敗時,判斷是否近期內拒絕授權導致;
- 若為拒絕授權導致,則提示並開啟許可權皮膚,供使用者再次操作;
- 若使用者依然未授權,則本次登入失敗,否則繼續後續流程。
這樣,使用者拒絕授權只會影響本次登入,不至於無法進行下次嘗試。
登入態過期問題
問題:
- 微信登入態有效期不可控
上圖截自微信官方文件,從中可以看出:- 後端session_key隨時可能失效,什麼時候失效開發者不可控;
- 要保證呼叫介面時後端session_key不失效,只能在每次呼叫前先使用wx.checkSession檢查有效期或直接重新執行微信登入介面;
- 前端不能隨便重新執行微信登入介面,可能導致正在進行的其它後端任務session_key失效;此外,實踐中發現,wx.checkSession平均耗時約需200ms,每次介面呼叫前都先檢查一遍,開銷還是蠻大的。如何既保證介面功能正確有效,又不用每次耗費高額的查詢開銷,成為了一個問題。
- 後端登入態過期
後端自身的登入態有效期也存在類似的問題,有可能在呼叫介面時才發現後端登入態已過期。
方案:
如上圖所示,增加以下流程以處理登入態過期問題:
- 呼叫資料介面時顯式指明是否需要登入態,若需要則在介面呼叫前後自動加入登入態校驗邏輯;
- 介面呼叫前只校驗前端登入態,不校驗後端登入態,也不校驗微信登入態,以節省每次校驗開銷;
- 介面呼叫後校驗後端及微信登入態,若後端返回登入態相關錯誤碼,則重置前端登入態、重新登入、重新呼叫資料介面。
這樣,只有在真正需要重新登入的時候(無前端登入態/後端登入態失效/後端被提示微信登入態失效)才會重新執行登入流程;並且,一旦需要重新登入,就會自動重新觸發登入流程。
併發問題
問題:
如上圖所示,頁面各元件各功能有可能同時觸發登入流程,可能會導致:
- 額外效能開銷,登入流程重複進行,登入介面重複呼叫;
- 體驗問題,連續多次彈窗,影響使用者互動;
- 邏輯問題,後一次登入重新整理了前一次登入的session_key,導致前一次登入介面解碼失敗,返回異常結果。
方案:
如上圖所示,加入免併發邏輯:若登入流程正在進行,則不重複觸發登入流程,而是加入當前流程的監聽佇列,待登入結束時再一併處理。這樣,任一時刻最多隻有一個登入流程正在進行。
流程實現
時序控制
如上圖所示,目前登入流程已較為複雜,步驟較多,且大多是非同步操作,每步成功失敗需要區分處理,處理過程又會相互交織。如果直接在微信介面/網路介面提供的success/fail回撥中進行邏輯處理,會造成:
- 回撥層層巢狀,影響程式碼書寫和閱讀;
- 不同路徑公共步驟難以統一提取;
- 時序邏輯不直觀,不易管理。
因而採用Promise+async/await進行時序管理:
- 將每個步驟Promise化:
123456789101112131415161718192021222324252627282930class Login {static _loginSteps = { //各登入步驟/*** 微信登入:呼叫微信相關API,獲取使用者標識(openid,某些情況下也能獲得unionid)* <a href='http://www.jobbole.com/members/wx1409399284'>@return</a> {Promise<Object>} 微信使用者標識*/wxLogin(){return new Promise((resolve,reject)=>{ //結果以Promise形式返回wx.login({success(res){resolve(Object.assign(res, {succeeded: true})); //成功失敗都resolve,並通過succeeded欄位區分},fail(res){resolve(Object.assign(res, {succeeded: false})); //成功失敗都resolve,並通過succeeded欄位區分},})});},/*** 獲取微信使用者資訊:呼叫微信相關API,請求使用者授權訪問個人資訊* <a href='http://www.jobbole.com/members/wx1409399284'>@return</a> {Promise<Object>} 微信使用者資訊*/requestUserInfo(){return new Promise((resolve,reject)=>{ //結果以Promise形式返回//...});},//...}} - 使用async/await管理整體時序:
123456789101112131415161718192021222324252627282930class Login {static async _login(){ //管理整體時序//....let steps = Login._loginSteps;//微信登入let wxLoginRes = await steps.wxLogin();if (!wxLoginRes.succeeded) //微信登入介面異常,登入失敗return { code: -1};//獲取微信使用者資訊let userInfoRes = await steps.requestUserInfo();if (!userInfoRes.succeeded && userInfoRes.failType==='userDeny'){ //使用者近期內曾經拒絕授權導致獲取資訊失敗await steps.tipAuth(); //提示授權let settingRes = await steps.openSetting(); //開啟許可權皮膚if (!settingRes.succeeded) //使用者依然拒絕授權,登入失敗return {code: -2};userInfoRes = await steps.requestUserInfo(); //使用者同意授權,重新獲取使用者資訊}if (!userInfoRes.succeeded) //其它原因導致的獲取使用者資訊失敗return {code: -3};//獲取使用者資訊成功,進行後續流程//....}}
如以上程式碼所示,微信登入、獲取微信使用者資訊、提示授權、開啟許可權皮膚等每一步都是非同步操作,都要等待success/fail回撥才能獲得操作結果併發起下一個操作;但利用Promise+async/await,可以像普通流程一樣,將這些操作線性組合,順序處理。
這樣,就可以實現直觀清晰的時序管理了。
過期處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
class Login { /** *登入 */ static async login(options){ if (Login.checkLogin()) //若已有前端登入態,則直接按登入成功返回 return {code: 0}; //否則執行登入流程 //... } /** * 普通資料請求,不進行登入態檢查,結果以Promise形式返回 * @param {Object}options 引數,格式同wx.request * <a href='http://www.jobbole.com/members/wx1409399284'>@return</a> {Promise} 請求結果,resolve時為資料介面返回內容, reject時為請求詳情 */ static async request(options){ return new Promise((resolve, reject)=>{ wx.request(Object.assign({}, options, { success(res){ resolve(res.data); }, fail(res){ reject(res); } }); }); } /** * 要求登入態的資料請求,封裝了登入態邏輯 * @param {Object} options 請求引數,格式同wx.request * @param {Object} options.loginOpts 登入選項,格式同login函式 * <a href='http://www.jobbole.com/members/wx1409399284'>@return</a> {Promise} 返回結果,resolve時為資料介面返回內容, reject時為請求詳情 */ static async requestWithLogin(options){ //先校驗/獲取前端登入態,保證大部分情況下請求發出時已登入 let loginRes = await Login.login(options.loginOpts); if (loginRes.code != 0) throw new Error('login failed, request not sent:'+options.url); //傳送資料請求 let resp = await Login.request(options); //若後端登入態正常,則正常返回資料請求結果 if(!Login._config.apiAuthFail(resp, options)) //根據後端統一錯誤碼判斷登入態是否過期 return resp; //若後端登入態過期 Login._clearLoginInfo(); //重置前端登入態,保證後續再次呼叫login時會真正執行登入環節 return Login.requestWithLogin(options); //重新登入,重新傳送請求,並將重新傳送的請求的返回結果作為本次呼叫結果予以返回 } } |
如以上程式碼所示,單獨封裝一個requestWithLogin函式,在資料請求前後加入登入態處理邏輯,可以保證資料請求會在有後端登入態時被髮送/重新傳送。
並且,重新登入過程對資料介面呼叫方是完全透明的,呼叫方只需要知道自己的介面需不需要登入態,而無需進行任何登入態相關判斷處理,重登入過程也不會對介面呼叫返回結果造成任何影響。
這樣,就可以實現登入態過期自動重新登入了。
併發控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Login { static _loginSingleton = null; //正在進行的登入流程 static async _login(){ //登入流程... } //封裝了免併發邏輯的登入函式 static async login(){ if (Login._loginSingleton) //若當前有登入流程正在進行,則直接使用其結果作為本次登入結果 return Login._loginSingleton; //否則觸發登入流程 Login._loginSingleton = Login._login(); //並在登入結束時釋放併發限制 Login._loginSingleton.then(()=>{Login._loginSingleton = null}).catch(()=>{Login._loginSingleton = null}); //返回登入結果 return Login._loginSingleton; } } |
如以上程式碼所示,利用Promise可以被多次then/catch的特性(亦即,一個async函式呼叫結果可以被await多次),可以使用一個Promise來記錄當前登入流程,後續呼叫直接對該Promise進行監聽。
這樣,就可以實現登入流程免併發了。
至此,我們就得到了一個功能可用、相對健壯、相對高效的登入模組。但依然還是存在優化空間的。
場景優化
二次授權問題
問題:
使用者同意授權後,小程式可以訪問到微信使用者資訊,並且一段時間內再次訪問時,也不會重新出現授權彈窗;
但是,如果使用者長時間未使用小程式,或將小程式刪除重進,則登入時會再次出現授權彈窗。
一方面會對使用者造成干擾,影響其瀏覽效率;另一方面,不利於流失使用者召回。
方案:
再次授權場景其實並不是很必要:
- 使用者第一次授權時,開發者已經可以獲得使用者暱稱、頭像等使用者資訊和openid、unionid等使用者標識;
- 再次授權時,雖然使用者資訊可能有更新,但完全可以等使用者進個人主頁/編輯資訊時再進行同步,沒必要剛進小程式就彈窗;
- 再次授權時,使用者標識並不會變化;
- 只呼叫微信登入介面,不觸發授權,已經可以獲得openid了,通過openid就可以從資料庫中查詢使用其上次授權時的使用者資訊和unionid等其它使用者標識。
因而,增加以下流程以優化二次授權場景:
如上圖所示,在微信登入介面呼叫成功之後,先嚐試直接根據openid完成登入過程,若失敗再去請求使用者授權。
這樣,只有新使用者才會出現授權彈窗;老使用者、迴歸使用者,都可以直接靜默完成登入過程。
場景適配問題
問題:
不同場景對登入行為可能有不同的期望:
- 有些場景,希望只在需要時自動登入,如商品詳情頁,希望在使用者點選留言、收藏等按鈕時自動調起登入並完成留言、收藏等相應操作;
- 有些場景,希望只嘗試靜默登入,如首頁,希望對使用者做個性化推薦和針對性投放,但又不願彈窗阻撓使用者;
- 有些場景,希望保證前後端登入態一致,如微信介面資料解碼。
單一的登入流程很難滿足這種多元的場景需求。
方案:
呼叫登入/要求登入的資料介面時支援指定場景模式:
如上圖所示,登入流程支援指定不同場景模式:
- 通用模式,為預設模式,會自動調起登入並完成相應資料請求和後續操作;
- 靜默模式,只會嘗試靜默登入,不會嘗試授權登入,成功與否均不影響頁面功能和後續介面呼叫;
- 強制模式,會重新登入,不管前端是否保有登入態,以保證前後端登入態同步。
實現
場景優化方案主要是增加了一些流程&判斷,使用上文中的“時序控制”基本可以解決。
主要難點在於,上文中的免併發機制不再適用。比如,靜默模式正在進行時又觸發了一個強制模式的請求,此時,應觸發授權彈窗正常登入而不是監聽使用靜默模式的登入結果。
如果拆成每個模式各自免併發,一方面,登入流程需重複書寫,不便複用;另一方面,模式之間併發也存在風險。
因而,引入公共步驟併合機制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
/** * 步驟併合修飾器,避免公共步驟併發進行 * 將公共步驟單例化:若步驟未在進行,則發起該步驟;若步驟正在進行,則監聽並使用其執行結果,而不是重新發起該步驟 */ function mergingStep(target, name, descriptor) { let oriFunc = descriptor.value; let runningInstance = null; descriptor.value = function (...args) { if (runningInstance) //若步驟正在進行,則監聽並使用其執行結果,而不是重新發起該步驟 return runningInstance; let res = oriFunc.apply(this, args); if (!(res instanceof Promise)) return res; runningInstance = res; runningInstance.then(function () { runningInstance = null; }).catch(function () { runningInstance = null; }); return runningInstance; } } class Login { static _loginSteps = { @mergingStep //步驟併合修飾器,避免公共步驟併發重複進行 wxLogin(){ return new Promise((resolve,reject)=>{ //... }); }, @mergingStep //步驟併合修飾器,避免公共步驟併發重複進行 async silentLogin({wxLoginRes}){ //... }, ... } static async login(options){ //.... //嘗試靜默登入 let silentRes = await steps.silentLogin({wxLoginRes}); if (silentRes.succeeded) { //靜默登入成功,結束 return {code: 0, errMsg: 'ok'}; } if (options.mode==='silent') //靜默模式,只嘗試靜默登入,不觸發授權彈窗;不管成功失敗都不影響頁面功能和後續介面呼叫 return {code: 0, errMsg: 'login failed silently'}; //其它模式繼續嘗試授權登入 //... } } |
如以上程式碼所示,將登入免併發改為每個公共步驟免併發,登入流程中就可以根據場景模式自由地進行步驟管理。
這樣,就可以實現對不同登入場景進行定製化支援。
效果示例
簡潔起見,以下程式碼使用wepy框架寫法,原生小程式/其它框架可類似參考。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import Login from '../../lib/Login'; export default class extends wepy.page { async onLoad(){ //頁面初始化 let dataRes = await Login.requestWithLogin({ //呼叫頁面資料介面 url: 'xxx/xxx', loginOpts: {mode: 'silent'} //使用靜默模式,若為老使用者/迴歸使用者,會自動悄悄登入,後端返回資料時可以包含一些個性化推薦;若為新使用者,也不會觸發彈窗,後端返回資料時只包含常規元素 }); //... } methods = { async onComment(){ //使用者點選了評論按鈕 let addRes = await Login.requestWithLogin({ //呼叫新增評論介面 url: 'xxx/addComment', data: {comment: 'xxx'}, loginOpts: {mode: 'common'} //使用通用模式,若已登入,會直接傳送請求;若未登入,會自動調起登入併傳送請求 }); //... } } } |
如以上程式碼所示,可以做到老使用者/迴歸使用者進入頁面時自動悄悄登入,以提供更多個性化服務;新使用者進入頁面時不進行任何干擾,直到進行留言等操作時才自動出現授權彈窗,且授權完成後自動完成該次行為,無需使用者再次操作。
並且,這些過程對業務程式碼是完全透明的,業務程式碼只需要知道自己呼叫的介面是 必須登入/最好登入/必須第一次呼叫就登入/不用登入,並相應地指定 mode=common/silent/force/不使用requestWithLogin,即可。
這樣,我們的登入模組可以在不同場景指定不同登入邏輯,從而支援設計實現更多元更精細更流暢的登入互動。
介面優化
問題:
獲取微信使用者資訊時,直接出現系統授權彈窗有時候是很突兀的;使用自定義授權介面和價值文案進行引導,得當的話可以有效提高授權成功率。
而且,從10月10號起,小程式將不再支援自動彈窗授權使用者資訊和自動開啟許可權皮膚,這兩種操作必須使用<button>元件由使用者主動觸發。彼時起,自定義介面將不再是優化,而會是必需。
這意味著登入過程必須與頁面dom耦合,之前的純js邏輯不再適用。
方案1:登入浮層
在所有頁面放置登入浮層,頁面需要登入時則調起該浮層,經由浮層按鈕完成授權及後續流程。
實現
- 浮層引入
各個頁面都需要存在登入浮層。可以將各種頁面公共dom元素,包括登入浮層、網路異常介面、返回首頁快捷導航、公眾號關注元件等統一抽離成一個父公共元件,編寫eslint規則要求所有頁面統一引入,以此實現&保證登入時浮層存在。 - 浮層無縫時序
授權浮層AuthModal.wpy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
<template> <view class="modal" wx:if="{{show}}"> <button open-type="getUserInfo" bindgetuserinfo="onGetUserInfo">登入</button> </view> </template> <script> import wepy from 'wepy'; export default class extends wepy.component { data = { show: false, listener: null, //結果監聽 } computed = {} methods = { onGetUserInfo(ev){ //使用者點選了授權按鈕 this.listener && this.listener({ //回撥授權結果 succeeded: ev.detail.errMsg.includes('ok'), data: ev.detail, }); this.show = false; //關閉授權浮層 this.$apply(); } } //開啟授權浮層 open(){ return new Promise((resolve, reject)=>{ this.listener = resolve; //設定監聽 this.show = true; //開啟浮層 this.$apply(); //使用者操作結束後會觸發監聽回撥'resolve',使當前Promise resolve,從而自動繼續執行後續登入步驟 }); } onUnload(){ //頁面解除安裝,使用者未點選按鈕直接返回 在此處理 this.listener && this.listener({ //授權失敗回撥 succeeded: false, data: null, }); } } </script> |
登入模組login.js:
1 2 3 4 5 6 7 8 9 10 11 12 |
_loginSteps = { async requestUserInfo(){ let page = getCurrentWepyPage(); //獲取當前頁面例項 let userInfoRes = await page.$invoke('AuthModal', 'open'); //開啟授權浮層,並監聽其操作結果 //正常進行後續處理 if (userInfoRes.succeeded) //授權成功後續處理... else //授權失敗後續處理... } } |
如以上程式碼所示,雖然自定義浮層需要展示按鈕、等待使用者點選、處理點選、考慮使用者不點選直接返回,互動流程相對複雜,但依然可以利用Promise使互動細節對外透明。開啟浮層時返回一個Promise,在各個互動出口對Promise進行resolve,則使用時只需將其作為一個普通的非同步過程對待。
這樣,就可以實現無縫接入自定義浮層授權。
方案2:獨立登入頁
需要授權使用者資訊時,跳轉至一個專門的登入頁面,頁面中展示引導內容和授權<button>,使用者操作完畢後再自動返回先前頁面。
實現
- 元素引入
登入所需dom元素只在登入頁引入即可。 - 頁面無縫時序
由於小程式的程式碼包特性,各頁面可以共享全域性變數和全域性函式;並且後一頁面開啟時,前一頁面依然駐留在記憶體中,前一頁面遺留的非同步任務也依然會繼續執行。因而,可以在前一頁面設定監聽,在登入頁進行回撥:
授權全域性資料模組userAuthHub.js:
1 2 3 4 5 6 7 8 9 10 |
export default { _listeners : [], subscribe(listener){ //前一頁面設定監聽 this._listeners.push(listener); }, notify(res){ //登入頁進行結果回撥 this._listeners.forEach(listener=>listener(res)); this._listeners = []; }, } |
登入模組login.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import userAuthHub from '../lib/userAuthHub'; _loginSteps = { async requestUserInfo(){ let userInfoRes = await new Promise((resolve, reject)=>{ userAuthHub.subscribe(resolve); //監聽授權結果 wx.navigateTo({url: '/pages/login/login'}); //開啟登入頁 //登入頁操作結束後會觸發監聽回撥'resolve',使當前Promise resolve,從而自動繼續執行後續登入步驟 }); //正常進行後續處理 if (userInfoRes.succeeded) //授權成功後續處理... else //授權失敗後續處理... } } |
登入頁login.wpy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<template> <button open-type="getUserInfo" bindgetuserinfo="onGetUserInfo">登入</button> </template> <script> import wepy from 'wepy' import userAuthHub from '../../lib/userAuthHub'; export default class extends wepy.page { data = { userInfoRes: { //記錄授權資訊 succeeded: false, data: null, } } methods = { onGetUserInfo(ev){ //使用者點選了授權按鈕 this.userInfoRes = { //記錄結果 succeeded: ev.detail.errMsg.includes('ok'), data: ev.detail, }; wx.navigateBack(); //返回原先頁面 } } onUnload(){ //頁面解除安裝,使用者未點選按鈕直接返回 和 點選按鈕授權後頁面自動返回 兩種場景在此處統一處理 userAuthHub.notify(this.userInfoRes); //回撥授權結果 } } </script> |
如以上程式碼所示,雖然授權過程需要進行跨頁面互動,但利用Promise和小程式程式碼包特性,可以在前一頁面設定監聽,登入頁面進行回撥。登入頁面互動結束後,前一頁面會自動繼續執行登入流程,呼叫方無需進行返回重新整理等額外處理,資料介面也會繼續呼叫,使用者無需再次操作。
這樣,就可以實現無縫接入跨頁面授權互動。
兩種方案都可以實現自定義授權介面。內嵌浮層會增加一定維護成本和少量資源開銷,但可以直接在當前頁面完成登入互動,頁面自定義空間也相對更大;獨立登入頁會來回跳轉犧牲一定的互動體驗,但可以把登入所需dom元素集中在登入頁,減少維護成本和頁面侵入。二者各有優劣,可以按需採用或混合使用。
這樣,我們的登入模組可以使用自定義授權介面,從而支援設計實現更雅觀更精緻的授權引導。
複用優化
多小程式間複用&定製
問題:
開發方可能同時維護著多個小程式,這些小程式使用著相同的後端介面和後端使用者體系,又有著各自的小程式標識和使用訴求。
一方面,希望登入模組可以統一維護,不需要每個小程式各自開發;另一方面,又希望各小程式可以進行差異化定製,包括小程式前端標識不一致等剛性差異,和授權提示文案、埋點、授權互動等個性差異。
方案&實現:
- 統一流程+個性化配置
公共&預設流程由登入模組統一維護,各小程式直接複用;差異流程支援各小程式以配置的形式自定義擴充套件&覆蓋。 e.g.:
1234567891011121314151617181920212223242526272829303132333435363738class Login {static _config = { //可配置項/*** 剛需:小程式編號,用於區分不同的小程式,由後端分配*/source: '',/*** 個性化:自定義使用者授權互動* <a href='http://www.jobbole.com/members/wx1409399284'>@return</a> {Promise<Object>} 格式同wx.getUserInfo,或返回null使用預設授權邏輯*/userAuthHandler: null,//....}static _loginSteps = {//靜默登入async _silentLogin({wxLoginRes}){let silentRes = await Login.request({url: 'xxx/mpSilenceLogin',data: {code: wxLoginRes.code,source: Login._config.source, //小程式需要配置自身編號,後端根據編號找到正確的解碼金鑰和id對映表,進行靜默登入}});//...},//獲取微信使用者資訊async requestUserInfo(){//小程式可以配置自定義授權互動,如:將授權互動改為自定義浮層/自定義登入頁/...let userInfoRes = Login._config.userAuthHandler && await Login._config.userAuthHandler();if (!userInfoRes) //若未配置自定義互動,亦提供預設授權互動userInfoRes = ...;//....}}} - 配置檢查
引入配置過程會存在一個潛在風險:觸發登入時,小程式尚未完成登入模組配置。
理論上,只要全域性都使用同一個登入例項並在app.js頂部進行配置,應該就沒有這樣的時序風險。但複用方是不會自覺的,不一定會使用同一個例項,配置過程也不一定會被放在頂部,甚至有可能被放在某些非同步資料返回之後。因而登入模組只匯出唯一例項並加入配置檢查環節以保證該邏輯健壯性:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354/*** 類修飾器,確保呼叫API時已完成小程式資訊配置* @param target Login*/function requireConfig(target) {for (let prop of Object.getOwnPropertyNames(target)){if (['arguments', 'caller', 'callee', 'name', 'length'].includes(prop)) //內建屬性,不予處理continue;if (typeof target[prop] !== "function") //非函式,不予處理continue;if (['config','install','checkConfig'].includes(prop) || prop[0]==='_') //配置/安裝/檢查函式、私有函式,不予處理continue;target[prop] = (function (oriFunc, funcName) { //對外介面,增加配置檢查步驟return function (...args) {if (!target.checkConfig()){ //若未進行專案資訊配置,則報錯console.error('[Login] 請先執行Login.config配置小程式資訊,後使用Login相關功能:',funcName);return;}return oriFunc.apply(this, args); //否則正常執行原函式}}(target[prop], prop));}}/*** 登入模組名稱空間*/@requireConfig //確保呼叫API時已完成專案資訊配置class Login {/***登入* @param {Object} options 登入選項* @param {string} options.mode 登入模式* <a href='http://www.jobbole.com/members/wx1409399284'>@return</a> {Promise<Object>} res 登入結果*/static async login(options){//...}/*** 要求登入態的資料請求* @param {Object} options 請求引數,格式同wx.request* @param {Object} options.loginOpts 登入選項,格式同login函式* <a href='http://www.jobbole.com/members/wx1409399284'>@return</a> {Promise} 返回結果,resolve時為資料介面返回內容, reject時為請求詳情*/static async requestWithLogin(options){//...}//@requireConfig修飾器會在login、requestWithLogin等對外API被呼叫時,自動檢查模組配置狀態,若未進行適當配置(如未提供source值),則直接報錯;從而避免編碼疏漏導致的潛在時序風險}export default Login;
這樣,就可以實現在多個小程式間複用登入模組,由登入模組統一維護整體時序和預設流程,同時支援各小程式進行差異性定製&擴充套件。
多頁面間複用&定製
問題:
不同頁面對登入過程有時也存在定製需求,比如授權引導文案,有些頁面可能希望提示“授權後可以免費領紅包”,有些頁面可能是“授權後可以為好友助力”/“授權後可以獲得智慧推薦”/… 諸如此類。
方案&實現:
在頁面中設定鉤子供其提供個性化配置。e.g.:
頁面xxx.wpy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<script> import wepy from 'wepy'; export default class extends wepy.page { //登入授權文案配置函式,可以覆蓋授權介面的預設提示文案 $loginUserAuthTips(){ return { title: '同意授權後你可以', content: '檢視附近的人,免費領紅包,低價淘好貨。授權僅供體驗產品功能,我們保證絕不會洩露您的隱私。', confirmTxt: '確認授權' } } } </script> |
小程式級登入配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Login.config({ async userAuthHandler(){ let page = getCurrentWepyPage(); let defaultTips = { //小程式級預設文案 title: '', content: '小程式需要您的授權才能提供更好的服務哦~', confirmTxt: '知道了' }; let tips = Object.assign({}, defaultTips, page.$loginUserAuthTips && page.$loginUserAuthTips()); //支援頁面提供頁面級自定義文案以覆蓋小程式預設文案 let userInfoRes = await page.$invoke('AuthModal', 'open', tips); //... } }); |
這樣,就可以實現所有頁面共用登入模組的同時,支援每個頁面進行定製化修改。
這樣,我們的登入模組可以在多小程式、多頁面中複用,並支援各小程式、各頁面進行差異性定製。從而實現更好的可維護性可擴充套件性:
- 公共&預設流程統一維護,避免維護過程重複、分化、膨脹,減少整體維護成本,並降低各方迭代不及時風險;
- 差異&定製流程各自擴充套件,擴充套件入口下放至各小程式各頁面,擴充套件過程相互獨立互不干擾,不會對其它小程式/其它頁面造成任何影響。
總結
- 完整登入流程
- 功能
- 通過微信授權一鍵登入
- 支援靜默登入,使用者授權一次過後不會再次被要求授權
- 支援多種登入場景:通用、靜默、強制
- 支援自定義授權介面
- 健壯性
- 曾經拒絕授權,會提示&開啟許可權皮膚供二次操作
- 登入態過期,會自動重新登入重新傳送資料請求並正常返回請求資料
- 登入流程&重試機制對呼叫方完全透明,頁面使用時流程遺漏風險基本為0
- 效能
- 後端登入態惰性檢測,減少每次查詢開銷
- 公共步驟併合,減少併發成本
- 登入操作與後續介面呼叫無縫銜接,減少返回重新整理/使用者重複操作成本
- 可複用性、可擴充套件性、可維護性
- 支援多小程式複用,公共流程統一維護,差異特性各小程式各自擴充套件;
- 支援多頁面複用,公共流程小程式統一配置,差異特性各頁面各自擴充套件。
轉轉的開源庫fancy-mini上附有實現原始碼,歡迎參閱;有更好的設計思路或實現方案,歡迎交流探討。
順帶一提,es6語法對於複雜時序管理相當有增益,推薦深入學習。
順帶二提,文中流程圖是用ProcessOn做的,挺方便的一個小工具,而且是線上、免費的,順手分享下。