极速快3精准计划|极速快3有规律吗
用戶
 找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

掃一掃,登錄網站

1

主題

4

帖子

46

積分

攻城獅

Rank: 2

積分
46
2019-3-26 10:15:55 w鹹魚86 攻城獅 樓主 42043
本文講述的是自微信官方在 wx.getUserInfo API更新后,微信小程序該如何實現登錄,以及在登錄與用戶授權邏輯方面遇到的種種矛盾做出的一些可行性分析,文中出現的源碼都可在以下鏈接中Clone,僅供大家交流、參考。
本文主要講解的是小程序前端代碼,但是Clone過來的源碼包含前端與后臺代碼,并且只須簡單幾步安裝即可在本地環境中運行起來。
Github:https://github.com/wuliang9524/mini_app_login  
Gitee:https://gitee.com/wuliang924/demo_miniapp_login  
常見問題
  • Q:在小程序代碼中遇到異步嵌套的問題,代碼能讀性非常差
      
    本文中采用ES6中的Promise對象解決異步嵌套問題,若有不了解者,建議先Google了解一番后再回頭查閱本文。
  • Q:小程序Page.onLoad之后,App.onLaunch才返回用戶登錄態信息
      
    這是由于在App.onLaunch中,獲取用戶登錄態信息請求后臺接口是異步執行而導致的,我們只需要在Page.onLoad中定義一個App的回調函數即可,但是如果每一個需要先驗證登錄的Page都要定義這么一個函數則實在不理智,本文后面也會提到封裝一個公共方法。
  • 更多經典常見問題等待您的留言…

