SPA登錄狀態管理簡單方案

4 人贊了文章

涉及到用戶狀態的應用一定會遇到的一個問題就是登錄狀態的管理,個人在多個項目中對這個問題的方案進行了許多嘗試和思考,形成了一個比較基礎的管理思路,在這裡做一個分享。以後有更深入的理解將再作補充,有不成熟不全面的地方還請指出。

分析

業務中不同類型的應用狀態的管理各有其特點,用戶的登錄狀態(包括一些用戶的基本信息如用戶名頭像等)管理過程中需要考慮的關鍵點如下:

  1. SPA 中用戶的登錄狀態通常是一個全局性的狀態,在應用的各個部分都可能需要訪問到。
  2. Web 應用中用戶的登錄狀態從根本上是由後端來維護的,而以 Http 為唯一通信協議的 Web 應用中後端不能主動推送數據,這就要求前端設置一定的同步機制,在合適的時機請求伺服器,同步用戶的登錄狀態。

考慮可能需要訪問用戶登錄狀態的情況,這裡的登錄驗證指的是驗證本地保存的登錄狀態

  • 路由級:SPA 中限制某些界面必需登錄後才能訪問,例如用戶的個人信息。在進入該界面前應驗證用戶的登錄狀態,若未登錄,將用戶引導至登錄界面。路由級的登錄狀態本質上還是由於某些界面上通過 Ajax 載入數據時要求用戶的許可權,因此實際上也可以載入在有許可權要求的數據時進行驗證並引導登錄,但這樣做無疑會增加很多相同的登錄狀態驗證邏輯,也不符合交互上的邏輯——我們需要限制未登錄用戶進入這個界面而不是允許他進入界面而不能看到數據。
  • 操作級:在某些不需要登錄的界面中也可能存在一些操作,要求用戶先登錄才能進行操作。實際上存在某些操作就是為了觸發路由跳轉的,這樣的操作的登錄驗證可以依賴於路由級的登錄驗證,也可以根據需要為其單獨添加登錄狀態驗證邏輯,以提供更友好的交互,例如直接將按鈕設置為灰色並禁止操作的觸發。

由於以上兩種登錄狀態的驗證都是使用本地保存的登錄狀態,可能會與後端維護的登錄狀態不一致,常見的就是登錄超時的情況了。

再考慮幾種同步登錄狀態的時機

  • 程序引導(初始化)時:通常情況下,用戶的刷新操作會導致 SPA 重新引導,這時候我們在應用內部維護的登錄狀態就會丟失,需要請求後端,獲取登錄狀態。程序引導時可能發生的登錄狀態轉移可能為:未登錄 -> 已登錄,未登錄 -> 未登錄
  • 登錄操作和註銷操作:需要再次注意,用戶的登錄狀態從根本上是由後端維護的,登錄和註銷這類操作只是發出一個請求,要求後端改變登錄狀態,然後將後端的新狀態同步到本地;這個過程中可能由於用戶密碼不匹配,伺服器的錯誤等原因導致後端狀態更改失敗。登錄操作可能發生的登錄狀態轉移可能為:未登錄 -> 已登錄,未登錄 -> 未登錄;註銷操作可能發生的的登錄狀態轉移可能為:已登錄 -> 已登錄,已登錄 -> 已登錄
  • 其他需要用戶許可權的介面:訪問需要用戶許可權的操作或數據介面時,後端應作一個驗證,確認當前的用戶是否登錄,時候有相應的操作許可權。當後端發現這個用戶未登錄時,就會返回一個對應的錯誤信息,提示前端這個用戶未登錄。在前端已經針對用戶操作進行了本地登錄狀態驗證的情況下,這種未登錄的情況就是由於登錄超時機制引起的。前端程序在獲取到這個錯誤信息的時候更新本地的登錄狀態,實際上就是被動地進行了一次登錄狀態的同步。訪問介面可能導致的登錄狀態轉移可能為:已登錄 -> 未登錄,已登錄 -> 已登錄

