跨标签页通信方法 - 多元展示与代码案例

发布于:2025-04-20 ⋅ 阅读:(50) ⋅ 点赞:(0)

1. localStorage 和 StorageEvent

标签页 B
标签页 A
触发storage事件
处理数据
监听storage事件
localStorage.setItem
写入数据

📌 代码案例

标签页 A (sender.html):

<!DOCTYPE html>
<html>
<head>
  <title>LocalStorage发送方</title>
</head>
<body>
  <h2>发送消息到其他标签页</h2>
  <input type="text" id="messageInput" placeholder="输入消息">
  <button id="sendBtn">发送</button>

  <script>
    document.getElementById('sendBtn').addEventListener('click', function() {
      const message = document.getElementById('messageInput').value;
      
      // ⭐ 核心部分:将消息写入localStorage
      localStorage.setItem('cross-tab-message', JSON.stringify({
        text: message,
        timestamp: Date.now(),
        sender: 'Tab A'
      }));
      
      alert('消息已发送!请查看其他标签页');
    });
  </script>
</body>
</html>

标签页 B (receiver.html):

<!DOCTYPE html>
<html>
<head>
  <title>LocalStorage接收方</title>
  <style>
    .message { padding: 10px; margin: 5px; background: #f0f0f0; border-radius: 5px; }
  </style>
</head>
<body>
  <h2>接收来自其他标签页的消息</h2>
  <div id="messages"></div>

  <script>
    const messagesContainer = document.getElementById('messages');
    
    // ⭐ 核心部分:监听storage事件
    window.addEventListener('storage', function(event) {
      // 只处理我们关心的键
      if (event.key === 'cross-tab-message') {
        const data = JSON.parse(event.newValue);
        
        // 在页面上显示收到的消息
        const messageElement = document.createElement('div');
        messageElement.className = 'message';
        messageElement.innerHTML = `
          <strong>${data.sender}</strong> 说:
          <p>${data.text}</p>
          <small>${new Date(data.timestamp).toLocaleTimeString()}</small>
        `;
        
        messagesContainer.appendChild(messageElement);
      }
    });
  </script>
</body>
</html>

核心原理

  • 写入触发事件:标签页A写入localStorage时,标签页B会自动触发storage事件
  • 同源策略:只能在相同域名下的标签页之间通信
  • ⚠️ 注意:在修改数据的当前页面不会触发storage事件

2. BroadcastChannel API

Channel: 'app_channel'
标签页 A
标签页 B
标签页 C
广播消息
广播消息
监听message事件
创建相同名称频道
监听message事件
创建相同名称频道
发送消息
创建频道

📌 代码案例

共享JavaScript (broadcast-channel.js):

// 创建或获取广播频道实例
function getBroadcastChannel() {
  // ⭐ 核心部分:创建相同名称的频道
  return new BroadcastChannel('app_communication_channel');
}

// 发送消息到所有标签页
function sendMessage(message) {
  const channel = getBroadcastChannel();
  
  // ⭐ 核心部分:发送消息
  channel.postMessage({
    data: message,
    timestamp: Date.now(),
    sender: document.title || 'Unknown Tab'
  });
}

// 注册消息处理函数
function onMessage(callback) {
  const channel = getBroadcastChannel();
  
  // ⭐ 核心部分:监听消息
  channel.onmessage = function(event) {
    callback(event.data);
  };
  
  return channel; // 返回频道实例,以便可以关闭
}

// 关闭频道
function closeChannel(channel) {
  if (channel) {
    channel.close();
  }
}

在页面中使用 (any-page.html):

<!DOCTYPE html>
<html>
<head>
  <title>BroadcastChannel示例</title>
  <style>
    .message-container { 
      border: 1px solid #ccc; 
      padding: 10px; 
      height: 300px; 
      overflow-y: auto; 
    }
    .message { 
      margin: 5px 0; 
      padding: 8px; 
      background-color: #f0f8ff; 
      border-radius: 4px; 
    }
    .my-message { background-color: #e6ffe6; }
  </style>
</head>
<body>
  <h2>BroadcastChannel通信演示</h2>
  
  <div class="message-container" id="messageContainer"></div>
  
  <div style="margin-top: 10px;">
    <input type="text" id="messageInput" placeholder="输入消息">
    <button id="sendButton">发送</button>
  </div>
  
  <script src="broadcast-channel.js"></script>
  <script>
    const messageInput = document.getElementById('messageInput');
    const sendButton = document.getElementById('sendButton');
    const messageContainer = document.getElementById('messageContainer');
    
    // 设置页面标题作为发送者ID
    document.title = `标签页 ${Math.floor(Math.random() * 1000)}`;
    
    // 注册消息处理函数
    const channel = onMessage(function(data) {
      // 如果是本标签页发送的消息,则不处理
      const isMine = data.sender === document.title;
      
      // 创建消息元素
      const messageEl = document.createElement('div');
      messageEl.className = `message ${isMine ? 'my-message' : ''}`;
      messageEl.innerHTML = `
        <strong>${isMine ? '我' : data.sender}</strong>: 
        ${data.data}
        <small style="display: block; text-align: right; font-size: 0.8em;">
          ${new Date(data.timestamp).toLocaleTimeString()}
        </small>
      `;
      
      messageContainer.appendChild(messageEl);
      messageContainer.scrollTop = messageContainer.scrollHeight;
    });
    
    // 发送按钮点击事件
    sendButton.addEventListener('click', function() {
      const message = messageInput.value.trim();
      if (message) {
        sendMessage(message);
        messageInput.value = '';
        messageInput.focus();
      }
    });
    
    // 按回车键发送
    messageInput.addEventListener('keypress', function(e) {
      if (e.key === 'Enter') {
        sendButton.click();
      }
    });
    
    // 页面关闭时关闭频道
    window.addEventListener('beforeunload', function() {
      closeChannel(channel);
    });
  </script>
</body>
</html>

核心原理

  • 频道订阅:所有标签页订阅同一个命名频道
  • 消息广播:一个标签页发送的消息会被所有订阅者接收
  • 🔄 双向通信:任何标签页都可以发送和接收消息

3. SharedWorker

标签页 C
标签页 B
标签页 A
端口连接
端口连接
端口连接
发送消息
发送消息
发送消息
广播
广播
广播
通信端口
创建Worker
通信端口
创建Worker
通信端口
创建Worker
共享Worker

📌 代码案例

共享Worker文件 (shared-worker.js):

// ⭐ 核心部分:维护所有连接的端口和状态
const connections = [];
let connectionCounter = 0;
let messageHistory = [];  // 保存最近的消息历史

// ⭐ 核心部分:处理新连接
self.onconnect = function(e) {
  // 获取通信端口
  const port = e.ports[0];
  const connectionId = ++connectionCounter;
  
  // 存储连接信息
  connections.push({
    port: port,
    id: connectionId,
    name: `Tab ${connectionId}`
  });
  
  console.log(`新连接建立: ${connectionId}, 当前连接数: ${connections.length}`);
  
  // 发送连接成功消息和历史记录
  port.postMessage({
    type: 'CONNECTED',
    data: {
      connectionId: connectionId,
      totalConnections: connections.length,
      history: messageHistory.slice(-10) // 只发送最近10条消息
    }
  });
  
  // 广播有新连接加入的消息
  broadcastMessage({
    type: 'SYSTEM',
    data: `标签页 ${connectionId} 已连接`,
    sender: 'System',
    timestamp: Date.now()
  }, port);
  
  // 监听来自此连接的消息
  port.onmessage = function(e) {
    const message = e.data;
    
    // 判断消息类型
    if (message.type === 'SET_NAME' && message.data) {
      // 更新连接名称
      const connection = connections.find(c => c.id === connectionId);
      if (connection) {
        connection.name = message.data;
      }
    } 
    else if (message.type === 'CHAT') {
      // 添加发送者信息
      message.sender = connections.find(c => c.id === connectionId)?.name || `Tab ${connectionId}`;
      message.timestamp = Date.now();
      
      // 保存到历史记录
      messageHistory.push(message);
      if (messageHistory.length > 100) { // 限制历史记录条数
        messageHistory.shift();
      }
      
      // 广播消息给所有连接
      broadcastMessage(message);
    }
  };
  
  // 处理连接断开
  port.addEventListener('messageerror', function() {
    removeConnection(port);
  });
};

// 广播消息给所有连接(可选排除源端口)
function broadcastMessage(message, excludePort = null) {
  connections.forEach(connection => {
    // 如果指定了排除端口且当前端口就是排除端口,则跳过
    if (excludePort && connection.port === excludePort) {
      return;
    }
    
    try {
      connection.port.postMessage(message);
    } catch (error) {
      console.error('发送消息失败:', error);
      removeConnection(connection.port);
    }
  });
}

// 移除断开的连接
function removeConnection(port) {
  const index = connections.findIndex(c => c.port === port);
  if (index !== -1) {
    const connection = connections[index];
    connections.splice(index, 1);
    
    // 广播连接断开消息
    broadcastMessage({
      type: 'SYSTEM',
      data: `${connection.name} 已断开连接`,
      sender: 'System',
      timestamp: Date.now()
    });
    
    console.log(`连接已断开: ${connection.id}, 剩余连接数: ${connections.length}`);
  }
}

页面代码 (shared-worker-demo.html):

<!DOCTYPE html>
<html>
<head>
  <title>SharedWorker通信示例</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
    #chat-container {
      display: flex;
      flex-direction: column;
      height: 90vh;
      border: 1px solid #ccc;
      border-radius: 5px;
    }
    #header {
      padding: 10px;
      background-color: #f0f0f0;
      border-bottom: 1px solid #ccc;
    }
    #messages {
      flex: 1;
      overflow-y: auto;
      padding: 10px;
    }
    .message {
      margin: 5px 0;
      padding: 8px;
      border-radius: 5px;
    }
    .user-message { background-color: #e6f7ff; }
    .system-message { 
      background-color: #f5f5f5; 
      font-style: italic;
      color: #666;
    }
    .input-area {
      display: flex;
      padding: 10px;
      border-top: 1px solid #ccc;
    }
    #message-input {
      flex: 1;
      padding: 8px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }
    #send-button {
      margin-left: 10px;
      padding: 8px 15px;
      background-color: #1890ff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    #send-button:hover { background-color: #40a9ff; }
    .connection-info {
      font-size: 0.8em;
      color: #888;
    }
  </style>
