浏览器跨页面通信方案

2,222 阅读11分钟

引言

在Web开发中,我们经常面临在不同页面间共享数据或进行通信。尽管在单页面应用(SPA)中,我们可以通过组件通信实现消息传递,但是当用户打开多个标签页时,就无法实现跨页面的消息传递。为了解决这个问题,我们需要借助浏览器中的跨页面通信。

浏览器跨页面通信的应用场景

用户登录当用户点击登录并打开新的标签页时,我们希望将登录信息传递给其他标签页,以便它们能够及时更新用户状态,从而提升用户体验。

用户操作例如,用户在点击跳转至文章详情页后,进行了收藏、点赞等操作,这些操作需要通知其他页面,确保各页面之间的数据同步。

其他场景需要在不同页面间共享信息的情况,从而实现全局状态的同步。

LocalStorage

LocalStorageHTML5提供的本地存储机制。具有以下特点:

数据持久性存储在localStorage中的数据在页面关闭后仍然存在,不会被清除,适用于长期保存用户偏好设置,用户可以手动删除。

受同源策略限制遵循同源策略,在同一域下,存储的数据是共享的。

存储容量通常比 Cookie 的存储容量大得多,支持存储5MB的数据,数据为键值对的形式,只能存储字符串格式。

基于同一域数据共享的原理,我们可以利用localStorage在同一域下的不同页面间进行通信。例如,如果在PageA中使用LocalStorage存储了数据,其他同域的PageB可以通过监听storage事件去获取消息,实现页面间的通信。

window.onstorage = function(e) {   console.log(`The ${e.key}  key has been changed from ${e.oldValue}  to  ${e.newValue}`); };
- key:存储在 localStorage 的键名。
- newValue:发生变化后的数据。
- oldValue:变化之前的数据。

接下来,我们将使用LocalStorage实现跨页面间的通信

<script setup lang="ts">
import { ref,onBeforeUnmount,onMounted} from 'vue'

const messages = ref<number[]>([1,2])
// 发送信息
function handleSendMessage(){
  const message = Math.floor(Math.random() * 10)
  localStorage.setItem('message',JSON.stringify(message))
}
// 对 storage 事件的监听
onMounted(()=>{
  window.addEventListener('storage',function(e){
    if(e.key === 'message'){
      const message:number = JSON.parse(e.newValue)
      messages.value.push(message)
    }
  })
})
// 移除监听器,防止内存泄漏
onBeforeUnmount(()=>{
  window.removeEventListener('storage',function(e){
    if(e.key === 'message'){
      const message:number = JSON.parse(e.newValue)
      messages.value.push(message)
    }
  })
})
</script>

<template>
  <h1>{{ messages}}</h1>
  <div class="card">
    <button type="button" @click="handleSendMessage">点击发送message</button>
  </div>
</template>
test.gif 当我们点击按钮存储数据到 `localStorage` 时,一旦 `localStorage` 中的数据发生变化,其他页面将触发 `storage` 事件。

注意:

  1. 如果其中一个页面修改数据,所有其他页面的 storage 事件都会被触发,而原始页面并不会触发该事件。
  2. 若修改的值未发生实际变化,storage 事件将不会被触发。
  3. sessionStorage 无法触发 storage 事件。
  4. 在某些浏览器的隐私模式下无法设置 localStorage,如 Safari,这导致 storage 事件无法使用。

缺点:

阻塞页面渲染 读取 localStorage 的操作是同步的,可能会阻塞页面的渲染,尤其是对于存储大量数据的情况。

无法跨域通信 存储的数据不能在非同源的页面中通信。

不安全 可能受到XSS等安全威胁的影响,因为数据存储在客户端。

Broadcast Channel

BroadcastChannel接口代理了一个命名频道,同一频道的页面可以相互发送和监听消息。当某个页面向频道发送消息时,所有监听该频道的页面都会接收到消息,实现了跨页面通信。

通过new BroadcastChannel创建一个频道实例,使用postMessage方法向频道发布消息,而在同一频道的对象则通过onmessageonmessageerror消息可以广播到所有监听了该频道的BroadcastChannel对象。

以下是两个演示 Broadcast Channel 通信的页面,分别是 PageA 和 PageB。

