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

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

MUKI AI Summary

公司希望使用者關閉視窗時清除 token 資料,於是使用 sessionStorage 儲存資訊。為了讓使用者在新分頁自動登入,初步使用 localStorage 監聽 storage 事件,但遇到使用者未登出直接關閉視窗的問題。最後改用 BroadcastChannel API,成功實現跨分頁同步 sessionStorage 資料。

BroadcastChannel 允許同網域的頁面互相傳遞訊息,避免使用 localStorage 當橋樑。然而 Safari 不支援此 API,需改用簡單登入登出功能。此技術解決了多分頁同步的需求,顯著提升登入體驗。...

需求

公司最近有一個登入的需求,為了安全性的問題,希望使用者即使沒登出,關閉視窗也會清除 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(() => {
  // 登出時,新增一個 resLogout 的 sessionStorage,判斷要不要關閉 broadcastChannel,避免登出後還會繼續廣播。
  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)

關於 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

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

39
20
9
4
MUKI says:

如果文章有幫助到你,歡迎分享給更多人知道。文章內容皆為 MUKI 本人的原創文章,我會經常更新文章以及修正錯誤內容,因此轉載時建議保留原出處,避免出現版本不一致或已過時的資訊。

本文地址:https://muki.tw/broadcastchannel-message-to-sessionstorage/ 已複製

Subscribe
Notify of
guest

0 則留言
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Copyright © since 2008 MUKI space* / omegaSS theme All Rights Reserved.