标签页通信
浏览器多标签页之间的实时通信解决方案。
使用场景
- 多标签同步: 用户在多个标签页中操作,需要实时同步状态
- 单点登录: 一个标签页登出,其他标签页同步登出
- 实时通知: 一个标签页的操作需要通知其他标签页
- 数据共享: 多个标签页共享相同的数据状态
- 协同编辑: 多个标签页同时编辑同一份文档
方案对比
| 特性 | BroadcastChannel | LocalStorage 事件 | Service Worker |
|---|---|---|---|
| 浏览器支持 | Chrome 54+, Firefox 38+ | 所有现代浏览器 | Chrome 40+, Firefox 44+ |
| API 复杂度 | ⭐ 简单 | ⭐⭐ 中等 | ⭐⭐⭐ 复杂 |
| 消息类型 | 任意可序列化对象 | 字符串 | 任意可序列化对象 |
| 跨域通信 | ❌ 同源 | ❌ 同源 | ✅ 可跨域 |
| 离线支持 | ❌ | ❌ | ✅ |
| 性能 | ⭐⭐⭐ 高 | ⭐⭐ 中 | ⭐⭐⭐ 高 |
| 消息可靠性 | ⭐⭐⭐ 高 | ⭐⭐ 中 | ⭐⭐⭐ 高 |
BroadcastChannel
基本信息
- 简介: 专为标签页通信设计的原生 API
- 文档: https://developer.mozilla.org/zh-CN/docs/Web/API/BroadcastChannel
- 兼容性: Chrome 54+, Firefox 38+, Edge 79+
特点
- ✅ API 简单直观
- ✅ 支持任意可序列化数据
- ✅ 性能优秀
- ✅ 自动管理连接
- ⚠️ Safari 不支持(需要 polyfill)
基础用法
创建频道并发送消息
typescript
// 创建广播频道
const channel = new BroadcastChannel('app-channel')
// 发送消息
channel.postMessage({
type: 'USER_LOGIN',
payload: {
userId: 123,
username: 'zhangsan'
}
})
// 发送不同类型的消息
channel.postMessage({ type: 'CART_UPDATE', items: 5 })
channel.postMessage({ type: 'NOTIFICATION', message: '新消息' })接收消息
typescript
const channel = new BroadcastChannel('app-channel')
// 监听消息
channel.onmessage = (event) => {
console.log('收到消息:', event.data)
// 根据消息类型处理
switch (event.data.type) {
case 'USER_LOGIN':
handleUserLogin(event.data.payload)
break
case 'CART_UPDATE':
updateCartCount(event.data.items)
break
case 'NOTIFICATION':
showNotification(event.data.message)
break
}
}
// 或使用 addEventListener
channel.addEventListener('message', (event) => {
console.log('收到消息:', event.data)
})关闭频道
typescript
// 不再需要时关闭频道
channel.close()
// 在组件卸载时关闭
window.addEventListener('beforeunload', () => {
channel.close()
})Vue 3 集成示例
vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
interface Message {
type: string
payload?: any
}
const channel = ref<BroadcastChannel | null>(null)
const messages = ref<Message[]>([])
const userStatus = ref<'online' | 'offline'>('offline')
onMounted(() => {
// 创建频道
channel.value = new BroadcastChannel('app-channel')
// 监听消息
channel.value.onmessage = (event: MessageEvent<Message>) => {
messages.value.push(event.data)
// 处理用户状态变化
if (event.data.type === 'USER_STATUS') {
userStatus.value = event.data.payload.status
}
}
})
onUnmounted(() => {
// 清理资源
channel.value?.close()
})
// 发送消息
const sendMessage = (type: string, payload?: any) => {
channel.value?.postMessage({ type, payload })
}
// 通知其他标签页用户登录
const notifyLogin = (userId: number) => {
sendMessage('USER_LOGIN', { userId, timestamp: Date.now() })
}
// 通知其他标签页用户登出
const notifyLogout = () => {
sendMessage('USER_LOGOUT', { timestamp: Date.now() })
}
</script>
<template>
<div>
<div>用户状态: {{ userStatus }}</div>
<button @click="notifyLogin(123)">通知登录</button>
<button @click="notifyLogout">通知登出</button>
<div>
<h3>消息列表</h3>
<div v-for="(msg, index) in messages" :key="index">
{{ msg.type }}: {{ JSON.stringify(msg.payload) }}
</div>
</div>
</div>
</template>实际应用场景
1. 单点登出
typescript
// 登录页面
const channel = new BroadcastChannel('auth-channel')
// 用户登出时通知所有标签页
function logout() {
// 清除本地登录状态
localStorage.removeItem('token')
// 通知其他标签页
channel.postMessage({
type: 'LOGOUT',
timestamp: Date.now()
})
// 跳转到登录页
window.location.href = '/login'
}
// 其他页面监听登出消息
channel.onmessage = (event) => {
if (event.data.type === 'LOGOUT') {
// 清除本地状态
localStorage.removeItem('token')
// 跳转到登录页
window.location.href = '/login'
}
}2. 购物车同步
typescript
const cartChannel = new BroadcastChannel('cart-channel')
// 添加商品到购物车
function addToCart(product: any) {
// 更新本地购物车
const cart = getCart()
cart.push(product)
saveCart(cart)
// 通知其他标签页
cartChannel.postMessage({
type: 'CART_ADD',
product,
totalItems: cart.length
})
}
// 监听购物车变化
cartChannel.onmessage = (event) => {
if (event.data.type === 'CART_ADD') {
// 更新购物车图标数字
updateCartBadge(event.data.totalItems)
// 显示提示
showToast(`已添加 ${event.data.product.name}`)
}
}3. 实时通知
typescript
const notificationChannel = new BroadcastChannel('notification-channel')
// 发送通知
function sendNotification(message: string, type: 'info' | 'success' | 'error') {
notificationChannel.postMessage({
type: 'NOTIFICATION',
message,
notificationType: type,
timestamp: Date.now()
})
}
// 接收通知
notificationChannel.onmessage = (event) => {
if (event.data.type === 'NOTIFICATION') {
showToast(event.data.message, event.data.notificationType)
}
}LocalStorage onstorage 事件
基本信息
- 简介: 利用 localStorage 的 storage 事件实现跨标签通信
- 文档: https://developer.mozilla.org/zh-CN/docs/Web/API/Window/storage_event
- 兼容性: 所有现代浏览器
特点
- ✅ 兼容性最好
- ✅ 无需额外依赖
- ✅ 简单易用
- ⚠️ 只能传输字符串
- ⚠️ 当前标签页不会触发自己的 storage 事件
- ⚠️ 有存储限制(5-10MB)
基础用法
发送消息
typescript
// 发送消息(通过修改 localStorage)
function sendMessage(type: string, payload: any) {
const message = {
type,
payload,
timestamp: Date.now()
}
// 存储到 localStorage 会触发其他标签页的 storage 事件
localStorage.setItem('message', JSON.stringify(message))
// 立即删除,避免占用存储空间
localStorage.removeItem('message')
}
// 使用
sendMessage('USER_LOGIN', { userId: 123 })接收消息
typescript
// 监听 storage 事件
window.addEventListener('storage', (event) => {
// 只处理我们的消息
if (event.key === 'message' && event.newValue) {
const message = JSON.parse(event.newValue)
console.log('收到消息:', message)
// 根据消息类型处理
switch (message.type) {
case 'USER_LOGIN':
handleUserLogin(message.payload)
break
case 'USER_LOGOUT':
handleUserLogout()
break
}
}
})Vue 3 集成示例
vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
interface Message {
type: string
payload?: any
timestamp: number
}
const messages = ref<Message[]>([])
const isLoggedIn = ref(false)
// 发送消息
const sendMessage = (type: string, payload?: any) => {
const message: Message = {
type,
payload,
timestamp: Date.now()
}
localStorage.setItem('tab-message', JSON.stringify(message))
localStorage.removeItem('tab-message')
}
// 处理消息
const handleStorageEvent = (event: StorageEvent) => {
if (event.key === 'tab-message' && event.newValue) {
const message: Message = JSON.parse(event.newValue)
messages.value.push(message)
// 处理登录/登出
if (message.type === 'USER_LOGIN') {
isLoggedIn.value = true
} else if (message.type === 'USER_LOGOUT') {
isLoggedIn.value = false
// 跳转到登录页
window.location.href = '/login'
}
}
}
onMounted(() => {
window.addEventListener('storage', handleStorageEvent)
})
onUnmounted(() => {
window.removeEventListener('storage', handleStorageEvent)
})
// 登录
const login = () => {
isLoggedIn.value = true
sendMessage('USER_LOGIN', { userId: 123 })
}
// 登出
const logout = () => {
isLoggedIn.value = false
localStorage.removeItem('token')
sendMessage('USER_LOGOUT')
}
</script>
<template>
<div>
<div>登录状态: {{ isLoggedIn ? '已登录' : '未登录' }}</div>
<button v-if="!isLoggedIn" @click="login">登录</button>
<button v-else @click="logout">登出</button>
<div>
<h3>消息历史</h3>
<div v-for="(msg, index) in messages" :key="index">
{{ new Date(msg.timestamp).toLocaleTimeString() }} - {{ msg.type }}
</div>
</div>
</div>
</template>实际应用场景
1. 主题切换同步
typescript
// 切换主题
function setTheme(theme: 'light' | 'dark') {
// 应用主题
document.documentElement.setAttribute('data-theme', theme)
// 保存到 localStorage
localStorage.setItem('theme', theme)
}
// 监听其他标签页的主题变化
window.addEventListener('storage', (event) => {
if (event.key === 'theme' && event.newValue) {
document.documentElement.setAttribute('data-theme', event.newValue)
}
})2. 语言切换同步
typescript
// 切换语言
function setLanguage(lang: 'zh-CN' | 'en-US') {
// 应用语言
i18n.global.locale = lang
// 保存到 localStorage
localStorage.setItem('language', lang)
}
// 监听语言变化
window.addEventListener('storage', (event) => {
if (event.key === 'language' && event.newValue) {
i18n.global.locale = event.newValue
// 重新加载页面以应用新语言
window.location.reload()
}
})3. 用户权限同步
typescript
// 更新用户权限
function updateUserPermissions(permissions: string[]) {
localStorage.setItem('permissions', JSON.stringify(permissions))
}
// 监听权限变化
window.addEventListener('storage', (event) => {
if (event.key === 'permissions' && event.newValue) {
const permissions = JSON.parse(event.newValue)
// 更新应用权限状态
store.commit('setPermissions', permissions)
// 如果当前页面需要的权限被移除,跳转到无权限页面
if (!hasRequiredPermission(permissions)) {
window.location.href = '/403'
}
}
})封装工具函数
typescript
// 标签页通信工具类
class TabMessenger {
private listeners: Map<string, Function[]> = new Map()
constructor() {
window.addEventListener('storage', this.handleStorage.bind(this))
}
// 发送消息
send(type: string, payload?: any) {
const message = {
type,
payload,
timestamp: Date.now()
}
localStorage.setItem('tab-message', JSON.stringify(message))
localStorage.removeItem('tab-message')
}
// 监听消息
on(type: string, callback: (payload: any) => void) {
if (!this.listeners.has(type)) {
this.listeners.set(type, [])
}
this.listeners.get(type)!.push(callback)
}
// 取消监听
off(type: string, callback: Function) {
const callbacks = this.listeners.get(type)
if (callbacks) {
const index = callbacks.indexOf(callback)
if (index > -1) {
callbacks.splice(index, 1)
}
}
}
// 处理 storage 事件
private handleStorage(event: StorageEvent) {
if (event.key === 'tab-message' && event.newValue) {
const message = JSON.parse(event.newValue)
const callbacks = this.listeners.get(message.type)
if (callbacks) {
callbacks.forEach(callback => callback(message.payload))
}
}
}
// 清理
destroy() {
window.removeEventListener('storage', this.handleStorage.bind(this))
this.listeners.clear()
}
}
// 使用
const messenger = new TabMessenger()
messenger.on('USER_LOGIN', (payload) => {
console.log('用户登录:', payload)
})
messenger.send('USER_LOGIN', { userId: 123 })Service Worker
基本信息
- 简介: 通过 Service Worker 作为中间层实现标签页通信
- 文档: https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API
- 兼容性: Chrome 40+, Firefox 44+, Safari 11.1+
特点
- ✅ 功能最强大
- ✅ 支持离线消息
- ✅ 可以处理复杂的消息路由
- ✅ 支持跨域通信(需要配置)
- ⚠️ 配置复杂
- ⚠️ 需要 HTTPS(localhost 除外)
- ⚠️ 调试相对困难
基础用法
注册 Service Worker
typescript
// main.ts
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js')
.then(registration => {
console.log('Service Worker 注册成功:', registration)
})
.catch(error => {
console.error('Service Worker 注册失败:', error)
})
}Service Worker 文件
javascript
// sw.js
// 存储所有客户端(标签页)
const clients = []
// 监听消息
self.addEventListener('message', async (event) => {
const { type, payload, targetId } = event.data
// 获取所有客户端
const allClients = await self.clients.matchAll({
includeUncontrolled: true,
type: 'window'
})
// 广播消息到所有标签页(除了发送者)
if (type === 'BROADCAST') {
allClients.forEach(client => {
if (client.id !== event.source.id) {
client.postMessage({
type: payload.type,
data: payload.data
})
}
})
}
// 发送给特定标签页
if (type === 'SEND_TO_TAB' && targetId) {
const targetClient = allClients.find(client => client.id === targetId)
if (targetClient) {
targetClient.postMessage({
type: payload.type,
data: payload.data
})
}
}
// 回复发送者
if (type === 'REQUEST') {
event.source.postMessage({
type: 'RESPONSE',
data: { message: '收到请求' }
})
}
})
// 激活事件
self.addEventListener('activate', (event) => {
console.log('Service Worker 已激活')
})发送消息
typescript
// 发送广播消息
async function broadcastMessage(type: string, data: any) {
if (!navigator.serviceWorker.controller) {
console.warn('Service Worker 未激活')
return
}
navigator.serviceWorker.controller.postMessage({
type: 'BROADCAST',
payload: { type, data }
})
}
// 使用
broadcastMessage('USER_LOGIN', { userId: 123 })接收消息
typescript
// 监听来自 Service Worker 的消息
navigator.serviceWorker.addEventListener('message', (event) => {
console.log('收到消息:', event.data)
const { type, data } = event.data
switch (type) {
case 'USER_LOGIN':
handleUserLogin(data)
break
case 'USER_LOGOUT':
handleUserLogout()
break
}
})Vue 3 集成示例
vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
interface Message {
type: string
data: any
}
const messages = ref<Message[]>([])
const swReady = ref(false)
// 注册 Service Worker
onMounted(async () => {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register('/sw.js')
console.log('Service Worker 注册成功')
// 等待 Service Worker 激活
await navigator.serviceWorker.ready
swReady.value = true
// 监听消息
navigator.serviceWorker.addEventListener('message', handleMessage)
} catch (error) {
console.error('Service Worker 注册失败:', error)
}
}
})
onUnmounted(() => {
navigator.serviceWorker.removeEventListener('message', handleMessage)
})
// 处理消息
const handleMessage = (event: MessageEvent) => {
messages.value.push(event.data)
// 处理特定消息
if (event.data.type === 'USER_LOGOUT') {
window.location.href = '/login'
}
}
// 发送广播消息
const broadcast = (type: string, data: any) => {
if (!navigator.serviceWorker.controller) {
console.warn('Service Worker 未激活')
return
}
navigator.serviceWorker.controller.postMessage({
type: 'BROADCAST',
payload: { type, data }
})
}
// 通知登录
const notifyLogin = () => {
broadcast('USER_LOGIN', { userId: 123, timestamp: Date.now() })
}
// 通知登出
const notifyLogout = () => {
broadcast('USER_LOGOUT', { timestamp: Date.now() })
}
</script>
<template>
<div>
<div>Service Worker: {{ swReady ? '已就绪' : '未就绪' }}</div>
<button :disabled="!swReady" @click="notifyLogin">
通知登录
</button>
<button :disabled="!swReady" @click="notifyLogout">
通知登出
</button>
<div>
<h3>消息列表</h3>
<div v-for="(msg, index) in messages" :key="index">
{{ msg.type }}: {{ JSON.stringify(msg.data) }}
</div>
</div>
</div>
</template>实际应用场景
1. 离线消息队列
javascript
// sw.js
const messageQueue = []
self.addEventListener('message', async (event) => {
const { type, payload } = event.data
if (type === 'QUEUE_MESSAGE') {
// 存储消息到队列
messageQueue.push({
...payload,
timestamp: Date.now()
})
// 尝试发送队列中的消息
await processQueue()
}
})
async function processQueue() {
const allClients = await self.clients.matchAll()
// 如果有在线的客户端,发送队列中的消息
if (allClients.length > 0) {
while (messageQueue.length > 0) {
const message = messageQueue.shift()
allClients.forEach(client => {
client.postMessage({
type: 'QUEUED_MESSAGE',
data: message
})
})
}
}
}
// 定期检查队列
setInterval(processQueue, 5000)2. 标签页管理
javascript
// sw.js
const tabs = new Map()
self.addEventListener('message', async (event) => {
const { type, payload } = event.data
// 注册标签页
if (type === 'REGISTER_TAB') {
tabs.set(event.source.id, {
id: event.source.id,
url: payload.url,
title: payload.title,
timestamp: Date.now()
})
// 通知所有标签页更新标签列表
broadcastTabList()
}
// 注销标签页
if (type === 'UNREGISTER_TAB') {
tabs.delete(event.source.id)
broadcastTabList()
}
})
async function broadcastTabList() {
const allClients = await self.clients.matchAll()
const tabList = Array.from(tabs.values())
allClients.forEach(client => {
client.postMessage({
type: 'TAB_LIST_UPDATE',
data: tabList
})
})
}3. 跨标签页状态同步
typescript
// 状态同步管理器
class StateSync {
private state: any = {}
constructor() {
this.init()
}
private async init() {
if ('serviceWorker' in navigator) {
await navigator.serviceWorker.ready
// 监听状态更新
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data.type === 'STATE_UPDATE') {
this.state = { ...this.state, ...event.data.data }
this.notifyListeners()
}
})
}
}
// 更新状态
setState(key: string, value: any) {
this.state[key] = value
// 通知其他标签页
navigator.serviceWorker.controller?.postMessage({
type: 'BROADCAST',
payload: {
type: 'STATE_UPDATE',
data: { [key]: value }
}
})
}
// 获取状态
getState(key: string) {
return this.state[key]
}
private listeners: Function[] = []
// 监听状态变化
subscribe(callback: Function) {
this.listeners.push(callback)
return () => {
const index = this.listeners.indexOf(callback)
if (index > -1) {
this.listeners.splice(index, 1)
}
}
}
private notifyListeners() {
this.listeners.forEach(callback => callback(this.state))
}
}
// 使用
const stateSync = new StateSync()
stateSync.subscribe((state) => {
console.log('状态更新:', state)
})
stateSync.setState('user', { id: 123, name: '张三' })推荐使用场景
BroadcastChannel
适用场景:
- 现代浏览器项目(不需要兼容 Safari)
- 需要高性能的实时通信
- 消息类型多样,数据结构复杂
- 简单的标签页同步需求
推荐指数: ⭐⭐⭐⭐⭐
LocalStorage 事件
适用场景:
- 需要最大兼容性的项目
- 简单的状态同步(主题、语言等)
- 不需要传输大量数据
- 快速实现,无需额外配置
推荐指数: ⭐⭐⭐⭐
Service Worker
适用场景:
- PWA 应用
- 需要离线消息队列
- 复杂的消息路由需求
- 需要跨域通信
- 已经使用 Service Worker 的项目
推荐指数: ⭐⭐⭐
完整示例:多标签页登录同步
点击查看完整代码
vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
// 支持多种通信方式
type CommunicationMethod = 'broadcast' | 'storage' | 'serviceworker'
const method = ref<CommunicationMethod>('broadcast')
const isLoggedIn = ref(false)
const username = ref('')
const messages = ref<string[]>([])
// BroadcastChannel
let channel: BroadcastChannel | null = null
// 初始化通信
const initCommunication = () => {
if (method.value === 'broadcast' && 'BroadcastChannel' in window) {
channel = new BroadcastChannel('auth-channel')
channel.onmessage = handleBroadcastMessage
} else if (method.value === 'storage') {
window.addEventListener('storage', handleStorageEvent)
} else if (method.value === 'serviceworker') {
initServiceWorker()
}
}
// 处理 BroadcastChannel 消息
const handleBroadcastMessage = (event: MessageEvent) => {
const { type, payload } = event.data
if (type === 'LOGIN') {
isLoggedIn.value = true
username.value = payload.username
addMessage(`用户 ${payload.username} 在其他标签页登录`)
} else if (type === 'LOGOUT') {
isLoggedIn.value = false
username.value = ''
addMessage('用户在其他标签页登出')
}
}
// 处理 Storage 事件
const handleStorageEvent = (event: StorageEvent) => {
if (event.key === 'auth-message' && event.newValue) {
const { type, payload } = JSON.parse(event.newValue)
if (type === 'LOGIN') {
isLoggedIn.value = true
username.value = payload.username
addMessage(`用户 ${payload.username} 在其他标签页登录`)
} else if (type === 'LOGOUT') {
isLoggedIn.value = false
username.value = ''
addMessage('用户在其他标签页登出')
}
}
}
// 初始化 Service Worker
const initServiceWorker = async () => {
if ('serviceWorker' in navigator) {
await navigator.serviceWorker.ready
navigator.serviceWorker.addEventListener('message', handleSWMessage)
}
}
// 处理 Service Worker 消息
const handleSWMessage = (event: MessageEvent) => {
const { type, data } = event.data
if (type === 'LOGIN') {
isLoggedIn.value = true
username.value = data.username
addMessage(`用户 ${data.username} 在其他标签页登录`)
} else if (type === 'LOGOUT') {
isLoggedIn.value = false
username.value = ''
addMessage('用户在其他标签页登出')
}
}
// 发送消息
const sendMessage = (type: string, payload?: any) => {
if (method.value === 'broadcast' && channel) {
channel.postMessage({ type, payload })
} else if (method.value === 'storage') {
const message = { type, payload, timestamp: Date.now() }
localStorage.setItem('auth-message', JSON.stringify(message))
localStorage.removeItem('auth-message')
} else if (method.value === 'serviceworker') {
navigator.serviceWorker.controller?.postMessage({
type: 'BROADCAST',
payload: { type, data: payload }
})
}
}
// 登录
const login = () => {
const user = 'User' + Math.floor(Math.random() * 1000)
isLoggedIn.value = true
username.value = user
// 保存到 localStorage
localStorage.setItem('token', 'fake-token-' + Date.now())
localStorage.setItem('username', user)
// 通知其他标签页
sendMessage('LOGIN', { username: user })
addMessage(`当前标签页登录成功`)
}
// 登出
const logout = () => {
isLoggedIn.value = false
username.value = ''
// 清除 localStorage
localStorage.removeItem('token')
localStorage.removeItem('username')
// 通知其他标签页
sendMessage('LOGOUT')
addMessage(`当前标签页登出`)
}
// 添加消息
const addMessage = (msg: string) => {
const time = new Date().toLocaleTimeString()
messages.value.unshift(`[${time}] ${msg}`)
// 只保留最近 10 条消息
if (messages.value.length > 10) {
messages.value = messages.value.slice(0, 10)
}
}
// 切换通信方式
const changeMethod = (newMethod: CommunicationMethod) => {
// 清理旧的监听器
if (channel) {
channel.close()
channel = null
}
window.removeEventListener('storage', handleStorageEvent)
navigator.serviceWorker.removeEventListener('message', handleSWMessage)
// 初始化新的通信方式
method.value = newMethod
initCommunication()
addMessage(`切换到 ${newMethod} 通信方式`)
}
onMounted(() => {
// 恢复登录状态
const token = localStorage.getItem('token')
const savedUsername = localStorage.getItem('username')
if (token && savedUsername) {
isLoggedIn.value = true
username.value = savedUsername
}
// 初始化通信
initCommunication()
})
onUnmounted(() => {
// 清理资源
if (channel) {
channel.close()
}
window.removeEventListener('storage', handleStorageEvent)
navigator.serviceWorker.removeEventListener('message', handleSWMessage)
})
</script>
<template>
<div class="p-6 max-w-2xl mx-auto">
<h1 class="text-2xl font-bold mb-6">多标签页登录同步示例</h1>
<!-- 通信方式选择 -->
<div class="mb-6">
<label class="block text-sm font-medium mb-2">通信方式:</label>
<div class="flex gap-4">
<button
@click="changeMethod('broadcast')"
:class="[
'px-4 py-2 rounded',
method === 'broadcast'
? 'bg-blue-500 text-white'
: 'bg-gray-200'
]"
>
BroadcastChannel
</button>
<button
@click="changeMethod('storage')"
:class="[
'px-4 py-2 rounded',
method === 'storage'
? 'bg-blue-500 text-white'
: 'bg-gray-200'
]"
>
LocalStorage
</button>
<button
@click="changeMethod('serviceworker')"
:class="[
'px-4 py-2 rounded',
method === 'serviceworker'
? 'bg-blue-500 text-white'
: 'bg-gray-200'
]"
>
Service Worker
</button>
</div>
</div>
<!-- 登录状态 -->
<div class="mb-6 p-4 bg-gray-100 rounded">
<div class="mb-2">
<span class="font-medium">登录状态:</span>
<span :class="isLoggedIn ? 'text-green-600' : 'text-red-600'">
{{ isLoggedIn ? '已登录' : '未登录' }}
</span>
</div>
<div v-if="isLoggedIn" class="mb-4">
<span class="font-medium">用户名:</span>
<span>{{ username }}</span>
</div>
<div class="flex gap-4">
<button
v-if="!isLoggedIn"
@click="login"
class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
>
登录
</button>
<button
v-else
@click="logout"
class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
登出
</button>
</div>
</div>
<!-- 消息列表 -->
<div>
<h2 class="text-lg font-semibold mb-3">消息日志</h2>
<div class="bg-gray-50 rounded p-4 max-h-64 overflow-y-auto">
<div
v-for="(msg, index) in messages"
:key="index"
class="text-sm py-1 border-b border-gray-200 last:border-0"
>
{{ msg }}
</div>
<div v-if="messages.length === 0" class="text-gray-400 text-sm">
暂无消息
</div>
</div>
</div>
<!-- 使用说明 -->
<div class="mt-6 p-4 bg-blue-50 rounded">
<h3 class="font-semibold mb-2">使用说明:</h3>
<ol class="text-sm space-y-1 list-decimal list-inside">
<li>打开多个标签页访问此页面</li>
<li>选择不同的通信方式</li>
<li>在任意标签页点击"登录"或"登出"</li>
<li>观察其他标签页的状态同步</li>
</ol>
</div>
</div>
</template>浏览器兼容性
BroadcastChannel
- ✅ Chrome: 54+ (2016年)
- ✅ Firefox: 38+ (2015年)
- ✅ Edge: 79+ (2020年)
- ❌ Safari: 不支持
- ✅ Opera: 41+ (2016年)
Safari 兼容性
Safari 目前不支持 BroadcastChannel API。如需支持 Safari,建议使用 LocalStorage 事件或 polyfill。
LocalStorage 事件
- ✅ Chrome: 所有版本
- ✅ Firefox: 所有版本
- ✅ Safari: 所有版本
- ✅ Edge: 所有版本
- ✅ 移动端: 全部支持
最佳兼容性
LocalStorage 事件是兼容性最好的方案,适合需要广泛浏览器支持的项目。
Service Worker
- ✅ Chrome: 40+ (2015年)
- ✅ Firefox: 44+ (2016年)
- ✅ Safari: 11.1+ (2018年)
- ✅ Edge: 17+ (2017年)
- ✅ 移动端: iOS Safari 11.3+, Chrome Android
HTTPS 要求
Service Worker 需要 HTTPS 环境(localhost 除外)。
最佳实践
1. 选择合适的方案
typescript
// 根据浏览器支持选择最佳方案
function getBestCommunicationMethod(): 'broadcast' | 'storage' | 'serviceworker' {
if ('BroadcastChannel' in window) {
return 'broadcast'
} else if ('serviceWorker' in navigator) {
return 'serviceworker'
} else {
return 'storage'
}
}2. 消息去重
typescript
// 避免重复处理消息
const processedMessages = new Set<string>()
function handleMessage(message: any) {
const messageId = `${message.type}-${message.timestamp}`
if (processedMessages.has(messageId)) {
return // 已处理过,跳过
}
processedMessages.add(messageId)
// 处理消息
processMessage(message)
// 清理旧消息 ID(保留最近 100 条)
if (processedMessages.size > 100) {
const firstId = processedMessages.values().next().value
processedMessages.delete(firstId)
}
}3. 错误处理
typescript
// 安全的消息发送
function safeSendMessage(type: string, payload: any) {
try {
if (channel) {
channel.postMessage({ type, payload })
}
} catch (error) {
console.error('发送消息失败:', error)
// 降级到 localStorage
fallbackToStorage(type, payload)
}
}
function fallbackToStorage(type: string, payload: any) {
try {
const message = { type, payload, timestamp: Date.now() }
localStorage.setItem('fallback-message', JSON.stringify(message))
localStorage.removeItem('fallback-message')
} catch (error) {
console.error('降级方案也失败:', error)
}
}4. 性能优化
typescript
// 节流发送消息
import { throttle } from 'lodash-es'
const throttledSend = throttle((type: string, payload: any) => {
channel?.postMessage({ type, payload })
}, 100)
// 使用
throttledSend('SCROLL_POSITION', { x: 0, y: 100 })5. 类型安全
typescript
// 定义消息类型
interface Message {
type: 'USER_LOGIN' | 'USER_LOGOUT' | 'CART_UPDATE'
payload?: any
timestamp: number
}
// 类型安全的消息发送
function sendTypedMessage(message: Message) {
channel?.postMessage(message)
}
// 类型安全的消息处理
function handleTypedMessage(message: Message) {
switch (message.type) {
case 'USER_LOGIN':
// TypeScript 知道这里的类型
handleLogin(message.payload)
break
case 'USER_LOGOUT':
handleLogout()
break
case 'CART_UPDATE':
updateCart(message.payload)
break
}
}注意事项
1. 安全性
- 不要通过标签页通信传输敏感信息(密码、令牌等)
- 验证消息来源和内容
- 使用类型检查防止恶意消息
2. 性能考虑
- 避免频繁发送大量消息
- 使用节流或防抖优化高频消息
- 及时清理不需要的监听器
3. 调试技巧
typescript
// 开发环境下记录所有消息
if (import.meta.env.DEV) {
channel.onmessage = (event) => {
console.log('[Tab Communication]', event.data)
handleMessage(event.data)
}
}4. 清理资源
typescript
// 组件卸载时清理
onUnmounted(() => {
channel?.close()
window.removeEventListener('storage', handleStorage)
})
// 页面关闭时清理
window.addEventListener('beforeunload', () => {
channel?.close()
})