小程序前端
  • 首先先對微信的API進行Promise對象的 “改造”,由于微信API的格式大都一致,在 /utils/ 文件夾中新建一個 wxapi.js 文件
    1// /utils/wxapi.js
    2
    3const wxapi = {
    4  /**
    5   * 對微信Api Promise化的公共函數
    6   */

    7  wxapi: (wxApiName, obj) => {
    8    return new Promise((resolve, reject) => {
    9      wx[wxApiName]({
    10        ...obj,     //注意這里涉及的語法
    11        success: (res) => {
    12          resolve(res);
    13        },
    14        fail: (res) => {
    15          reject(res);
    16        }
    17      });
    18    });
    19  },
    20
    21  /**
    22   * 以下是微信Api Promise化的特殊案例
    23   */

    24  wxsetData: (pageObj, obj) => {
    25    if(pageObj && obj){
    26      return new Promise((resolve, reject) => {
    27        pageObj.setData(obj, resolve(obj));
    28      });
    29    }
    30  },
    31}
    32
    33module.exports = wxapi;
  • 接著我們在 app.js 中封裝一個 exeLogin() 方法,該方法主要做以下幾件事情:
    • 調用 wx.login 獲取到 code;
    • 用 code 請求后臺接口,后臺接口返回自定義登錄態信息(本文中包括登錄態 token 及用戶的基本信息);
    • 調用 wx.setStorage 緩存登錄態信息
    注意代碼中 exeLogin() 方法返回的是一個Promise對象,以及在 app.js 文件的開頭,載入了上一步的 wxapi.js 中定義的方法。
    1// app.js
    2
    3import {
    4  wxapi,
    5  wxsetData
    6} from './utils/wxapi.js';
    7
    8/**
    9* [exeLogin 執行登錄流程]
    10* @param  {[string]} loginKey  自定義登錄態信息緩存的key
    11* @param  {[string]} timeout   調用wx.login的超時時間
    12* @return {[Promise]}          返回一個Promise對象
    13*/

    14exeLogin: function(loginKey, timeout = 3000) {
    15    var _this = this;
    16    return new Promise((resolve, reject) => {
    17      wxapi('login', {
    18        'timeout': timeout
    19      }).then(function(res) {
    20        return wxapi('request', {
    21          'method': 'POST',
    22          'url': _this.gData.api.request + '/api/User/third',
    23          'header': {
    24            'Content-type': 'application/x-www-form-urlencoded',
    25          },
    26          'data': {
    27            'code': res.code,
    28            'platform': 'miniwechat',
    29          }
    30        })
    31      }).then(function(res) {
    32        //當服務器內部錯誤500(或者其它目前我未知的情況)時,wx.request還是會執行success回調,所以這里還增加一層服務器返回的狀態碼的判斷
    33        if (res.statusCode === 200 && res.data.code === 1) {
    34          //獲取到自定義登錄態信息后存入緩存,由于我們無需在意緩存是否成功(前面代碼有相應的處理邏輯),所以這里設置緩存可以由它異步執行即可
    35          wxapi('setStorage', {
    36            'key': loginKey,
    37            'data': res.data.data.userinfo
    38          });
    39          //userinfo里面包含有用戶昵稱、頭像、性別等信息,以及自定義登錄態的token
    40          resolve(res.data.data.userinfo);
    41        } else {
    42          return Promise.reject({
    43            'errMsg': (res.data.msg ? 'ServerApi error:' + res.data.msg : 'Fail to network request!') + ' Please feedback to manager and close the miniprogram manually.'
    44          });
    45        }
    46      }).catch(function(error) {
    47        reject(error);
    48      });
    49    });
    50},
  • OK,接著我們先繼續看下去。在 app.js 中再定義一個方法 getLoginInfo(),主要做以下幾件事情:
    • 調用 wx.checkSession() 驗證當前的登錄是否有效;
    • 若無效,則調用上一步的 exeLogin() ,執行登錄并緩存登錄態信息;
    • 若有效,則調用 wx.getStorage() 讀取緩存;
    • 當然,調用讀取緩存時我們還要判斷是否成功,若是失敗或讀取到信息與預期的不符,也直接執行 exeLogin();
    由于同樣是在 app.js 中,開頭的導入 wxapi.js 這一段省略了,能理解文中代碼里出現的 wxapi() 的含義即可,之后若沒有特殊說明, wxapi() 的含義都一樣。
    1// app.js
    2
    3/**
    4* [getLoginInfo 獲得自定義登錄態信息]
    5* @param  {[string]]} loginKey [緩存的key值]
    6* @return {[Promise]}          返回一個Promise對象
    7*/

    8getLoginInfo: function(loginKey = 'loginInfo') {
    9    var _this = this;
    10    return new Promise((resolve, reject) => {
    11      wxapi('checkSession').then(function() {
    12        //登錄態有效,從緩存中讀取
    13        return wxapi('getStorage', {
    14          'key': loginKey
    15        }).then(function(res) {
    16          //獲取loginKey緩存成功
    17          if (res.data) {
    18            //緩存獲取成功,并且值有效
    19            return Promise.resolve(res.data);
    20          } else {
    21            //緩存獲取成功,但值無效,重新登錄
    22            return _this.exeLogin(loginKey, 3000);
    23          }
    24        }, function() {
    25          //獲取loginKey緩存失敗,重新登錄
    26          return _this.exeLogin(loginKey, 3000);
    27        });
    28      }, function() {
    29        //登錄態失效,重新調用登錄
    30        return _this.exeLogin(loginKey, 3000);
    31      }).then(function(res) {
    32        resolve(res);
    33      }).catch(function(error) {
    34        reject(error);
    35      });
    36    });
    37},
  • 前面的這些,都是為接下來在 app.onLaunch() 中做準備。說說小程序注冊時 onlaunch() 主要做些什么吧:
    • 調用上一步 getLoginInfo(),然后只需要對 resolve() 和 reject() 做對應的邏輯即可;
    • resolve() 里面再調用 wx.getSetting() 獲取到相關的授權列表,與登錄態信息一并賦值給 app.gData;
    代碼中有這么一段 (_this.loginedCb && typeof(_this.loginedCb) === 'function') && _this.loginedCb();  
    若無法理解可以先忽略,后面會重點說這就是為了解決文章開頭 常見問題 第二個問題的解決方案。
    1// app.js
    2
    3onLaunch: function() {
    4    var _this = this;// 獲取登錄態信息
    5this.getLoginInfo().then(function(res) {
    6  if ((typeof res !== 'undefined') && res.token) {
    7    //獲取用戶全部的授權信息
    8    wxapi('getSetting').then(function(setting) {
    9      _this.gData.logined = true;
    10      _this.gData.userinfo = res;
    11      _this.gData.authsetting = setting.authSetting;
    12
    13      //執行頁面定義的回調方法
    14      (_this.loginedCb && typeof(_this.loginedCb) === 'function') && _this.loginedCb();
    15    }, function(error) {
    16      return Promise.reject(error);
    17    });
    18  } else {
    19    return Promise.reject({
    20      errMsg: 'LoginInfo miss token!',
    21    });
    22  }
    23}).catch(function(error) {
    24  wx.showModal({
    25    title: 'Error',
    26    content: error.errMsg,
    27  });
    28  return false;
    29});
    30},
  • 到此,小程序一進來開始的登錄流程基本完成,在開始涉及頁面 Page 相關的邏輯之前,我們先針對上述的代碼提一個問題:
    上述代碼中自始至終都沒有提到 請求用戶允許授權、獲取用戶昵稱、頭像等基本信息這一點;你甚至會發現,沒有用戶基本數據,在 exeLogin() 中我們登錄請求后臺接口只有一個 code 有效參數的情況下,后臺怎么完用戶注冊的邏輯呢?
      
    說說我對這個問題的理解,也是本文創作的動力。
      
    首先明確一點,在邏輯設計上的確就只要 wx.login() 返回的 code 傳給后臺, 就能完成后臺注冊新用戶、返回后臺自定義登錄態信息等等。
      
    實現上也不難,只需要后臺API通過前端傳過來的 code以及小程序 appid && secret 開發者管理的重要秘鑰,即可在后臺調用微信小程序服務端接口 code2Session,接口返回的 openid、unionid、session_key 就足夠后臺解決 用戶唯一性 的問題了。
      
    至于用戶注冊需要的基本數據,先又系統隨機生成。而真正要關心的是,設計一套自己的后臺API token機制,機制里關聯上當前注冊的用戶返回自定義登錄態信息給前端。下一次請求業務接口時參數中帶上自定義登錄態信息即可驗證登錄,這樣就已經完成了小程序登錄流程。
      
    至于用戶信息完善,則就涉及到小程序授權。我們只需要在某些需要用戶授權的 Page 頁面里,檢驗用戶是否授權,若沒有授權,統一跳轉到 /pages/auth/auth 頁面完成授權并請求后臺更新用戶信息的接口,而這個接口的前提是驗證用戶登錄。
  • 帶上對上面答案的認知,我們開始說小程序頁面 Page 方面的邏輯。分以下幾點:
    • 由于在 App.onLaunch() 中,用戶登錄態信息是異步的方式請求后臺接口的,接口返回登錄態信息并賦值給全局變量 app.gData 前,很大可能小程序頁面已經執行完了 onLoad() 方法,這樣直接對我們頁面里后面的寫邏輯造成了致命的錯誤(頁面中獲取到的登錄態信息是錯誤的)。
    • 還有就是用戶授權方面的問題。對于那些業務邏輯要求必須有用戶基本信息的頁面,我們得在頁面初始化時驗證用戶授權狀態(在登錄的時候我們為這一步做過準備),若未曾詢問過或者用戶拒絕授權,我們同意跳轉到 /pages/auth/auth 頁面進行用戶授權步驟,同意后返回上一頁并做相應的更新。

    我們很容易會想到,上面的這兩點都是多頁面中調用到的,必然會考慮到靈活封裝好,之后每個頁面調用即可。
    在 app.js 中先預定義全局控制字段,包括登錄控制字段 logined, 授權列表 authsetting,以及用戶信息(包含token,就是登錄態信息):
    1// app.js
    2
    3'gData': {
    4    'logined': false, //用戶是否登錄
    5    'authsetting': null, //用戶授權結果
    6    'userinfo': null, //用戶信息(包含自定義登錄態token)
    7},

    針對上面的第一點,我們在 app.js 下封裝 pageGetLoginInfo() 方法,該方法主要做的事情有一下幾點:
    • 判斷登錄控制字段 app.gData.logined,若已經登錄——控制字段值為 true,直接把全局控制字段賦值給頁面的控制字段;
    • 若全局登錄狀態控制字段值為 false,則我們完全可認為是由于異步請求后臺的原因導致的全局登錄控制字段未賦值(因為上文提到登錄失敗都可以認為是系統的一個Bug)。所以若為 false, 則在 app 對象中定義一個新函數 loginedCb(),供 app.onLaunch() 中異步獲取到登錄態信息后回調(在本文第四點有特意提過)。而 loginedCb() 方法要做的也是把全局控制字段賦值給頁面的控制字段;

    代碼中出現的 wxsetData() 方法是在 /utils/wxapi.js 定義的,這里我們導入進來
    1// app.js
    2
    3import {
    4  wxsetData
    5} from './utils/wxapi.js';
    6
    7/**
    8* 獲取小程序注冊時返回的自定義登錄態信息(小程序頁面中調用)
    9* 主要是解決pageObj.onLoad 之后app.onLaunch()才返回數據的問題
    10*/

    11pageGetLoginInfo: function(pageObj) {
    12    var _this = this;
    13    return new Promise((resolve, reject) => {
    14      // console.log(_this.gData.logined);
    15      if (_this.gData.logined == true) {
    16        wxsetData(pageObj, {
    17          'logined': _this.gData.logined,
    18          'authsetting': _this.gData.authsetting,
    19          'userinfo': _this.gData.userinfo
    20        }).then(function(data) {
    21          //執行pageObj.onShow的回調方法
    22          (pageObj.authorizedCb && typeof(pageObj.authorizedCb) === 'function') && pageObj.authorizedCb(data);
    23          resolve(data);
    24        });  } else {
    25    /**
    26     * 小程序注冊時,登錄并發起網絡請求,請求可能會在 pageObj.onLoad 之后才返回數據
    27     * 這里加入loginedCb回調函數來預防,回調方法會在接收到請求后臺返回的數據后執行,詳看app.onLaunch()
    28     */

    29    _this.loginedCb = () => {
    30      wxsetData(pageObj, {
    31        'logined': _this.gData.logined,
    32        'authsetting': _this.gData.authsetting,
    33        'userinfo': _this.gData.userinfo
    34      }).then(function(data) {
    35        //執行pageObj.onShow的回調方法
    36        (pageObj.authorizedCb && typeof(pageObj.authorizedCb) === 'function') && pageObj.authorizedCb(data);
    37        resolve(data);
    38      });
    39    }
    40  }
    41});
    42},
    然后我們再封裝一個 pageOnLoadInit() 方法,也簡單說說方法的邏輯:
    • 調用上一步 pageGetLoginInfo() 方法,保證頁面拿到有效準確的登錄態信息;
    • 驗證登錄,同時通過參數來決定當前頁面初始化時是否需要校驗用戶授權;
    • 若用戶沒有授權,則從當前頁面跳轉到 /pages/auth/auth 頁面,auth 頁面就是一個授權按鈕,用戶點擊后彈窗提示用戶確認授權(小程序官方已修改只能通過點擊按鈕彈窗用戶授權);
    涉及到授權方面的我們放在后面討論:
    1// app.js
    2
    3/**
    4* 封裝小程序頁面的公共方法
    5* 在小程序頁面onLoad里調用
    6* @param {Object}  pageObj   小程序頁面對象Page
    7* @param {Boolean} needAuth  是否檢驗用戶授權(scope.userInfo)
    8* @return {Object}           返回Promise對象,resolve方法執行驗證登錄成功后且不檢驗授權(特指scope.userInfo)的回調函數,reject方法是驗證登錄失敗后的回調
    9*/

    10pageOnLoadInit: function(pageObj, needAuth = false) {
    11    var _this = this;
    12    return new Promise((resolve, reject) => {
    13      _this.pageGetLoginInfo(pageObj).then(function(res) {
    14        // console.log(_this.gData.logined);
    15        if (res.logined === true) {
    16          //登錄成功、無需授權
    17          resolve(res);      if (needAuth) {
    18        if (res.authsetting['scope.userInfo'] === false || typeof res.authsetting['scope.userInfo'] === 'undefined') {
    19          common.navigateTo('/pages/auth/auth');
    20        }
    21      }
    22
    23    } else {
    24      reject({
    25        'errMsg': 'Fail to login.Please feedback to manager.'
    26      });
    27    }
    28  });
    29});
    30},
    現在問題基本解決了,剩下的就是在每個小程序頁面中調用,只校驗登錄的邏輯在 Page.onLoad() 里面執行,下面以代碼寫在小程序頁面 /pages/mine/index/index.js 中為例:
    1// /pages/mine/index/index.js
    2
    3const app = getApp();
    4
    5/**
    6* 生命周期函數--監聽頁面加載
    7*/

    8onLoad: function(options) {
    9    var _this = this;app.pageOnLoadInit(this).then(function(res) {
    10  //這里寫驗證登錄成功后且無需驗證授權 需要執行的邏輯
    11  //若還需驗證授權成功才執行的邏輯需寫在onShow方法里面,并且這里pageOnLoadInit()第二個參數要為 true
    12
    13}, function(error) {
    14  //登錄失敗
    15  wx.showModal({
    16    title: 'Error',
    17    content: error.errMsg ? error.errMsg : 'Fail to login.Please feedback to manager.',
    18  })
    19  return false;
    20});
    21},
    到此,針對第一點——頁面登錄已經完成。

    針對第二點用戶授權的。先看看在 app.js 中封裝的 exeAuth() 方法,該方法就是統一授權與后臺接口的交互
    1  /**
    2   * [exeAuth 執行用戶授權流程]
    3   * @param  {[string]} loginKey  自定義登錄態信息緩存的key
    4   * @param  {[Object]} data      wx.getUserInfo接口返回的數據結構一致
    5   * @return {[Promise]}          返回一個Promise對象
    6   */

    7  exeAuth: function(loginKey, data) {
    8    var _this = this;return new Promise((resolve, reject) => {
    9  wxapi('request', {
    10    'method': 'POST',
    11    'url': _this.gData.api.request + '/api/User/thirdauth',
    12    'header': {
    13      'Content-type': 'application/x-www-form-urlencoded',
    14    },
    15    'data': {
    16      'platform': 'miniwechat',
    17      'token': _this.gData.userinfo.token,
    18      'encryptedData': data.encryptedData,
    19      'iv': data.iv,
    20    }
    21  }).then(function(res) {
    22    //當服務器內部錯誤500(或者其它目前我未知的情況)時,wx.request還是會執行success回調,所以這里還增加一層服務器返回的狀態碼的判斷
    23    if (res.statusCode === 200 && res.data.code === 1) {
    24      //更新app.gData中的數據
    25      _this.gData.authsetting['scope.userInfo'] = true;
    26      _this.gData.userinfo = res.data.data.userinfo;
    27
    28      //更新自定義登錄態的緩存數據,防止再次進入小程序時讀取到舊的緩存數據,這里讓它異步執行即可,
    29      //倘若異步執行的結果失敗,直接清除自定義登錄態緩存,再次進入小程序時系統會自動重新登錄生成新的
    30      wxapi('setStorage', {
    31        'key': loginKey,
    32        'data': res.data.data.userinfo
    33      }).catch(function(error) {
    34        console.warn(error.errMsg);
    35        wxapi('removeStorage', {
    36          'key': loginKey
    37        });
    38      });
    39
    40      resolve(res.data.data.userinfo);
    41    } else {
    42      return Promise.reject({
    43        'errMsg': res.data.msg ? 'ServerApi error:' + res.data.msg : 'Fail to network request!'
    44      });
    45    }
    46  }).catch(function(error) {
    47    reject(error);
    48  });
    49});
    50  },
    要調用上述授權方法的地方必不可少的就是 /pages/auth/auth 統一授權頁面了,對于其它可能用到的地方我們之后也可直接調用,我們來看看 /pages/auth/auth 的 bindGetUserinfo() type = getUserInfo 按鈕的回調函數:
    1/**
    2* getUserinfo回調函數
    3*/

    4bindGetUserinfo: function(e) {
    5    var data = e.detail;
    6    if (data.errMsg === "getUserInfo:ok") {
    7      app.exeAuth('loginInfo', data).then(function(res) {
    8        var pages = getCurrentPages();
    9        var prevPage = pages[pages.length - 2]; //上一個頁面    prevPage.setData({
    10      'userinfo': res,
    11      'authsetting.scope\\.userInfo': true  這里請注意反斜杠轉義,'scope.userInfo'被看做一個完整的鍵名
    12    }, function() {
    13      wx.navigateBack({
    14        delta: 1
    15      });
    16    });
    17
    18  }).catch(function(error) {
    19    console.error(error);
    20    wx.showModal({
    21      title: 'Error',
    22      content: error.errMsg,
    23    })
    24  });
    25} else {
    26  wx.showModal({
    27    title: 'Warning',
    28    content: 'Please permit to authorize.',
    29    showCancel: false
    30  })
    31  return false;
    32}
    33},
    最后像上面登錄一樣,在 app.js 里封裝一個 pageOnShowInit() 供需要授權的頁面調用:
    1/**
    2* 封裝小程序頁面的公共方法
    3* 在小程序頁面onShow里調用
    4* @param {Object}  pageObj   小程序頁面對象Page
    5* @return {Object}           返回Promise對象,resolve方法執行驗證授權(特指scope.userInfo)成功后的回調函數,reject方法是驗證授權失敗后的回調
    6*/

    7pageOnShowInit: function(pageObj) {
    8    var _this = this;
    9    return new Promise((resolve, reject) => {
    10      /**
    11       * 這里通過pageObj.data.authsetting == (null || undefined)
    12       * 來區分pageObj.onLoad方法中是否已經執行完成設置頁面授權列表(pageObj.data.authsetting)的方法,
    13       *
    14       * 因為如果已經執行完成設置頁面授權列表(pageObj.data.authsetting)的方法,并且獲取到的授權列表為空的話,會把pageObj.data.authsetting賦值為
    15       * 空對象 pageObj.data.authsetting = {} ,所以pageObj.data.authsetting倘若要初始化時,請務必初始化為 null ,不能初始化為 {},切記!
    16       */

    17      if (pageObj.data.authsetting === null || typeof pageObj.data.authsetting === 'undefined') {
    18        /**
    19         * pageObj.onLoad是異步獲取用戶授權信息的,很大可能會在 pageObj.onShow 之后才返回數據
    20         * 這里加入authorizedCb回調函數預防,回調方法會在pageObj.onLoad拿到用戶授權狀態列表后調用,詳看app.pageOnLoadInit()
    21         */

    22        pageObj.authorizedCb = (res) => {
    23          if (res.authsetting['scope.userInfo'] === true) {
    24            //授權成功執行resolve
    25            resolve();
    26          } else {
    27            reject();
    28          }
    29        }
    30      } else {
    31        if (res.authsetting['scope.userInfo'] === true) {
    32          //授權成功執行resolve
    33          resolve();
    34        } else {
    35          reject();
    36        }
    37      }
    38    });
    39},
    由于授權多數情況下是從授權頁面跳轉回來的,所以這個方法設計在小程序頁面的 Page.onShow() 中調用,具體調用這里不貼代碼了,類似校驗登錄一樣。

