1. localStorage 和 StorageEvent
📌 代码案例
标签页 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
📌 代码案例
共享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
📌 代码案例
共享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
📌 代码案例
父窗口 (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 轮询
📌 代码案例
共享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 | 大部分现代浏览器 |
选择建议
综合性建议
快速实现:首选 localStorage 或 BroadcastChannel
- localStorage 几乎没有兼容性问题
- BroadcastChannel 代码最简洁直观
大型应用:考虑 SharedWorker 或 IndexedDB
- SharedWorker 适合需要实时性和复杂逻辑
- IndexedDB 适合需要大量数据存储和离线支持
最佳实践
- ⚠️ 添加错误处理和重试机制
- ⚠️ 避免发送过大的数据量
- ⚠️ 使用类型化数据格式,保证数据一致性
- ⚠️ 考虑添加消息去重机制
性能优化
- 合并连续消息减少通信频率
- 使用节流或防抖限制更新频率
- 优化轮询间隔,平衡实时性和资源消耗