從 WeRequest 登錄態管理來聊聊業務代碼

在開發微信小程序以前,我的歷來沒有接觸過開發中涉及到第三方服務器交互的流程。在開發的過程自己卻是沒有什麼太大的意外,只是在維護服務器登錄狀態這一點很討厭。由於涉及到自身服務器的登陸狀態以及微信官方服務器登錄狀態三方的關係。 前端

下圖是微信登錄機制:ios

api-login.jpg

在這種場景下,我的很是關注的點在於: 如何可以無感知的進行登錄(而且無多餘請求)。微信的登錄狀態卻是還好解決,能夠利用 wx.checkSession 來進行斷定,可是在與後臺服務器交互時候,若是後臺交互中返回 HTTP 狀態碼 401 (未受權)或者其餘未登錄指示時候。則須要對其進行額外處理。 git

當時記得爲了優雅的解決這個問題,想了不少方案,也與一些夥伴討論過這個問題。雖然當時的確實現了無感知的登錄,可是要麼須要多請求服務器,要麼就是代碼上實現邏輯過於複雜,代碼維護。雖然不滿意,可是在當時也沒想到什麼很是好的解決方法。程序員

weRequest 自帶狀態管理的請求組件

後面通過老大的介紹,看到這個組件時,我頓時眼前一亮,這正是我所須要的解決方案,該方案的圖示以下:github

flow_login.png

只須要配置一些初始化項目,即可以直接拿去使用了。web

// 導入
import weRequest from 'we-request';

weRequest.init({
    // [可選] 存在localStorage的session名稱,且CGI請求的data中會自動帶上以此爲名稱的session值;可不配置,默認爲session
    sessionName: "session",
    // [可選] 請求URL的固定前綴;可不配置,默認爲空
    urlPerfix: "https://www.example.com/",
    // [必填] 觸發從新登陸的條件,res爲CGI返回的數據
    loginTrigger: function (res) {
        // 此處例子:當返回數據中的字段errcode等於-1,會自動觸發從新登陸
        return res.errcode == -1;
    },
    // [必填] 用code換取session的CGI配置
    codeToSession: {
        // [必填] CGI的URL
        url: 'user/login',
        // [可選] 調用改CGI的方法;可不配置,默認爲GET
        method: 'GET',
        // [可選] CGI中傳參時,存放code的名稱,此處例子名稱就是code;可不配置,默認值爲code
        codeName: 'code',
        // [可選] 登陸接口須要的其餘參數;可不配置,默認爲{}
        data: {},
        // [必填] CGI中返回的session值
        success: function (res) {
            // 此處例子:CGI返回數據中的字段session即爲session值
            return res.session;
        }
    },
    // [可選] 登陸重試次數,當連續請求登陸接口返回失敗次數超過這個次數,將再也不重試登陸;可不配置,默認爲重試3次
    reLoginLimit: 2,
    // [必填] 觸發請求成功的條件
    successTrigger: function (res) {
        // 此處例子:當返回數據中的字段errcode等於0時,表明請求成功,其餘狀況都認爲業務邏輯失敗
        return res.errcode == 0;
    },
    // [可選] 成功以後返回數據;可不配置
    successData: function (res) {
        // 此處例子:返回數據中的字段data爲業務接受到的數據
        return res.data;
    },
    // [可選] 當CGI返回錯誤時,彈框提示的標題文字
    errorTitle: function(res) {
        // 此處例子:當返回數據中的字段errcode等於0x10040730時,錯誤彈框的標題是「舒適提示」,其餘狀況下則是「操做失敗」
        return res.errcode == 0x10040730 ? '舒適提示' : '操做失敗'
    },
    // [可選] 當CGI返回錯誤時,彈框提示的內容文字
    errorContent: function(res) {
        // 此處例子:返回數據中的字段msg爲錯誤彈框的提示內容文字
        return res.msg ? res.msg : '服務可能存在異常,請稍後重試'
    },
    // [可選] 當出現CGI錯誤時,統一的回調函數,這裏能夠作統一的錯誤上報等處理
    errorCallback: function(obj, res) {
        // do some report
    },
    // [可選] 是否須要調用checkSession,驗證小程序的登陸態過時,可不配置,默認爲false
    doNotCheckSession: true,
    // [可選] 上報耗時的函數,name爲上報名稱,startTime爲接口調用開始時的時間戳,endTime爲接口返回時的時間戳
    reportCGI: function(name, startTime, endTime, request) {
        //wx.reportAnalytics(name, {
        //    time: endTime - startTime
        //});
        //request({
        //    url: 'reportCGI',
        //    data: {
        //        name: name,
        //        cost: endTime - startTime
        //    },
        //    fail: function() {
        //
        //    }
        //})
        console.log(name + ":" + (endTime - startTime));
    },
    // [可選] 提供接口的mock,若不需使用,請設置爲false。url爲調用weRequest.request()時的url。mock數據的格式與正式接口提供的數據格式一致。
    mockJson: {
        url1: require("../../mock1.json"),
        url2: require("../../mock2.json"),
        url3: require("../../mock3.json")
    }
    // [可選] 全部請求都會自動帶上globalData裏的參數
    globalData: function() {
        return {
            version: getApp().version
        }
    },
    // [可選] session本地緩存時間(單位爲ms),可不配置,默認不設置本地緩存時間
    sessionExpireTime: 24 * 60 * 60 * 1000,
    // [可選] session本地緩存時間存在Storage中的名字,可不配置,默認爲 sessionExpireKey
    sessionExpireKey: "sessionExpireKey"
})

