單頁應用與多頁應用
傳統的 web 項目中,多數是多頁應用,即每次頁面跳轉的時候,後臺服務器都會給返回一個新的 html
和與之相關的 css
和 js
資源,例如熟悉的 JSP
,這種類型的網站也就是多頁網站,也叫做多頁應用。這種應用最明顯的缺點就是頁面切換的響應速度慢、靜態資源的重複加載等。
而單頁應用(SPA)在頁面跳轉的時候,並不需要請求新的 html
、css
和 js
等資源,這樣就節省了很多 http
請求,加快了頁面切換的速度。但也帶來了一個明顯的缺點就是第一次加載應用的時候就要獲取到所有的頁面,導致首屏加載時間較長。幸運的是,服務端渲染(SSR)技術能夠很好的解決這個問題。
綜合來看,對於中小規模的應用,在如今強調前後端分離的時代,單頁應用是很好的選擇。接下來的分享是我在利用 vue
、vue-router
和 vuex
開發單頁應用中遇到的一些問題以及對應的解決方法。
單頁應用的訪問權限
單頁應用不需要爲每個頁面都新建一個 html
文件和若干個 css
和 js
文件,頁面切換不需要後端的控制,看起來貌似對前端開發人員很友好,但實際情況是,對於大多數單頁應用而言,前端開發人員需要考慮的東西更多了。主要在於頁面訪問權限的控制,例如在哈希模式下,用戶可以手動改變url的哈希值來訪問不同的路由,但在實際應用中,對於用戶的這種操作是需要進行判斷控制的,例如,某些路由需要登錄之後纔有權限訪問、某些路由對應的頁面需要特殊的權限才能訪問等。
我將單頁應用中的頁面類型分爲以下兩種:
- 登錄頁面
- 需要登錄之後才能訪問的頁面
登錄頁面的權限控制
登錄頁面的特殊之處在於,登錄頁面只應該在用戶需要進行登錄操作的時候纔對其可見,也就是說,若用戶的登錄狀態沒有過期,則用戶不能訪問登錄頁面的路由。
需要登錄之後才能訪問的頁面的權限控制
在 vue
應用中,通常會在 mounted
方法中發送 ajax
請求獲取數據。但在單頁應用中,用戶可以改變 url 中的哈希值來訪問不同的路由,但對於某些頁面,例如個人中心等,需要用戶登錄之後才能獲取到有效的數據進行展示。雖然有一種簡單的處理方法:允許用戶訪問這個路由的頁面,但 ajax
獲取不到有效數據,但顯然不是最好的解決辦法。比較好的做法是,通過 vue-router
的生命週期函數判斷進入這些路由前,用戶是否已經登錄,沒有登錄則重定向到登錄頁面的路由。
router.beforeEach((to, from, next) => {
if (to.path == '/login') { // 訪問登錄頁面的路由
if (login) { // 登錄狀態有效,重定向到首頁
next('/');
} else { // 需要登錄
next();
}
} else { // 訪問其他的路由
if (login) { // 登錄狀態有效,允許訪問
next();
} else { // 需要登錄,重定向到登錄頁面
next('/login');
}
}
})
登錄狀態
經過上面的分析,問題最終都指向了 前端如何判斷用戶的登錄狀態。
通常情況下,前端只負責發送請求,根據響應顯示頁面的不同內容,而後端會通過 sessionId
或者 token
來判斷用戶的登錄狀態是否過期。那麼前端有哪些辦法(哪些地方) 記錄(保存)用戶的登錄狀態是否有效這個數據呢?
客戶端存儲
sessionStorage
、localStorage
和 cookie
是前端常用的客戶端存儲方法。
cookie
作爲讓後端有記憶能力的解決辦法,通常會攜帶一些後端判斷請求是來着於誰的信息,如 sessionId
/ token
,如前面所說,這些信息還是由後端判斷是否過期,一般不會在 cookie
中直接存儲登錄狀態是否有效。
sessionStorage
存儲在瀏覽器中,可以在用戶登錄成功之後利用 setItem
保存一個屬性,在路由的生命週期函數 beforeEach
中通過 getItem
判斷是否已經登錄。實現簡單,但存在的問題較多:
-
sessionStorage
本身的過期時間決定了用戶的登錄狀態與sessionStorage
的有效期同步。例如關閉瀏覽器之後都得重新登錄,而不管後端是否設置了cookie
的expire
屬性。- 後端的登錄狀態過期了,但
sessionStorage
還有效,導致用戶本能訪問頁面,只是沒有數據。用戶只能通過一些操作讓sessionStorage
失效(關閉瀏覽器窗口重新進) - 後端的登錄狀態有效,但
sessionStorage
沒有了,導致用戶需要重新登錄。
- 後端的登錄狀態過期了,但
-
sessionStorage
是可讀可寫的,在瀏覽器的調試工具中可以輕鬆執行clear
等方法,也能輕鬆讀取到setItem
的值,低安全性也決定了sessionStorage
不能存儲重要信息。
localStorage
除了過期時間與 sessionStorage
不一樣,其他的功能都類似。
應用存儲
在應用中存儲,通常就是在 vuex
中記錄用戶是否登錄。在路由的生命週期函數 beforeEach
中讀取這個數據來判斷用戶的登錄狀態是否有效。那麼問題來了:
- 登錄成功之後更新這個數據沒問題,還需要什麼時候更新這個數據呢?不更新就一直處於登錄有效的狀態
- 直接刷新頁面之後 vuex 清空了,若後端的登錄狀態是有效的,也要重新登錄嗎?
我的解決辦法是:通過發送一個請求判斷登錄狀態是否有效。對於獲取數據的請求,若返回值中的信息代表需要重新登錄了,則清空 vuex
並重定向到登錄頁面。
來看看利用 vuex
記錄登錄狀態時會遇到的所有情況以及處理辦法:
router.beforeEach(async (to, from, next) => {
if (to.path == '/login') { // 訪問登錄頁面的路由
if (store.login) {
// 登錄狀態有效,重定向到首頁
// 這種情況通常是用戶手動修改了 url 中的 hash
next('/');
} else {
// 發送 ajax 判斷登錄狀態是否有效
const login = await ajax();
if (login) {
// 修改 store,重定向到首頁
store.commit('login', true);
next('/');
} else {
next();
}
}
} else { // 訪問其他的路由
if (store.login) {
// 登錄狀態有效,允許訪問
next();
} else {
// 還沒有經過登錄頁面就訪問其他路由,這種情況通常也是用戶手動修改了 url 中的 hash
// 發送 ajax 判斷登錄狀態是否有效
const login = await ajax();
if (login) {
// 修改 store,允許訪問
store.commit('login', true);
next();
} else {
next('/login');
}
}
}
})
另外,對於每一個獲取數據的請求,若後端的返回值中代表用戶的登錄狀態已經過期,則改變 vuex
中的登錄狀態並重定向到登錄頁:
store.commit('login', false);
location.href = '/#/login';
location.reload();