2023-01-16
Vue3 使用 Vite 整合 axios 攔截器與 axios proxy
Vue
公司最近有一個登入的需求,為了安全性的問題,希望使用者即使沒登出,關閉視窗也會清除 token
等相關資料,因此在這個前提下,我使用了 sessionStorage
儲存使用者資訊。
不過我又希望在登入的情況下,使用者「1. 直接開新分頁」或是「2. 按右鍵選擇在新分頁中開啟連結」時,也能自動登入,不用重新打帳密。
一開始,我是用 localStorage
,帶入一個 getSeesionStorage
的識別,開新分頁時,如果有登入的資料,就觸發監聽事件,把 getSeesionStorage
傳到新分頁。
也就是說,靠著監聽 storage
事件,讓 localStorage
當作一個橋樑,去串接兩邊的資訊。
router.beforeEach((to, from, next) => { function storageListener () { window.addEventListener('storage', function (e) { if (e.key === 'getSessionStorage') { localStorage.setItem('sessionStorage', JSON.stringify(sessionStorage)) localStorage.removeItem('sessionStorage') } else if (e.key === 'sessionStorage' && !sessionStorage.length) { var data = JSON.parse(e.newValue) for (var key in data) { sessionStorage.setItem(key, data[key]) } next() } }) } const getSessionStorage = localStorage.getItem('getSessionStorage') const getToken = sessionStorage.getItem('token') if (getSessionStorage && getToken) { storageListener() next() } else if (getSessionStorage && !getToken) { localStorage.setItem('getSessionStorage', Date.now()) storageListener() } else { if (to.path !== '/login') next('/login') else next() } })
用 localStorage
的寫法的確可以解決,開新分頁時要重新登入的問題。
但在這個情境下會有問題:
「如果使用者沒登出,就直接關閉視窗,或是關閉該網域的所有分頁。」
那麼重新開啟網站時,就會因為「沒有清除 localStorage
」(因為沒按登出),進入跟開新分頁一樣的條件式。但又不會進入監聽事件,導致畫面一片空白。
因此我碰到的問題是,該怎麼區分以下兩者情境:
最後我用了 beforeunload
這個監聽事件,監聽如果使用者關閉網站,就把 localStorage
清除,算是解決了這個難題:
router.beforeEach((to, from, next) => { function storageListener () { window.addEventListener('storage', function (e) { localStorage.setItem('getSessionStorage', Date.now()) if (e.key === 'getSessionStorage') { localStorage.setItem('sessionStorage', JSON.stringify(sessionStorage)) localStorage.removeItem('sessionStorage') } else if (e.key === 'sessionStorage' && !sessionStorage.length) { var data = JSON.parse(e.newValue) for (var key in data) { sessionStorage.setItem(key, data[key]) } next() } }) } const getSessionStorage = localStorage.getItem('getSessionStorage') const getToken = sessionStorage.getItem('token') if (getSessionStorage && getToken) { storageListener() next() } else if (getSessionStorage && !getToken) { localStorage.setItem('getSessionStorage', Date.now()) storageListener() } else { if (to.path !== '/login') next('/login') else next() } window.addEventListener('beforeunload', function (e) { e.preventDefault() localStorage.removeItem('getSessionStorage') }) })
不過用 beforeunload
也不是個好解法,例如手機可能就無法觸發該行為,而且有朋友也提到:「beforeunload
不被建議拿來偷塞 side effect」。
我在 FB 前端社團詢問後,非常謝謝所有朋友的回應與建議,特別感謝司馬光(有種夢回古代的感覺 😂),讓我認識了 BroadcastChannel
這個 API,而且他很貼心的寫了一個範例給我參考,我就從他這個範例做了修改,實現了跨分頁讀取 sessionStorage
的功能。
GitHub – simagu/sessionstorage-test
Contribute to simagu/sessionstorage-test development by creating an account on GitHub.
https://github.com/simagu/sessionstorage-test
BroadcastChannel
顧名思義,就是一個廣播的頻道。只要我們的頁面在同一個頻率(同網域),就能透過對講機發送 / 接收訊息。
▼ 可以自由建立多組頻道
const authChannel = new BroadcastChannel('AUTH_CHANNEL')
▼ 發送訊息到剛剛建立的頻道
authChannel.postMessage({ action: 'reqLogin', path: to.path })
▼ 從頻道接收訊息
authChannel.onmessage = (event) => { console.log(event.data) }
▼ 如果不想再從這個頻道接收內容,可以關起來
authChannel.close()
以上,核心語法非常簡單!!但就不用再透過 localStorage
當作橋樑了。
最後,放上全部的語法:
const authChannel = new BroadcastChannel('AUTH_CHANNEL') const saveUserInfo = data => { sessionStorage.setItem('path', data.path) sessionStorage.setItem('token', data.token) } const pushSignInRequest = () => { authChannel.postMessage({ action: 'reqLogin', path: to.path }) if (to.path !== '/login') next('/login') else next() } const receiveSiteMessage = e => { const data = e.data switch (data.action) { case 'reqLogin': { const getToken = sessionStorage.getItem('token') if (getToken !== null) { authChannel.postMessage({ action: 'resMember', path: data.path, token: sessionStorage.getItem('token'), accountId: sessionStorage.getItem('accountId'), nick_name: sessionStorage.getItem('nickname'), office_name: sessionStorage.getItem('officeName') }) } } break case 'resMember': saveUserInfo(e.data) break } } authChannel.onmessage = e => receiveSiteMessage(e) window.setTimeout(() => { if (sessionStorage.getItem('resLogout')) { // console.log('logout') authChannel.close() sessionStorage.removeItem('token') if (to.path !== '/login') next('/login') else next() } else { const getToken = sessionStorage.getItem('token') if (!getToken || getToken === 'null') { // console.log('no token') pushSignInRequest() } else { // console.log('has token') const path = sessionStorage.getItem('path') if (to.path === '/login') next(path) else next() } } }, 500)
resLogout
的 sessionStorage
,判斷要不要關閉 broadcastChannel
,避免登出後還會繼續廣播。
很開心的做完後,發現 Safari 居然不支援 BroadcastChannel 😂,所以在 Safari 瀏覽器 (含 iPhone)都沒辦法使用。
但我已經很滿意這個完成度了,所以最後只做了簡單的偵測,假設使用者用的是 Safari 瀏覽器,就是很單純的登入登出功能,只要重開分頁就是要輸入帳密。
if ((navigator.userAgent.indexOf('Safari') !== -1) && (navigator.userAgent.indexOf('Chrome') === -1)) { const getToken = sessionStorage.getItem('token') if (getToken) { next() } else { if (to.path !== '/login') next('/login') else next() } } else { // BroadcastChannel API }
這次學到了一個新技術好開心,卡了一個禮拜的 login 功能,算是能有一個初步的交代了 xD。
社團也有朋友分享了一篇很棒的文章,整理了跨分頁通訊的相關方法,這邊也分享給各位!
面试官:前端跨页面通信,你知道哪些方法?
在浏览器中,我们可以同时打开多个Tab页,每个Tab页可以粗略理解为一个“独立”的运行环境,即使是全局对象也不会在多个Tab间共享。然而有些时候,我们希望能在这些“独立”的Tab页面之间同步页面的数据、信息或状态。 正如下面这个例子:我在列表页点击“收藏”后,对应的详情页按钮会…
https://juejin.cn/post/6844903811232825357