</head>
<body>
  <div id="chat-container">
    <div id="header">
      <h2>SharedWorker跨标签页通信</h2>
      <div id="connection-info" class="connection-info">连接中...</div>
      <div>
        <label for="tab-name">标签页名称: </label>
        <input type="text" id="tab-name" placeholder="输入名称">
        <button id="set-name-btn">设置</button>
      </div>
    </div>
    
    <div id="messages"></div>
    
    <div class="input-area">
      <input type="text" id="message-input" placeholder="输入消息...">
      <button id="send-button">发送</button>
    </div>
  </div>

  <script>
    // DOM元素
    const messagesEl = document.getElementById('messages');
    const messageInput = document.getElementById('message-input');
    const sendButton = document.getElementById('send-button');
    const connectionInfo = document.getElementById('connection-info');
    const tabNameInput = document.getElementById('tab-name');
    const setNameBtn = document.getElementById('set-name-btn');
    
    // ⭐ 核心部分:创建SharedWorker实例
    let worker;
    let myName = `Tab ${Math.floor(Math.random() * 1000)}`;
    let myConnectionId;
    
    try {
      worker = new SharedWorker('shared-worker.js');
      
      // ⭐ 核心部分:设置消息处理函数
      worker.port.onmessage = function(e) {
        const message = e.data;
        
        if (message.type === 'CONNECTED') {
          myConnectionId = message.data.connectionId;
          tabNameInput.value = myName = `Tab ${myConnectionId}`;
          
          connectionInfo.textContent = `连接ID: ${myConnectionId} | 当前连接数: ${message.data.totalConnections}`;
          
          // 显示历史消息
          if (message.data.history && message.data.history.length > 0) {
            message.data.history.forEach(msg => displayMessage(msg));
            
            // 添加分隔线
            const divider = document.createElement('div');
            divider.className = 'system-message';
            divider.textContent = '------------ 以上是历史消息 ------------';
            messagesEl.appendChild(divider);
          }
        } 
        else if (message.type === 'SYSTEM') {
          displayMessage(message, 'system-message');
        }
        else if (message.type === 'CHAT') {
          displayMessage(message, 'user-message');
        }
      };
      
      // ⭐ 核心部分:启动通信
      worker.port.start();
      
    } catch (error) {
      console.error('创建SharedWorker失败:', error);
      connectionInfo.textContent = '连接失败: ' + error.message;
    }
    
    // 显示消息
    function displayMessage(message, className = '') {
      const messageEl = document.createElement('div');
      messageEl.className = `message ${className}`;
      
      const time = new Date(message.timestamp).toLocaleTimeString();
      
      if (message.type === 'SYSTEM') {
        messageEl.innerHTML = `<strong>系统消息</strong> (${time}): ${message.data}`;
      } else {
        messageEl.innerHTML = `<strong>${message.sender}</strong> (${time}): ${message.data}`;
      }
      
      messagesEl.appendChild(messageEl);
      messagesEl.scrollTop = messagesEl.scrollHeight;
    }
    
    // 发送消息
    function sendMessage() {
      const text = messageInput.value.trim();
      if (!text) return;
      
      // ⭐ 核心部分:通过端口发送消息
      worker.port.postMessage({
        type: 'CHAT',
        data: text
      });
      
      messageInput.value = '';
      messageInput.focus();
    }
    
    // 设置标签页名称
    function setTabName() {
      const name = tabNameInput.value.trim();
      if (name) {
        myName = name;
        worker.port.postMessage({
          type: 'SET_NAME',
          data: name
        });
      }
    }
    
    // 事件监听
    sendButton.addEventListener('click', sendMessage);
    messageInput.addEventListener('keypress', function(e) {
      if (e.key === 'Enter') sendMessage();
    });
    
    setNameBtn.addEventListener('click', setTabName);
    
    // 页面关闭前的清理
    window.addEventListener('beforeunload', function() {
      if (worker && worker.port) {
        // 如果需要,可以在这里发送断开连接的消息
        // worker.port.close(); // SharedWorker端口会自动关闭
      }
    });
  </script>
</body>
</html>

核心原理

  • 中央控制:SharedWorker充当中央消息处理器
  • 端口通信:每个标签页通过唯一端口与Worker通信
  • 👥 连接管理:Worker负责管理所有连接和消息广播

4. Window.postMessage

子窗口
父窗口
发送消息
回复
验证来源
添加message监听器
处理消息
回复消息
获取窗口引用
window.open
postMessage

📌 代码案例

父窗口 (parent.html):

<!DOCTYPE html>
<html>
<head>
  <title>父窗口 - postMessage示例</title>
  <style>
    body { font-family: Arial, sans-serif; padding: 20px; }
    #log-container {
      height: 300px;
      border: 1px solid #ccc;
      padding: 10px;
      overflow-y: auto;
      margin-bottom: 10px;
      background-color: #f9f9f9;
    }
    .log-entry {
      margin: 5px 0;
      padding: 5px;
      border-bottom: 1px solid #eee;
    }
    .incoming { color: green; }
    .outgoing { color: blue; }
    #control-panel {
      display: flex;
      margin-bottom: 10px;
    }
    #message-input {
      flex: 1;
      padding: 8px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }
    button {
      padding: 8px 15px;
      margin-left: 10px;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    #open-btn { background-color: #2196F3; }
    button:hover { opacity: 0.9; }
  </style>
</head>
<body>
  <h2>窗口间通信 - 父窗口</h2>
  
  <div id="control-panel">
    <button id="open-btn">打开子窗口</button>
  </div>
  
  <div id="message-panel" style="display: none;">
    <div id="control-panel">
      <input type="text" id="message-input" placeholder="输入要发送的消息">
      <button id="send-btn">发送</button>
    </div>
  </div>
  
  <div id="log-container"></div>
  
  <script>
    // DOM元素
    const openBtn = document.getElementById('open-btn');
    const messagePanel = document.getElementById('message-panel');
    const messageInput = document.getElementById('message-input');
    const sendBtn = document.getElementById('send-btn');
    const logContainer = document.getElementById('log-container');
    
    // 子窗口引用
    let childWindow = null;
    
    // 显示日志
    function log(message, type = '') {
      const entry = document.createElement('div');
      entry.className = `log-entry ${type}`;
      entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
      logContainer.appendChild(entry);
      logContainer.scrollTop = logContainer.scrollHeight;
    }
    
    // 打开子窗口
    openBtn.addEventListener('click', function() {
      // ⭐ 核心部分:打开子窗口并获取引用
      const childUrl = 'child.html';
      childWindow = window.open(childUrl, 'childWindow', 'width=600,height=400');
      
      if (childWindow) {
        log(`子窗口已打开: ${childUrl}`, 'outgoing');
        messagePanel.style.display = 'block';
        openBtn.disabled = true;
      } else {
        log('无法打开子窗口,可能被浏览器拦截', 'error');
      }
    });
    
    // 发送消息
    sendBtn.addEventListener('click', function() {
      if (!childWindow || childWindow.closed) {
        log('子窗口已关闭,无法发送消息', 'error');
        messagePanel.style.display = 'none';
        openBtn.disabled = false;
        return;
      }
      
      const message = messageInput.value.trim();
      if (!message) return;
      
      // ⭐ 核心部分:使用postMessage发送消息
      childWindow.postMessage({
        type: 'MESSAGE',
        content: message,
        timestamp: Date.now()
      }, '*'); // 生产环境中应指定确切的目标源,而不是'*'
      
      log(`发送到子窗口: ${message}`, 'outgoing');
      messageInput.value = '';
      messageInput.focus();
    });
    
    // 接收消息
    window.addEventListener('message', function(event) {
      // ⭐ 核心部分:验证消息来源
      // 生产环境应验证event.origin
      
      const data = event.data;
      
      if (data && data.type === 'MESSAGE') {
        log(`收到来自子窗口的消息: ${data.content}`, 'incoming');
      } else if (data && data.type === 'READY') {
        log('子窗口已准备好接收消息', 'incoming');
      } else if (data && data.type === 'CLOSED') {
        log('子窗口通知其将关闭', 'incoming');
        messagePanel.style.display = 'none';
        openBtn.disabled = false;
        childWindow = null;
      }
    });
    
    // 检测子窗口关闭
    function checkChildWindow() {
      if (childWindow && childWindow.closed) {
        log('子窗口已关闭', 'incoming');
        messagePanel.style.display = 'none';
        openBtn.disabled = false;
        childWindow = null;
      }
    }
    setInterval(checkChildWindow, 1000);
  </script>
</body>
</html>

子窗口 (child.html):

<!DOCTYPE html>
<html>
<head>
  <title>子窗口 - postMessage示例</title>
  <style>
    body { font-family: Arial, sans-serif; padding: 20px; }
    #log-container {
      height: 200px;
      border: 1px solid #ccc;
      padding: 10px;
      overflow-y: auto;
      margin-bottom: 10px;
      background-color: #f9f9f9;
    }
    .log-entry {
      margin: 5px 0;
      padding: 5px;
      border-bottom: 1px solid #eee;
    }
    .incoming { color: green; }
    .outgoing { color: blue; }
    #control-panel {
      display: flex;
      margin-bottom: 10px;
    }
    #message-input {
      flex: 1;
      padding: 8px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }
    button {
      padding: 8px 15px;
      margin-left: 10px;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    #close-btn { background-color: #f44336; }
    button:hover { opacity: 0.9; }
  </style>
</head>
<body>
  <h2>窗口间通信 - 子窗口</h2>
  
  <div id="control-panel">
    <input type="text" id="message-input" placeholder="输入要回复的消息">
    <button id="reply-btn">回复</button>
    <button id="close-btn">关闭窗口</button>
  </div>
  
  <div id="log-container"></div>
  
  <script>
    // DOM元素
    const messageInput = document.getElementById('message-input');
    const replyBtn = document.getElementById('reply-btn');
    const closeBtn = document.getElementById('close-btn');
    const logContainer = document.getElementById('log-container');
    
    // 显示日志
    function log(message, type = '') {
      const entry = document.createElement('div');
      entry.className = `log-entry ${type}`;
      entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
      logContainer.appendChild(entry);
      logContainer.scrollTop = logContainer.scrollHeight;
    }
    
    // 页面加载完成后通知父窗口
    window.onload = function() {
      // ⭐ 核心部分:通知父窗口子窗口已准备就绪
      if (window.opener) {
        window.opener.postMessage({
          type: 'READY',
          timestamp: Date.now()
        }, '*'); // 生产环境中应指定确切的目标源
        
        log('已通知父窗口子窗口已准备就绪', 'outgoing');
      } else {
        log('无法找到父窗口', 'error');
      }
    };
    
    // 回复消息
    replyBtn.addEventListener('click', function() {
      if (!window.opener) {
        log('父窗口不可访问', 'error');
        return;
      }
      
      const message = messageInput.value.trim();
      if (!message) return;
      
      // ⭐ 核心部分:回复消息给父窗口
      window.opener.postMessage({
        type: 'MESSAGE',
        content: message,
        timestamp: Date.now()
      }, '*'); // 生产环境中应指定确切的目标源
      
      log(`发送到父窗口: ${message}`, 'outgoing');
      messageInput.value = '';
      messageInput.focus();
    });
    
    // 接收消息
    window.addEventListener('message', function(event) {
      // ⭐ 核心部分:验证消息来源
      // 生产环境应验证event.origin
      
      const data = event.data;
      
      if (data && data.type === 'MESSAGE') {
        log(`收到来自父窗口的消息: ${data.content}`, 'incoming');
      }
    });
    
    // 关闭窗口
    closeBtn.addEventListener('click', function() {
      if (window.opener) {
        // 通知父窗口即将关闭
        window.opener.postMessage({
          type: 'CLOSED',
          timestamp: Date.now()
        }, '*');
      }
      
      // 关闭窗口
      window.close();
    });
    
    // 窗口关闭前通知父窗口
    window.addEventListener('beforeunload', function() {
      if (window.opener) {
        window.opener.postMessage({
          type: 'CLOSED',
          timestamp: Date.now()
        }, '*');
      }
    });
  </script>
</body>
</html>

核心原理

  • 窗口引用:通过window.open()获取子窗口引用,子窗口通过window.opener引用父窗口
  • 安全验证:生产环境中应验证event.origin确保消息来源安全
  • 🔒 跨域能力:可以在不同域的窗口间安全通信

5. IndexedDB 轮询

标签页 B
标签页 A
有新消息
无新消息
定期查询
打开数据库
处理新消息
创建事务
打开数据库
存储消息
IndexedDB

📌 代码案例

共享JavaScript (indexeddb-comm.js):

// IndexedDB通信模块

// ⭐ 核心部分:打开数据库连接
function openDatabase() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('CrossTabDB', 1);
    
    // 数据库升级或创建时设置对象仓库
    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      
      // 创建消息存储对象仓库
      if (!db.objectStoreNames.contains('messages')) {
        const store = db.createObjectStore('messages', { keyPath: 'id', autoIncrement: true });
        store.createIndex('timestamp', 'timestamp', { unique: false });
      }
      
      // 创建客户端信息对象仓库
      if (!db.objectStoreNames.contains('clients')) {
        const clientStore = db.createObjectStore('clients', { keyPath: 'id' });
        clientStore.createIndex('lastActive', 'lastActive', { unique: false });
      }
    };
    
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// 客户端ID生成
function generateClientId() {
  return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}

// 获取或创建客户端ID
let clientId = localStorage.getItem('indexeddb_client_id');
if (!clientId) {
  clientId = generateClientId();
  localStorage.setItem('indexeddb_client_id', clientId);
}

// ⭐ 核心部分:注册客户端
async function registerClient(name = null) {
  const db = await openDatabase();
  const transaction = db.transaction(['clients'], 'readwrite');
  const store = transaction.objectStore('clients');
  
  const clientInfo = {
    id: clientId,
    name: name || `标签页 ${clientId.substring(clientId.length - 5)}`,
    lastActive: Date.now()
  };
  
  return new Promise((resolve, reject) => {
    const request = store.put(clientInfo);
    request.onsuccess = () => resolve(clientInfo);
    request.onerror = () => reject(request.error);
  });
}

// 更新客户端活跃状态
async function updateClientActivity() {
  const db = await openDatabase();
  const transaction = db.transaction(['clients'], 'readwrite');
  const store = transaction.objectStore('clients');
  
  return new Promise((resolve) => {
    const request = store.get(clientId);
    request.onsuccess = () => {
      const clientInfo = request.result || { id: clientId };
      clientInfo.lastActive = Date.now();
      store.put(clientInfo);
      resolve();
    };
    request.onerror = () => resolve(); // 忽略错误,继续执行
  });
}

