引言
在Web开发中,我们经常面临在不同页面间共享数据或进行通信。尽管在单页面应用(SPA)中,我们可以通过组件通信实现消息传递,但是当用户打开多个标签页时,就无法实现跨页面的消息传递。为了解决这个问题,我们需要借助浏览器中的跨页面通信。
浏览器跨页面通信的应用场景
用户登录当用户点击登录并打开新的标签页时,我们希望将登录信息传递给其他标签页,以便它们能够及时更新用户状态,从而提升用户体验。
用户操作例如,用户在点击跳转至文章详情页后,进行了收藏、点赞等操作,这些操作需要通知其他页面,确保各页面之间的数据同步。
其他场景需要在不同页面间共享信息的情况,从而实现全局状态的同步。
LocalStorage
LocalStorage
是HTML5
提供的本地存储机制。具有以下特点:
数据持久性存储在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>
注意:
- 如果其中一个页面修改数据,所有其他页面的
storage
事件都会被触发,而原始页面并不会触发该事件。 - 若修改的值未发生实际变化,
storage
事件将不会被触发。 sessionStorage
无法触发storage
事件。- 在某些浏览器的隐私模式下无法设置
localStorage
,如 Safari,这导致storage
事件无法使用。
缺点:
阻塞页面渲染 读取 localStorage
的操作是同步的,可能会阻塞页面的渲染,尤其是对于存储大量数据的情况。
无法跨域通信 存储的数据不能在非同源的页面中通信。
不安全 可能受到XSS等安全威胁的影响,因为数据存储在客户端。
Broadcast Channel
BroadcastChannel
接口代理了一个命名频道,同一频道的页面可以相互发送和监听消息。当某个页面向频道发送消息时,所有监听该频道的页面都会接收到消息,实现了跨页面通信。
通过new BroadcastChannel
创建一个频道实例,使用postMessage
方法向频道发布消息,而在同一频道的对象则通过onmessage
和onmessageerror
消息可以广播到所有监听了该频道的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>
在这个例子中,在PageA和PageB中通过Broadcast Channel
创建了一个名为 message
的频道。PageA通过点击触发postMessage
方法,向频道中发送了一条消息,而PageB使用onmessage
监听同一个频道中发送的消息,这样就使两个页面就建立了一条通信通道,实现了消息的相互传递和接收。
当在PageA中使用postmessage
发送消息时,该页面无法通过 onmessage
事件监听到这条消息。onmessage
事件只会在不同上下文(比如不同标签页或窗口)之间触发。这是因为BroadcastChannel
的设计是为了在同一源的不同上下文之间进行通信,而不是在同一上下文内部通信。
注意:
受到同源策略的限制Broadcast Channel
和LocalStorage
一样,遵循同源策略。
消息的数据类型相较于LocalStorage
,Broadcast Channel
更灵活,能够传输更复杂的数据类型Object
、Array
、Map
、Set
等。
手动关闭频道在开启频道之后,将不会自动关闭频道,需要我们手动调用close
方法去关闭频道,防止内存泄漏。
SharedWorker
SharedWorker
是Web 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
详情
首先,我们创建了两个页面 PageA 和 PageB,并分别实例化了同一个 SharedWorker
。通过 port
属性建立联系,PageA 和 PageB 可以通过调用 postMessage
方法向 SharedWorker
发送消息,而 SharedWorker
则通过 port
属性监听这些消息,并将其发送到页面。
然而,我们发现当 PageA 调用 postMessage
向 SharedWorker
发送消息时,只有 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);
};
}
注意:
受到同源策略的限制和上面的两种方式一样,SharedWorker
也遵循同源策略。
关闭条件SharedWorker
是长连接关闭某一个与之连接的实例并不会关闭SharedWorker
,只会断开连接,只有所有与Sharedworker
连接的实例关闭才会销毁。
postMessage
postMessage
是HTML5
提供的一种安全传递消息的方法,主要用于实现跨窗口通信,尤其在不同域之间的页面通信。
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
引用,例如iframe
的contentWindow
属性、通过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>
通过在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之间传递数据。