健壯高效的小程序登錄方案


健壯高效的小程序登錄方案

登錄是一項核心基礎功能,通過登錄對用戶進行唯一標識,繼而才可以提供各種跟蹤服務,如收藏、下單、留言、消息、發布、個性化推薦等。小程序功能的方方面面大多會直接/間接涉及登錄,因而,登錄功能健壯與否高效與否是值得重點關注與保障的。

登錄涉及的面比較多:觸發場景上,各種頁面各種交互路徑都可能觸發登錄;交互過程上,既需要用戶提供/證明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化:
class Login {
  static _loginSteps = { //各登錄步驟
    /**
     * 微信登錄:調用微信相關API,獲取用戶標識(openid,某些情況下也能獲得unionid)
     * @return {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,請求用戶授權訪問個人信息
     * @return {Promise<Object>} 微信用戶信息
     */
    requestUserInfo(){
      return new Promise((resolve,reject)=>{ //結果以Promise形式返回
        //... 
      });
    },
    //...
  }
}
  • 使用async/await管理整體時序:
  class 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,可以像普通流程一樣,將這些操作線性組合,順序處理。
這樣,就可以實現直觀清晰的時序管理了。

過期處理

  class Login {
    /**
    *登錄
    */
    static async login(options){
      if (Login.checkLogin()) //若已有前端登錄態,則直接按登錄成功返回
        return {code: 0};
  
      //否則執行登錄流程
      //...
    }
  
    /**
    * 普通數據請求,不進行登錄態檢查,結果以Promise形式返回
    * @param {Object}options 參數,格式同wx.request
    * @return {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函數
     * @return {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函數,在數據請求前后加入登錄態處理邏輯,可以保證數據請求會在有后端登錄態時被發送/重新發送。
並且,重新登錄過程對數據接口調用方是完全透明的,調用方只需要知道自己的接口需不需要登錄態,而無需進行任何登錄態相關判斷處理,重登錄過程也不會對接口調用返回結果造成任何影響。
這樣,就可以實現登錄態過期自動重新登錄了。

並發控制

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完成登錄過程,若失敗再去請求用戶授權。

這樣,只有新用戶才會出現授權彈窗;老用戶、回歸用戶,都可以直接靜默完成登錄過程。

場景適配問題

問題:
不同場景對登錄行為可能有不同的期望:

  • 有些場景,希望只在需要時自動登錄,如商品詳情頁,希望在用戶點擊留言、收藏等按鈕時自動調起登錄並完成留言、收藏等相應操作;
  • 有些場景,希望只嘗試靜默登錄,如首頁,希望對用戶做個性化推薦和針對性投放,但又不願彈窗阻撓用戶;
  • 有些場景,希望保證前后端登錄態一致,如微信接口數據解碼。

單一的登錄流程很難滿足這種多元的場景需求。

方案:
調用登錄/要求登錄的數據接口時支持指定場景模式:

如上圖所示,登錄流程支持指定不同場景模式:

  • 通用模式,為默認模式,會自動調起登錄並完成相應數據請求和后續操作;
  • 靜默模式,只會嘗試靜默登錄,不會嘗試授權登錄,成功與否均不影響頁面功能和后續接口調用;
  • 強制模式,會重新登錄,不管前端是否保有登錄態,以保證前后端登錄態同步。

實現

場景優化方案主要是增加了一些流程&判斷,使用上文中的“時序控制”基本可以解決。
主要難點在於,上文中的免並發機制不再適用。比如,靜默模式正在進行時又觸發了一個強制模式的請求,此時,應觸發授權彈窗正常登錄而不是監聽使用靜默模式的登錄結果。
如果拆成每個模式各自免並發,一方面,登錄流程需重復書寫,不便復用;另一方面,模式之間並發也存在風險。
因而,引入公共步驟並合機制:

/**
 * 步驟並合修飾器,避免公共步驟並發進行
 * 將公共步驟單例化:若步驟未在進行,則發起該步驟;若步驟正在進行,則監聽並使用其執行結果,而不是重新發起該步驟
 */
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框架寫法,原生小程序/其它框架可類似參考。

  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:

<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:

_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:

export default {
  _listeners : [],
  subscribe(listener){ //前一頁面設置監聽
    this._listeners.push(listener);
  },
  notify(res){  //登錄頁進行結果回調
    this._listeners.forEach(listener=>listener(res));
    this._listeners = [];
  },
}

登錄模塊login.js:

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:

<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.:
class Login {
  static _config = { //可配置項
    /**
    * 剛需:小程序編號,用於區分不同的小程序,由后端分配
    */
    source: '',
    /**
    * 個性化:自定義用戶授權交互
    * @return {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頂部進行配置,應該就沒有這樣的時序風險。但復用方是不會自覺的,不一定會使用同一個實例,配置過程也不一定會被放在頂部,甚至有可能被放在某些異步數據返回之后。因而登錄模塊只導出唯一實例並加入配置檢查環節以保證該邏輯健壯性:
/**
 * 類修飾器,確保調用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 登錄模式
   * @return {Promise<Object>} res 登錄結果
   */
  static async login(options){
    //...
  }

  /**
   * 要求登錄態的數據請求
   * @param {Object} options 請求參數,格式同wx.request
   * @param {Object} options.loginOpts 登錄選項,格式同login函數
   * @return {Promise} 返回結果,resolve時為數據接口返回內容, reject時為請求詳情
   */
  static async requestWithLogin(options){
    //...
  }

  //@requireConfig修飾器會在login、requestWithLogin等對外API被調用時,自動檢查模塊配置狀態,若未進行適當配置(如未提供source值),則直接報錯;從而避免編碼疏漏導致的潛在時序風險
}

export default Login;

這樣,就可以實現在多個小程序間復用登錄模塊,由登錄模塊統一維護整體時序和默認流程,同時支持各小程序進行差異性定制&擴展。

多頁面間復用&定制

問題:
不同頁面對登錄過程有時也存在定制需求,比如授權引導文案,有些頁面可能希望提示“授權后可以免費領紅包”,有些頁面可能是“授權后可以為好友助力”/“授權后可以獲得智能推薦”/... 諸如此類。

方案&實現:
在頁面中設置鈎子供其提供個性化配置。e.g.:

頁面xxx.wpy:

<script>
  import wepy from 'wepy';

  export default class extends wepy.page {
    //登錄授權文案配置函數,可以覆蓋授權界面的默認提示文案
    $loginUserAuthTips(){
      return {
        title: '同意授權后你可以',
        content: '查看附近的人,免費領紅包,低價淘好貨。授權僅供體驗產品功能,我們保證絕不會泄露您的隱私。',
        confirmTxt: '確認授權'
      }
    }
  }
</script>  

小程序級登錄配置:

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做的,挺方便的一個小工具,而且是在線、免費的,順手分享下。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM