Skip to content

标签页通信

浏览器多标签页之间的实时通信解决方案。

使用场景

  • 多标签同步: 用户在多个标签页中操作,需要实时同步状态
  • 单点登录: 一个标签页登出,其他标签页同步登出
  • 实时通知: 一个标签页的操作需要通知其他标签页
  • 数据共享: 多个标签页共享相同的数据状态
  • 协同编辑: 多个标签页同时编辑同一份文档

方案对比

特性BroadcastChannelLocalStorage 事件Service Worker
浏览器支持Chrome 54+, Firefox 38+所有现代浏览器Chrome 40+, Firefox 44+
API 复杂度⭐ 简单⭐⭐ 中等⭐⭐⭐ 复杂
消息类型任意可序列化对象字符串任意可序列化对象
跨域通信❌ 同源❌ 同源✅ 可跨域
离线支持
性能⭐⭐⭐ 高⭐⭐ 中⭐⭐⭐ 高
消息可靠性⭐⭐⭐ 高⭐⭐ 中⭐⭐⭐ 高

BroadcastChannel

基本信息

特点

  • ✅ 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 事件

基本信息

特点

  • ✅ 兼容性最好
  • ✅ 无需额外依赖
  • ✅ 简单易用
  • ⚠️ 只能传输字符串
  • ⚠️ 当前标签页不会触发自己的 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

基本信息

特点

  • ✅ 功能最强大
  • ✅ 支持离线消息
  • ✅ 可以处理复杂的消息路由
  • ✅ 支持跨域通信(需要配置)
  • ⚠️ 配置复杂
  • ⚠️ 需要 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()
})

相关资源