<!-- PageA --->
<script setup lang="ts">
import { ref,onBeforeUnmount,onMounted} from 'vue'

const receiveMessages = ref<number[]>([])
// 创建一个广播名为message的实例
const broadcast = new BroadcastChannel('message') 
// 发送信息
const handleSendMessage = () =>{
  const message = Math.floor(Math.random() * 100)
  broadcast.postMessage(message)
}
// 接收同一个频道传递的消息
broadcast.onmessage = (e)=>{
  receiveMessages.value.push(e.data)
}
// 处理接收到的错误消息
broadcast.onmessageerror = (e) =>{console.log(e)}
// 手动关闭频道广播 防止内存泄漏
onBeforeUnmount(()=>broadcast.close())
</script>

<template>
  <h1>PageA</h1>
  <h1>{{ receiveMessages }}</h1>
  <div class="card">
    <button type="button" @click="handleSendMessage">点击发送message</button>
  </div>
</template>
<!-- PageB --->
<script setup lang="ts">
import { ref,onBeforeUnmount,onMounted} from 'vue'

const receiveMessages = ref<number[]>([])
const broadcast = new BroadcastChannel('message') 
const handleSendMessage = () =>{
  const message = Math.floor(Math.random() * 100)
  broadcast.postMessage(message)
}
broadcast.onmessage = (e)=>{
  receiveMessages.value.push(e.data)
}
broadcast.onmessageerror = (e) =>{console.log(e)}
onBeforeUnmount(()=>broadcast.close())
</script>

<template>
  <h1>PageB</h1>
  <h1>{{ receiveMessages }}</h1>
  <div class="card">
    <button type="button" @click="handleSendMessage">点击发送message</button>
  </div>
</template>
test2.gif

在这个例子中,在PageA和PageB中通过Broadcast Channel创建了一个名为 message 的频道。PageA通过点击触发postMessage方法,向频道中发送了一条消息,而PageB使用onmessage监听同一个频道中发送的消息,这样就使两个页面就建立了一条通信通道,实现了消息的相互传递和接收。

当在PageA中使用postmessage发送消息时,该页面无法通过 onmessage 事件监听到这条消息。onmessage 事件只会在不同上下文(比如不同标签页或窗口)之间触发。这是因为BroadcastChannel 的设计是为了在同一源的不同上下文之间进行通信,而不是在同一上下文内部通信。

注意:

受到同源策略的限制Broadcast ChannelLocalStorage一样,遵循同源策略。

消息的数据类型相较于LocalStorageBroadcast Channel更灵活,能够传输更复杂的数据类型ObjectArrayMapSet等。

手动关闭频道在开启频道之后,将不会自动关闭频道,需要我们手动调用close方法去关闭频道,防止内存泄漏。

SharedWorker

SharedWorkerWeb Worker API的一部分,允许在多个页面之间共享一个工作线程。它允许多个浏览上下文,比如同一域下的多个窗口或标签页,共享同一个工作线程。通过共享同一份数据和状态,实现跨页面通信。由于 SharedWorker 属于 Worker,所以它在独立的上下文中运行,不依赖于任何具体的页面。

在使用 SharedWorker 时,首先需要创建一个 SharedWorker 实例。创建了 SharedWorker 后,我们可以通过 port 属性在页面中与 SharedWorker 进行通信。

接下来,我将通过代码演示一个计数器,展示如何通过 SharedWorker 与主线程通信的例子。

// sharedWorker.ts
let count = 0;
self.onconnect = (e:MessageEvent) => {
    const port = e.ports[0];
    // 监听消息
    port.onmessage = (event):void => {
      event.data === 'increment' && port.postMessage(++count);
    };
}

创建两个页面,并实例化sharedWorker,定义接收消息事件和发送消息事件。

<!--PageA-->
<script setup lang="ts">
import { ref} from 'vue'

const worker = new SharedWorker(new URL('./shareWorker.ts', import.meta.url))
const receiveMessage = ref()
// 接收 SharedWorker 发送的消息
worker.port.onmessage = function (event) {
  receiveMessage.value = event.data
};
// 发送消息给 SharedWorker
const handleSendMessage = () => worker.port.postMessage('increment');
</script>