export default weRequest;

使用時候直接拿到 weRequest 既可以使用編程

weRequest.request({
    url: 'order/detail',
    data: {
      id: '107B7615E04AE64CFC10'
    },
    method: 'GET'
}).then((data)=>{
    // 省略...
})

代碼淺析

簡單的介紹一下 weRequest 庫的實現機制, 在這裏代碼簡化一下,只會說明最主要調用的三個函數。json

  • requestHandler.request 管理請求,即每一次請求都要執行該函數
  • sessionManager.main 管理 session 狀態。session 的設置與刪除,同時也在第一次確認擁有 session 時設置標識符,即只會在第一次缺失登錄態或者錯誤時候纔會執行。
  • responseHandler.response 管理返回數據,對返回數據進行解析,若是沒有登錄態,刪除 session,從新請求,結合第二個 sessionManager.main 來作。
// requestHandler.request 方法
function request(obj: IRequestOption): any {
  return new Promise((resolve, reject) => {
    
    // 傳入 api 請求對象進行處理
    obj = preDo(obj);

    // 讀取 session, 若是 session 沒有問題。成功的話,進行業務請求
    sessionManager.main().then(() => {
        // 進行業務請求
        return doRequest(obj)
    }).then((res) => {

      // 對 返回的數據進行解析 responseHandler.response 方法
      let response = responseHandler(res as wx.RequestSuccessCallbackResult, obj, 'request');

      if (response != null) {
        // 返回請求結果
        return resolve(response);
      }
    }).catch((e) => {
        // 異常處理機制
        catchHandler(e, obj, reject)
    })
  })
}

// sessionManager.main 方法
function main() {
    return new Promise((resolve, reject) => {
       
       // 檢查登錄態並返回, 若是登錄態過時,直接登錄,登錄成功後返回成功
        return checkLogin().then(() => {

           // 若是登錄態 ok, 把 config.doNotCheckSession 設置爲 true。避免下次再次執行檢查
            return config.doNotCheckSession ? Promise.resolve() : checkSession()
        }, ({title, content}) => {
            errorHandler.doError(title, content);
            return reject({title, content});
        }).then(() => {
           // 對checkSession 進行檢查操做
            return resolve();
        }, ({title, content})=> {
            errorHandler.doError(title, content);
            return reject({title, content});
        })
    })
}

// responseHandler.response 方法
function response ( res: wx.RequestSuccessCallbackResult,
    obj: IRequestOption,
    method: "request") {

      if (res.statusCode === 200) {
        // ... 省略代碼
        
        // 登陸態失效,且重試次數不超過配置
        if (config.loginTrigger!(res.data) && 
        obj.reLoginCount !== undefined && 
        obj.reLoginCount < config.reLoginLimit!) {
     
            // 刪除session
            sessionManager.delSession();

            if (method === "request") {
              // 從新請求
                return requestHandler.request(obj as IRequestOption);
            }
        }
      }

}

咱們能夠利用結合官方網站的圖示進行代碼分析 axios

若是用戶歷來沒有登錄過期,或者 checkSession 過時:小程序

  • request 直接請求須要的 api
  • sessionManager.main 檢查登錄態,即 checkSession 是否過時
  • isSessionExpireOrEmpty 若是 session 過時或者爲空(當前爲空)
  • wx.login -> code2Session 登錄兩個服務器
  • 成功後,設置標識符 doNotCheckSession 繼續 request 請求

flow2.png

用戶登錄態未過時,再次打開小程序:

  • request 直接請求須要的 api
  • sessionManager.main 檢查登錄態,看到 doNotCheckSession
  • 繼續第一步 request 請求

flow1.png
若是是請求成功後的第二次請求,直接會取得內存中的 session,而並不是 getStorage,因此沒必要擔憂

