公司最近有一個登入的需求,為了安全性的問題,希望使用者即使沒登出,關閉視窗也會清除 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