<template>
  <h1>PageA</h1>
  <h1>Page A received:{{ receiveMessage}}</h1>
  <div class="card">
    <button type="button" @click="handleSendMessage">点击发送message</button>
  </div>
</template>

<!--PageB-->
<script setup lang="ts">
import { ref} from 'vue'

const worker = new SharedWorker(new URL('./shareWorker.ts', import.meta.url))
const receiveMessage = ref()
// 接收 SharedWorker 发送的消息
worker.port.onmessage = function (event) {
  receiveMessage.value = event.data
};
// 发送消息给 SharedWorker
const handleSendMessage = () => worker.port.postMessage('increment');
</script>

<template>
  <h1>PageB</h1>
  <h1>Page B received:{{ receiveMessage}}</h1>
  <div class="card">
    <button type="button" @click="handleSendMessage">点击发送message</button>
  </div>
</template>

注意 使用vite构建的项目需要使用构造器来导入SharedWorker详情

test3.gif

首先,我们创建了两个页面 PageA 和 PageB,并分别实例化了同一个 SharedWorker。通过 port 属性建立联系,PageA 和 PageB 可以通过调用 postMessage 方法向 SharedWorker 发送消息,而 SharedWorker 则通过 port 属性监听这些消息,并将其发送到页面。

然而,我们发现当 PageA 调用 postMessageSharedWorker 发送消息时,只有 PageA 的 onmessage 事件被触发,而 PageB 并未监听到相应的消息。

经过查阅相关文章,我们了解到 SharedWorker 无法主动通知所有页面,因此我们采取了轮询的方式来获取最新的数据。

我们使用定时器周期性地向 SharedWorker 发送 get 字段 ,当 SharedWorker 接收到 get 字段后,它会向页面发送当前的计数。通过这种方式,我们成功实现了消息的同步,确保页面能够获取到最新的数据。这种机制保证了在 SharedWorker 与多个页面之间建立通信后,页面间能够及时获取到共享的状态信息。

// 在PageA和PageB中添加轮询
const timer = setInterval(function () {
  worker.port.postMessage('get');
}, 1000);

onBeforeUnmount(()=>clearInterval(timer)) // 清除定时器 防止内存泄漏
// sharedWorker
let count = 0;
self.onconnect = (e:MessageEvent) => {
    const port = e.ports[0];
    // 监听消息
    port.onmessage = (event):void => {
      event.data === 'get' && port.postMessage(count)
      event.data === 'increment' && port.postMessage(++count);
    };
}
test4.gif

注意:

受到同源策略的限制和上面的两种方式一样,SharedWorker也遵循同源策略。

关闭条件SharedWorker是长连接关闭某一个与之连接的实例并不会关闭SharedWorker,只会断开连接,只有所有与Sharedworker连接的实例关闭才会销毁。

postMessage

postMessageHTML5提供的一种安全传递消息的方法,主要用于实现跨窗口通信,尤其在不同域之间的页面通信。

otherWindow.postMessage(message, targetOrigin, [transfer])
otherWindow:其他窗口的一个引用
message:传递的信息,字符串、对象...
targetOrigin:指定消息发送给哪个窗口的源。可以是具体的域名,也可以是通配符 `"*"` 表示不限制源。
transfer:可选参数,

通过otherWindow.postMessage,我们可以在不同域之间建立通信,通常通过window.open或者 iframe方法实现。

举例而言,当在A域下的PageA页面希望与B域下的PageB进行通信时,需要在PageA中使用window.open引入PageB页面。window.open会返回PageB页面的window引用,通过这个引用的postMessage方法,我们就能够安全地在不同域之间传递消息。

注意:

otherWindow指的是非当前页面的window引用,例如iframecontentWindow属性、通过window.open返回的窗口对象,这样的引用允许我们在不同窗口或文档之间建立通信通道,实现安全的跨域消息传递。

下面的两个页面分别位于不同的域,用于演示postMessage进行跨域通信。

<!--http://localhost:5173 PageA -->

<script setup lang="ts">
import { ref,onMounted,onBeforeUnmount } from 'vue'

const receiveMessage = ref<string[]>([])
const otherOrigin = "http://localhost:5174" // B域