用戶某次登錄後端,後端登錄態過時:

  • request 直接請求須要的 api
  • sessionManager.main 檢查登錄態,即 checkSession 是否過時
  • isSessionExpireOrEmpty 若是 session 過時或者爲空(當前爲不爲空)
  • 成功後,設置標識符 doNotCheckSession 繼續 第一步 request 請求
  • 後臺返回錯誤碼,經過 responseHandler.response 解析。發現錯誤,刪除session,重複請求。

flow3.png
提個點,必定要設定 reLoginCount 至少1次,不然該業務沒法完成。

new Promise 內部封裝異步操做

以前在寫關於異步代碼操做時候,一般是基於 axios 直接返回 api 請求響應數據,對其進行正常和錯誤處理。當時屢次異步操做從而返回正確與錯誤的流程卻不多進行梳理。若是在一次請求內有多個異步操做:代碼就會變得難以維護。事實上咱們能夠把 Promise 當作狀態機。只有在某些狀況下才會返回正確。

// 異步操做封裝
function asyncCompnent(opt: any) {
  return new Promise((resolve, reject) => {


    // 傳入的 opt 異步操做


    // 多個 異步操做, 在最後一個異步操做成功後執行
    reslove(result)


    // 多個 異步操做中的 catch, 在每一個錯誤中執行
    reject(error)
  })  
}


asyncComponent(data).then(result => {
  // 正常流程
}).catch(error => {
  // 錯誤流程
})

寫出如上的代碼,就能夠在不少業務項內進行操做,諸如某些操做有前置權限請求,或者某些錯誤代碼須要從新請求或者埋點等操做。

可能會有人認爲,在http 請求框架中都會有 interceptor 攔截器, 徹底用不到 new Promise 來判斷與操做。可是每每來講,攔截器對於代碼是全局的,若是是單單對於某些模塊,在攔截器中寫大量 if 判斷以及業務處理,這毫不是一件好事。由於場景上,業務的易變性使得全局代碼被大量修改不利於項目的維護,可是若是該方案使用不當,則又會形成業務代碼的可控性下降。

固然以上代碼也可使用 async 與 await 來處理,建議多研究一下 async 錯誤處理,這裏推薦兩篇關於 async 錯誤處理的博客(由於我的一直不喜歡 async 函數須要配合回調函數或者 Promise.reject 來處理錯誤,因此通常來講,我更多用 async 來處理非 api 請求的異步操做,這樣的話基本上不太須要處理錯誤)。
如何在Javascript中優雅的使用Async和Await進行錯誤的處理?
從不用 try-catch 實現的 async/await 語法說錯誤處理

以前在閱讀 《MobX Quick Start Guide》 時候,我看到一個公式

VirtualDOM = fn (props, state)

只要輸入等同的 屬性和狀態,獲得的必定是 相同的 VirtualDOM 數據。

可是我想說的是對於一個業務而言,若是不考慮界面美觀性,以及必要的中間狀態,我認爲符合如下公式:

前端業務封裝 = 管理 (交互狀態, 數據狀態, 配置項)

其中,結合交互狀態和數據狀態面向的是最終用戶,用戶看到怎樣的界面取決於前兩個。然後一個配置項是面向於開發者,你的代碼能究竟支持多少種場景。可以經過配置來減小多少的代碼量。

難道只要輸入等同的交互以及數據就能的到一樣的業務嗎?固然並不是如此,由於對於前端而言,始終有不知道的數據狀態。咱們只能經過防護式編程與錯誤處理來搞定不清楚的數據狀態(經過增長各類交互狀態來解決數據數據狀態未知狀況)。

對於 weRequest 這個庫而言,整個 微信的登錄態是保存在 storage 中,整個庫都在維護微信的登錄狀態(和後臺的交互狀態並無保存,只要出現沒有權限狀態時,就會刪除微信登錄狀態,從新login)。那麼除去代碼,整個的交互狀態就是被存到內存以及 Storage 中的 session,doNotCheckSession 。數據狀態是咱們須要請求的api配置以及咱們未知的後端狀態。

session, doNotCheckSession // 可變的交互狀態

weRequest.init({
  // 固定的交互狀態(配置項)
  // ...
})     

{
  url: '../'
} // 數據狀態

這裏也推薦了一篇關於前端 axios 從新請求的方案參考,相比於 weRequest 更加清晰:

axios請求超時,設置從新請求的完美解決方法

一樣對於咱們的業務代碼而言(組件內部實現),每每有些數據也是配置項目。若是你對於這三者清楚的瞭解而且管理的很好,那麼寫出來的業務代碼必定不會差。

"無狀態"的優點

在 request 代碼中很容易發現,代碼可以維護和後端的狀態並非由於持有了後端的 session,而是一種試錯機制,只要上一次請求和下一次請求之間的數據沒有變過,那麼在錯誤處理中重複請求就沒有問題。同時呢,雖然是有 session,doNotCheckSession 這個數據在,可是被移除到非業務中,只做爲交互使用。因此我在這裏想說的是,思考如何減小中間狀態,也就是銷燬,新建的模型更簡單。