// 清理不活跃的客户端
async function cleanupInactiveClients(maxInactiveTime = 30000) { // 默认30秒
  const db = await openDatabase();
  const transaction = db.transaction(['clients'], 'readwrite');
  const store = transaction.objectStore('clients');
  const index = store.index('lastActive');
  
  const now = Date.now();
  const range = IDBKeyRange.upperBound(now - maxInactiveTime);
  
  return new Promise((resolve) => {
    const request = index.openCursor(range);
    request.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor) {
        store.delete(cursor.value.id);
        cursor.continue();
      } else {
        resolve();
      }
    };
    request.onerror = () => resolve();
  });
}

// ⭐ 核心部分:获取活跃客户端列表
async function getActiveClients() {
  const db = await openDatabase();
  const transaction = db.transaction(['clients'], 'readonly');
  const store = transaction.objectStore('clients');
  
  return new Promise((resolve) => {
    const request = store.getAll();
    request.onsuccess = () => resolve(request.result || []);
    request.onerror = () => resolve([]);
  });
}

// ⭐ 核心部分:发送消息
async function sendMessage(message) {
  const db = await openDatabase();
  const transaction = db.transaction(['messages'], 'readwrite');
  const store = transaction.objectStore('messages');
  
  const messageData = {
    senderId: clientId,
    content: message,
    timestamp: Date.now()
  };
  
  return new Promise((resolve, reject) => {
    const request = store.add(messageData);
    request.onsuccess = () => resolve(request.result); // 返回消息ID
    request.onerror = () => reject(request.error);
  });
}

