JavaScript, Vue

2022.06.17

使用 BroadcastChannel 讓 sessionStorage 的資料可以跨分頁同步讀取

需求

公司最近有一個登入的需求,為了安全性的問題,希望使用者即使沒登出,關閉視窗也會清除 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」(因為沒按登出),進入跟開新分頁一樣的條件式。但又不會進入監聽事件,導致畫面一片空白。

因此我碰到的問題是,該怎麼區分以下兩者情境:

  1. 使用者開新分頁
  2. 使用者沒登出就關閉網站,再重新打開

最後我用了 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」。

改用 BroadcastChannel 發現一片天

我在 FB 前端社團詢問後,非常謝謝所有朋友的回應與建議,特別感謝司馬光(有種夢回古代的感覺 😂),讓我認識了 BroadcastChannel 這個 API,而且他很貼心的寫了一個範例給我參考,我就從他這個範例做了修改,實現了跨分頁讀取 sessionStorage 的功能。

網友司馬光的 demo code

GitHub - simagu/sessionstorage-test

GitHub – simagu/sessionstorage-test

Contribute to simagu/sessionstorage-test development by creating an account on GitHub.

https://github.com/simagu/sessionstorage-test

BroadcastChannel 核心語法

BroadcastChannel 顧名思義,就是一個廣播的頻道。只要我們的頁面在同一個頻率(同網域),就能透過對講機發送 / 接收訊息。

建立頻道

▼ 可以自由建立多組頻道

const authChannel = new BroadcastChannel('AUTH_CHANNEL')

發送與接收

▼ 發送訊息到剛剛建立的頻道

authChannel.postMessage({ action: 'reqLogin', path: to.path })

▼ 從頻道接收訊息

authChannel.onmessage = (event) => {
	console.log(event.data)
}

關閉頻道

▼ 如果不想再從這個頻道接收內容,可以關起來

authChannel.close()

以上,核心語法非常簡單!!但就不用再透過 localStorage 當作橋樑了。

利用 BroadcastChannel 同步 session Storage 資料

最後,放上全部的語法:

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)
Line 39: 登出時,新增一個 resLogoutsessionStorage,判斷要不要關閉 broadcastChannel,避免登出後還會繼續廣播。

關於 Safari 瀏覽器不支援 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

歡迎給我點鼓勵,讓我知道你來過 :)

36
15
8
4
guest
0 則留言
Inline Feedbacks
View all comments
Copyright (C) MUKI space* / Reborn Theme All Rights Reserved.