在剛開始處理業務代碼時候,我老是較多處理 state,老是使用組件的顯隱來控制 Dialog,有時候填寫 form 表單很麻煩,由於當時組件的一些機制不夠完善,在處理完一個form後,reset並不能清除 上一次數據的驗證錯誤,須要多寫一部分代碼來搞定開發,後來開始轉變了,對於大部分場景而言,不如直接銷燬,新建,無需管理中間態。

其實前端業務中,其實不少這樣的例子,處理子組件與父組件的關係,甚至來講架構端,把單頁面應用改爲基於業務的多頁面應用,也是一種銷燬,新建的模式。利用瀏覽器自己的機制去除大部分的中間態。

當咱們費勁心思去維護一箇中間狀態的時候,利用各類工具提高性能,不妨多思考如下,去除是不是一種更簡單的方案。

之因此會有這樣的感慨: 是由於在我剛畢業時曾經作過一個需求,裏面有 5,6 個複雜的功能點,如今還要增長一個功能點。可是當時徹底沒法經過增長代碼來解決問題,必須把代碼拆分重組才能夠搞定。遇到這種事情,有小夥伴可能會想,是否是當時的代碼寫的很差。其實並非該代碼寫的很差,而是以前的代碼寫的至關好,契合的很是好,徹底不知道怎麼搞定,初出茅廬的我徹底沒法控制(須要對全部功能點通盤考慮,複雜度很高),所以,我在這個需求上徹底失控了。因此,能契合複雜的代碼,考慮到各類多是能力。能解析複雜的的代碼,作必定的犧牲決策,化繁爲簡,也是一種能力。前者的能力是我的能力的強大,是不可複製和替代的。後者則是讓團隊實現更加簡單,快速的實現各類功能。對於一個成熟的程序員而言,二者的提高都是很重要的。

若是對於前端來講,無狀態的優點是簡單的話,web 後端的無狀態的利好就更多了,能夠經過外部擴展實現水平擴容,其實質也是把交互狀態(用戶數據)移除到其餘介質上來實現請求能夠打到不一樣的服務器上,而不是單服務器。同時實現了每次請求都是獨一無二的,徹底不須要考慮中間狀態的遷移,有利於開發速度與正確性。

weRequest 源碼結構解析

weRequest 是一個很是小而美的庫,代碼很是簡單幹練,我我的很是喜歡他的源碼結構,因此列出來:

  • api

    • getConfig.ts 獲取配置信息,把代碼中配置的數據導出
    • getSession.ts 獲取 session
    • init.ts 初始化設置配置 同時讀取 storage 中的 session 與 session 過時時間 放入呢次
    • login.ts 直接調用 sessionManager.main()
    • request.ts 直接調用 requestHandler.request(obj)
    • setSession.ts 直接 setSession(內部接管了,不建議調用,多是一個用戶兩個小程序之間的特殊需求)
    • uploadFile.ts 上傳文件
  • module

    • cacheManager.ts api 基於 api 與參數 進行緩存管理,目前沒有過時時間,只適合不變化的數據
    • catchHandler.ts 異常處理
    • durationReporter.ts 耗時上報
    • errorHandler.ts 錯誤處理
    • mockManager.ts mock 假數據接口
    • requestHandler.ts 請求處理,格式化,上傳文件等
    • responseHandle.ts 響應處理,從請求等
    • sessinManager.ts session 管理,對於登錄態進行控制,設置與刪除
  • store

    • config.ts 默認配置,在 init 時候會使用 Object.assign 來進行默認配置覆蓋
    • status.ts session, 在 init 時候會從 storage 中設置
  • typings 小程序的接口 .d.ts
  • util

    • loading.ts 請求中顯示 loading 配置
    • url.ts 根據傳入的對象來構造 get 請求url
  • index.ts 全部 api 的導出
  • interface.ts weRequest 接口
  • version.ts 版本信息

題外話

很難有人能一次搞定業務需求,只有在它出現後,才知道什麼他是它最須要的。業務代碼也同樣。

同時 weRequest 不是萬能的,它符合大衆的需求,但不必定符合每一個業務的需求。你也能夠根據代碼改造甚至改進。

鼓勵一下

若是你以爲這篇文章不錯,但願能夠給與我一些鼓勵,在個人 github 博客下幫忙 star 一下。
博客地址

參考

weRequest
如何在Javascript中優雅的使用Async和Await進行錯誤的處理?
從不用 try-catch 實現的 async/await 語法說錯誤處理
axios請求超時,設置從新請求的完美解決方法

相關文章
相關標籤/搜索