PHP后端
由于文章篇幅原因,文中涉及的登錄和授權兩個后臺接口這里不貼源碼,有興趣了解可以到文章開頭的地址克隆項目,項目的安裝文檔也有具體說明。若有疑問,可以聯系本人。
結語
文章可能一定的條理性,望諒解~
純屬原創,轉載請注明出處,謝謝~

評分

參與人數 1浮云 +5 收起 理由
w余永新35 + 5 很給力!

查看全部評分

分享至 : QQ空間
2 人收藏
大佬,我是新手入坑,最近看了看官方文檔,但是感覺要自己開始從零寫一個,有點沒有頭緒啊,大佬能不能指導一下啊:)嘻嘻嘻
寫的很詳細,學習了
寫的很好
2019-4-16 13:41:17 w鹹魚86 攻城獅
5#
P3terRabb1t 發表于 2019-3-29 09:23
大佬,我是新手入坑,最近看了看官方文檔,但是感覺要自己開始從零寫一個,有點沒有頭緒啊,大佬能不能指導 ...

想一個自己感興趣的小程序項目,然后慢慢著手去實現,實現過程中遇到的問題會成全你去學習未知的知識,這就是我目前能給你的建議。
發新帖
您需要登錄后才可以回帖 登錄 | 立即注冊
极速快3精准计划 mg电子网站有哪些 香港闪部3肖6码原装版 hi彩分分彩稳赚技巧 大熊猫宝乐 电子游戏平台网址大全 pc蛋蛋加拿大28软件下载 pk10免费永久计划app 极速pk10app开奖下载 彩宝app是骗局揭秘 老时时彩历史开奖记录