const handleSendMessage = () => {
  const otherWindow = window.open(otherOrigin); // 打开B域的网址
  setTimeout(()=>{
    otherWindow?.postMessage({message:'message for PageA'},otherOrigin); // 向B域发送信息
  },1000)
}

function handle(event:MessageEvent):void{
  const origin = event.origin; // 发送消息的源
  if(origin === otherOrigin){
    receiveMessage.value.push(event.data) 
  }
}
// 接收信息
onMounted(()=> window.addEventListener('message',handle))
// 移除监听
onBeforeUnmount(()=> window.removeEventListener('message',handle))
</script>


<template>
  <h1>PageA</h1>
  <h1>Page A received:{{receiveMessage}}</h1>
  <div class="card">
    <button type="button" @click="handleSendMessage">点击发送message</button>
  </div>
</template>
<!--http://localhost:5174 PageB -->

<script setup lang="ts">
import { ref,onBeforeUnmount,onMounted} from 'vue'

const receiveMessage = ref<string[]>([])
const otherOrigin = "http://localhost:5173" // A域

// 监听的处理函数
function handle(event:MessageEvent):void{
  const origin = event.origin; // 发送消息的源
  if(origin === otherOrigin){
    receiveMessage.value.push(event.data) 
    event.source?.postMessage("received message from PageB",otherOrigin);
  }
}
// 接收信息
onMounted(()=> window.addEventListener('message',handle))
// 移除监听
onBeforeUnmount(()=> window.removeEventListener('message',handle))
</script>

<template>
  <h1>PageB</h1>
  <h1>Page B received:{{receiveMessage}}</h1>
</template>
test6.gif

通过在PageA中使用window.open获取PageB的引用,然后使用postMessage向PageB发送消息,并在PageB中监听message事件,实现非同源跨页面通信。通过event.origin可以获取发送消息的源,以确保只接收指定源的消息。

在PageA向PageB发送信息,如何知道PageB是否成功收到消息呢?

message事件的event对象中,还有一个source属性,通过event.source可以获取发送消息的window引用。通过调用event.source.postMessage,PageB就能发送消息给PageA,从而使PageA知道消息发送成功。

因为window.open方法是异步的,打开一个新窗口需要一定的时间。使用定时器确保在打开新窗口后等待一段时间再发送消息。但是延迟1秒发送信息,这样的实现不是最好的,因为网络情况和其他复杂情况可能导致定时器触发的时机不准确。为了确保在窗口加载完成后再发送消息,这样可以确保目标窗口已经完全加载和准备好接收消息,而不需要依赖定时器。

注意:

保护敏感信息在使用postMessage发送消息时,注意不要包含任何敏感信息,如密码、用户名等。postMessage是一种公开的通信方式,可能受到其他网站的监听。确保只发送安全的、不敏感的数据,以防止信息泄露。

避免死循环在使用postMessage时,需要避免页面间的通信陷入死循环。例如,在PageA中打开PageB,而PageB又通过消息触发PageA执行操作,这可能导致无限循环。确保页面通信是单向的。

防止点击劫持考虑到点击劫持攻击,可以在网站中设置X-Frame-Options头,限制网站在iframe中的显示。这样可以有效防止攻击者将目标网站覆盖在透明的iframe中,从而保护用户免受点击劫持攻击的影响。

总结

localStorage

基于本地存储,可以在同一域下的不同页面之间进行数据共享,存在同步阻塞和同源策略的限制。

Broadcast Channel

基于频道广播,受同源策略限制,允许不同页面通过命名频道进行通信。

SharedWorker

基于共享Worker,提供了一个独立的线程,可以进行多个页面之间的数据共享和通信,受同源策略限制,适用于大量数据的实时共享。

postMessage

用于非同源页面的通信,通过窗口间的消息传递实现跨域通信,异步通信,可在不同窗口或iframe之间传递数据。

参考资料

跨页面通信有多少种技术方式可以实现?

💞💞💞SharedWorker 让你多个页面相互通信

面试官:前端跨页面通信,你知道哪些方法?

【3分钟速览】前端广播式通信:Broadcast Channel

SharedWorker 讲解 & 广播实现

前端跨域解决方案——postMessage