// ⭐ 核心部分:获取新消息
async function getNewMessages(lastTimestamp = 0) {
  const db = await openDatabase();
  const transaction = db.transaction(['messages'], 'readonly');
  const store = transaction.objectStore('messages');
  const index = store.index('timestamp');
  
  // 查询大于上次检查时间的消息
  const range = IDBKeyRange.lowerBound(lastTimestamp, true);
  
  return new Promise((resolve) => {
    const request = index.openCursor(range);
    const messages = [];
    
    request.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor) {
        messages.push(cursor.value);
        cursor.continue();
      } else {
        resolve(messages);
      }
    };
    
    request.onerror = () => resolve([]);
  });
}

// 清理旧消息
async function cleanupOldMessages(maxAge = 3600000) { // 默认1小时
  const db = await openDatabase();
  const transaction = db.transaction(['messages'], 'readwrite');
  const store = transaction.objectStore('messages');
  const index = store.index('timestamp');
  
  const now = Date.now();
  const range = IDBKeyRange.upperBound(now - maxAge);
  
  return new Promise((resolve) => {
    const request = index.openCursor(range);
    request.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor) {
        store.delete(cursor.value.id);
        cursor.continue();
      } else {
        resolve();
      }
    };
    request.onerror = () => resolve();
  });
}

// 导出API
window.IndexedDBComm = {
  clientId,
  registerClient,
  updateClientActivity,
  cleanupInactiveClients,
  getActiveClients,
  sendMessage,
  getNewMessages,
  cleanupOldMessages
};

页面实现 (indexeddb-demo.html):