設計

首先設計一下後端應提供的 API:

  • 登錄/註銷介面:根據需要傳用戶密碼等信息,返回登錄信息(包括用戶基本信息)
  • 同步登錄狀態介面:請求時不需要傳參數(或只傳用戶憑證),返回登錄信息
  • 其他需要用戶許可權的介面:後端加一個類似過濾器的模塊,對需要登錄而未登錄的介面訪問,返回未登錄錯誤信息

再設計一下前端應用的登錄狀態同步機制,參考分析中提到的同步時機,先設計一個比較粗暴的同步機制:

  1. 程序引導時,訪問同步狀態的介面,同步登錄狀態信息
  2. 登錄或註銷操作後,在代碼中引導頁面刷新,則回到情況 1
  3. 訪問其他需要用戶許可權的介面,收到後端返回的未登錄的初錯誤後,在代碼中引導頁面刷新,回到情況 1

要處理訪問其他需要用戶許可權的介面引發的登錄狀態的轉移(已登錄 -> 未登錄),意味著調用這些介面的過程中要加入一個處理邏輯,用於攔截後端返回的未登錄的錯誤。為每一個介面的調用過程單獨設置攔截&處理邏輯顯然是不可能的,我們需要統一處理未登錄的錯誤。這麼說有點抽象,舉個例子吧。

以我使用比較多的 axios 為例,axios 提供了一個 interceptors 功能,用於攔截 request 和 response,加入一些處理邏輯,可以將一些全局性的錯誤處理邏輯放在這裡。示例代碼中就採用了引導刷新的方式處理未登錄的錯誤

這個設計裏通過刷新把後面兩種情況中的登錄狀態同步的過程都交給程序引導時同步,乍一看有點蠢,畢竟 SPA 一個重要的特徵就是無刷新或者少刷新頁面,但是具體應用到業務中會發現這種機制的適應性還是很好的。對於那些希望刷新後仍保留的應用狀態(如列表頁碼,業務過程的步驟序號),可以放到路由的 hash 裏,在程序引導時再讀取。

進階設計

上面給出的簡單的同步機制已經能滿足很多的業務需求了,但是不夠靈活。在 SPA 中,還是力求使用一種無需刷新的同步機制。

對於登錄和註銷的操作,成功後一般會跳轉到特定界面,或者不進行跳轉但重新載入本界面的數據,可以直接寫到登錄和註銷操作結果的處理邏輯中。

對於訪問其他需要用戶許可權的介面引發的登錄狀態的轉移(已登錄 -> 未登錄),仍希望做一個統一的處理,但是這裡會有一個問題:

首先在程序結構設計的過程中一般把與後端進行 Ajax 交互的邏輯封裝在一起,作為獨立的一層,比如我這裡叫 Service 層吧。Service 層可能會在多個 SPA 中復用,因此只能被其他模塊調用,不能依賴其他模塊。這就意味著 Service 中不能訪問其他的模塊,也就不能主動通知其他模塊,改變登錄狀態。因此只能是在其他模塊調用 Service 層提供的非同步函數,在回調中處理數據和錯誤。

其他模塊與 Service 層對接時,通常是調用 Service 層的某一個函數。如果其他模塊中包含調用 Service 層函數的代碼是分散的,處理數據和錯誤的邏輯也就是分散的,那麼就無法做到統一處理未登錄等全局錯誤。以 Vuex 為例,調用 Service 層函數的代碼分散在不同的 Action 中

問題核心在於我們希望達到統一處理登錄失效並且避免負責 AJAX 請求的模塊依賴其他模塊。想要將依賴反轉過來,可以使用事件模型處理。將登錄狀態失效作為一個事件,當其他介面返回登錄狀態失效的消息時,負責 AJAX 請求的模塊發出登錄失效的事件。在上述應用設計結構中,可以讓狀態管理模塊去監聽登錄失效事件,在登錄失效時將本地的用戶狀態設置為離線狀態。

推薦閱讀:

相關文章