<!DOCTYPE html>
<html>
<head>
  <title>IndexedDB跨标签页通信</title>
  <style>
    body { font-family: Arial, sans-serif; padding: 20px; }
    .container {
      display: flex;
      height: 80vh;
    }
    .chat-panel {
      flex: 3;
      display: flex;
      flex-direction: column;
      border: 1px solid #ccc;
      border-radius: 5px;
      margin-right: 10px;
    }
    .sidebar {
      flex: 1;
      border: 1px solid #ccc;
      border-radius: 5px;
      padding: 10px;
    }
    .header {
      padding: 10px;
      background-color: #f0f0f0;
      border-bottom: 1px solid #ddd;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .messages {
      flex: 1;
      overflow-y: auto;
      padding: 10px;
    }
    .message {
      margin: 8px 0;
      padding: 8px;
      border-radius: 5px;
      max-width: 80%;
    }
    .message.incoming {
      background-color: #f1f0f0;
      align-self: flex-start;
    }
    .message.outgoing {
      background-color: #dcf8c6;
      align-self: flex-end;
      margin-left: auto;
    }
    .system-message {
      color: #888;
      font-style: italic;
      text-align: center;
      margin: 5px 0;
    }
    .input-area {
      display: flex;
      padding: 10px;
      border-top: 1px solid #ddd;
    }
    #message-input {
      flex: 1;
      padding: 8px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }
    button {
      background-color: #4CAF50;
      color: white;
      border: none;
      padding: 8px 15px;
      margin-left: 10px;
      border-radius: 4px;
      cursor: pointer;
    }
    button:hover { opacity: 0.9; }
    .client-list {
      margin-top: 10px;
    }
    .client-item {
      padding: 5px;
      margin: 5px 0;
      background-color: #f9f9f9;
      border-radius: 3px;
    }
    .current-client { font-weight: bold; background-color: #e6f7ff; }
    .status {
      font-size: 0.8em;
      color: #666;
      margin-top: 5px;
    }
  </style>
</head>
<body>
  <h2>IndexedDB跨标签页通信演示</h2>
  
  <div class="container">
    <div class="chat-panel">
      <div class="header">
        <div>
          <span>当前标签页:</span>
          <input type="text" id="client-name" placeholder="输入标签页名称">
          <button id="set-name-btn">设置</button>
        </div>
        <div class="status" id="status">初始化中...</div>
      </div>
      
      <div class="messages" id="messages"></div>
      
      <div class="input-area">
        <input type="text" id="message-input" placeholder="输入消息...">
        <button id="send-btn">发送</button>
      </div>
    </div>
    
    <div class="sidebar">
      <h3>活跃标签页</h3>
      <div class="client-list" id="client-list">加载中...</div>
      
      <div style="margin-top: 20px;">
        <h3>统计信息</h3>
        <div id="stats">
          <div>消息总数: <span id="message-count">0</span></div>
          <div>标签页总数: <span id="client-count">0</span></div>
          <div>上次更新: <span id="last-update">-</span></div>
        </div>
      </div>
    </div>
  </div>
  
  <script src="indexeddb-comm.js"></script>
  <script>
    // DOM元素
    const clientNameInput = document.getElementById('client-name');
    const setNameBtn = document.getElementById('set-name-btn');
    const messageInput = document.getElementById('message-input');
    const sendBtn = document.getElementById('send-btn');
    const messagesContainer = document.getElementById('messages');
    const clientListContainer = document.getElementById('client-list');
    const statusEl = document.getElementById('status');
    const messageCountEl = document.getElementById('message-count');
    const clientCountEl = document.getElementById('client-count');
    const lastUpdateEl = document.getElementById('last-update');
    
    // 使用IndexedDBComm API
    const { 
      clientId, 
      registerClient, 
      updateClientActivity, 
      cleanupInactiveClients, 
      getActiveClients, 
      sendMessage, 
      getNewMessages, 
      cleanupOldMessages 
    } = window.IndexedDBComm;
    
    // 应用状态
    let lastCheckTimestamp = 0;
    let activeClients = [];
    let currentClientInfo = null;
    let messageCount = 0;
    let pollInterval;
    
    // 初始化应用
    async function initApp() {
      try {
        // 注册当前客户端
        currentClientInfo = await registerClient();
        clientNameInput.value = currentClientInfo.name;
        
        // 显示一条欢迎消息
        displaySystemMessage(`欢迎!你的标签页ID是: ${clientId.substring(clientId.length - 5)}`);
        
        // 开始轮询和维护
        startPolling();
        startMaintenance();
        
        updateStatus('已连接');
      } catch (error) {
        console.error('初始化失败:', error);
        updateStatus('初始化失败: ' + error.message, true);
      }
    }
    
    // ⭐ 核心部分:开始轮询新消息和活跃客户端
    function startPolling() {
      // 立即执行一次
      pollForUpdates();
      
      // 设置轮询间隔,每秒检查一次
      pollInterval = setInterval(pollForUpdates, 1000);
    }
    
    // ⭐ 核心部分:轮询检查更新
    async function pollForUpdates() {
      try {
        // 更新客户端活跃状态
        await updateClientActivity();
        
        // 获取新消息
        const newMessages = await getNewMessages(lastCheckTimestamp);
        if (newMessages.length > 0) {
          processNewMessages(newMessages);
          lastCheckTimestamp = Math.max(...newMessages.map(msg => msg.timestamp));
        }
        
        // 更新活跃客户端列表
        activeClients = await getActiveClients();
        updateClientList();
        
        // 更新统计信息
        updateStats();
      } catch (error) {
        console.error('轮询更新失败:', error);
      }
    }
    
    // 处理新消息
    function processNewMessages(messages) {
      messages.forEach(message => {
        // 过滤掉自己发的消息(因为自己发送时已经显示了)
        if (message.senderId !== clientId) {
          // 查找发送者信息
          const sender = activeClients.find(client => client.id === message.senderId);
          const senderName = sender ? sender.name : '未知标签页';
          
          displayMessage(message.content, senderName, false, new Date(message.timestamp));
        }
      });
      
      // 更新消息计数
      messageCount += messages.length;
    }
    
    // 维护任务:清理不活跃客户端和旧消息
    function startMaintenance() {
      // 每30秒执行一次维护
      setInterval(async () => {
        try {
          // 清理60秒内不活跃的客户端
          await cleanupInactiveClients(60000);
          
          // 清理1小时前的旧消息
          await cleanupOldMessages(3600000);
        } catch (error) {
          console.error('维护任务失败:', error);
        }
      }, 30000);
    }
    
    // 发送消息
    async function sendMessageHandler() {
      const content = messageInput.value.trim();
      if (!content) return;
      
      try {
        // 发送消息
        await sendMessage(content);
        
        // 在本地显示消息
        displayMessage(content, '我', true);
        
        // 清空输入框
        messageInput.value = '';
        messageInput.focus();
        
        // 更新消息计数
        messageCount++;
      } catch (error) {
        console.error('发送消息失败:', error);
        alert('发送消息失败: ' + error.message);
      }
    }
    
    // 设置标签页名称
    async function setClientName() {
      const name = clientNameInput.value.trim();
      if (!name) return;
      
      try {
        currentClientInfo = await registerClient(name);
        displaySystemMessage(`你的标签页名称已更改为: ${name}`);
      } catch (error) {
        console.error('设置名称失败:', error);
        alert('设置名称失败: ' + error.message);
      }
    }
    
    // 显示消息
    function displayMessage(content, sender, isOutgoing, timestamp = new Date()) {
      const messageEl = document.createElement('div');
      messageEl.className = `message ${isOutgoing ? 'outgoing' : 'incoming'}`;
      
      const timeStr = timestamp.toLocaleTimeString();
      
      messageEl.innerHTML = `
        <div><strong>${sender}</strong> (${timeStr}):</div>
        <div>${content}</div>
      `;
      
      messagesContainer.appendChild(messageEl);
      messagesContainer.scrollTop = messagesContainer.scrollHeight;
    }
    
    // 显示系统消息
    function displaySystemMessage(content) {
      const messageEl = document.createElement('div');
      messageEl.className = 'system-message';
      messageEl.textContent = content;
      
      messagesContainer.appendChild(messageEl);
      messagesContainer.scrollTop = messagesContainer.scrollHeight;
    }
    
    // 更新客户端列表
    function updateClientList() {
      clientListContainer.innerHTML = '';
      
      if (activeClients.length === 0) {
        clientListContainer.textContent = '没有活跃的标签页';
        return;
      }
      
      activeClients.forEach(client => {
        const clientEl = document.createElement('div');
        clientEl.className = `client-item ${client.id === clientId ? 'current-client' : ''}`;
        
        const lastActive = new Date(client.lastActive).toLocaleTimeString();
        
        clientEl.innerHTML = `
          <div>${client.name || '未命名标签页'}</div>
          <small>ID: ${client.id.substring(client.id.length - 5)}</small>
          <small>最后活跃: ${lastActive}</small>
        `;
        
        clientListContainer.appendChild(clientEl);
      });
      
      clientCountEl.textContent = activeClients.length;
    }
    
    // 更新状态显示
    function updateStatus(message, isError = false) {
      statusEl.textContent = message;
      if (isError) {
        statusEl.style.color = 'red';
      } else {
        statusEl.style.color = '';
      }
    }
    
    // 更新统计信息
    function updateStats() {
      messageCountEl.textContent = messageCount;
      clientCountEl.textContent = activeClients.length;
      lastUpdateEl.textContent = new Date().toLocaleTimeString();
    }
    
    // 事件监听
    sendBtn.addEventListener('click', sendMessageHandler);
    messageInput.addEventListener('keypress', function(e) {
      if (e.key === 'Enter') sendMessageHandler();
    });
    
    setNameBtn.addEventListener('click', setClientName);
    
    // 页面关闭前清理
    window.addEventListener('beforeunload', function() {
      clearInterval(pollInterval);
    });
    
    // 初始化应用
    initApp();
  </script>
</body>
</html>

核心原理

  • 共享数据库:所有标签页访问同一IndexedDB数据库
  • 轮询机制:定期检查数据库变化以发现新消息
  • 活跃状态:通过定期更新时间戳来维护客户端活跃状态
  • 📊 结构化存储:使用对象仓库分别存储消息和客户端信息

方法对比表

通信方法 优点 缺点 适用场景 数据大小 兼容性
localStorage • 简单易用
• 良好兼容性
• 无需额外文件
• 仅限字符串数据
• 存储空间有限
• 同页面不触发
• 简单状态同步
• 配置更改广播
• 用户设置共享
~5MB 几乎所有浏览器
BroadcastChannel • API直观
• 实时通信
• 支持复杂数据
• 兼容性较差
• 仅同源可用
• 实时协作
• 即时通知
• 简单聊天应用
无明确限制 现代浏览器
SharedWorker • 高效通信
• 中央控制
• 支持复杂逻辑
• 需单独文件
• 配置复杂
• 调试困难
• 复杂应用
• 需后台处理
• 高频通信
无明确限制 大部分现代浏览器
postMessage • 跨域支持
• 安全性高
• 兼容性好
• 需窗口引用
• 架构受限
• 父子窗口通信
• iframe通信
• 跨域应用
无明确限制 几乎所有浏览器
IndexedDB • 大容量存储
• 结构化数据
• 离线支持
• 轮询开销
• 代码复杂
• 实时性差
• 大量数据同步
• 离线应用
• 低频更新场景
几百MB甚至GB 大部分现代浏览器

选择建议

现代浏览器
需兼容旧浏览器
简单
复杂
简单数据
复杂/大量数据
需要跨标签页通信
是否需要跨域?
Window.postMessage
对实时性要求?
浏览器兼容性要求?
数据复杂度?
通信复杂度?
localStorage
BroadcastChannel
SharedWorker
IndexedDB

综合性建议

  1. 快速实现:首选 localStorage 或 BroadcastChannel

    • localStorage 几乎没有兼容性问题
    • BroadcastChannel 代码最简洁直观
  2. 大型应用:考虑 SharedWorker 或 IndexedDB

    • SharedWorker 适合需要实时性和复杂逻辑
    • IndexedDB 适合需要大量数据存储和离线支持
  3. 最佳实践

    • ⚠️ 添加错误处理和重试机制
    • ⚠️ 避免发送过大的数据量
    • ⚠️ 使用类型化数据格式,保证数据一致性
    • ⚠️ 考虑添加消息去重机制
  4. 性能优化

    • 合并连续消息减少通信频率
    • 使用节流或防抖限制更新频率
    • 优化轮询间隔,平衡实时性和资源消耗

网站公告

今日签到

点亮在社区的每一天
去签到