什么是跨标签页通讯
同一浏览器,可以打开多个标签页,跨标签页通讯就是,一个标签页能够发消息给另一标签页。
有哪些实现方案
- localStorage (window.onstorage事件监听)
- BroadcastChannel(广播)
- ServiceWorker (代理服务线程)
- SharedWorker + 轮询
- indexedDB + 轮询
- cookie + 轮询
- window.open + window.postMessage()
- WebSocket + 后端服务
方案一:localStorage
基于storage
事件
页面一(localStorage1.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>localStorage1</title>
</head>
<body>
<h1>localStorage1</h1>
<ul id="ul"></ul>
<script>
/** 更新视图*/
function changeView(){
const ul = document.getElementById('ul');
ul.innerHTML = '';
for(let i = 0; i < localStorage.length; i++){
const key = localStorage.key(i);
const value = localStorage.getItem(key);
const li = document.createElement('li');
li.textContent = `${key}: ${value}`;
ul.appendChild(li);
}
}
/** 数据更新-更新视图*/
function changeData(key,value){
localStorage.setItem(key,value);
changeView();
}
/** 更新localStorage中数据 */
changeData('name','张三')
changeData('age','18')
</script>
</body>
</html>
页面二(localStorage2.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>localStorage1</title>
</head>
<body>
<h1>localStorage2</h1>
<ul id="ul"></ul>
<script>
function changeView(){
const ul = document.getElementById('ul');
ul.innerHTML = '';
for(let i = 0; i < localStorage.length; i++){
const key = localStorage.key(i);
const value = localStorage.getItem(key);
const li = document.createElement('li');
li.textContent = `${key}: ${value}`;
ul.appendChild(li);
}
}
changeView();
//当localStorage发生变化时,会触发storage事件
window.addEventListener('storage',changeView)
</script>
</body>
</html>
方案二:BroadcastChannel
BroadcastChannel可以创建一个用于广播的通信频道,当所有页面都监听同一频道的消息时,其中某一个页面通过它发送的消息就会被其他页面接收到,前提是同源页面。
页面1(channel1.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>BroadcastChannel1-Tom</h1>
<p>接收消息:</p>
<p id="message" style="white-space: pre;"></p>
<input type="text" name="message" id="messageInput">
<button onclick="postMessage()">发送消息</button>
<script>
const messageElement = document.getElementById('message');
const messageInput = document.getElementById('messageInput');
const bc = new BroadcastChannel('channel');
bc.onmessage = function(e) {
console.log('收到消息:', e.data);
messageElement.textContent += '\n'+e.data;
}
function postMessage() {
console.log('发送消息');
const message = messageInput.value;
bc.postMessage(message);
messageInput.value = '';
}
</script>
</body>
</html>
页面2(channel1.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>BroadcastChannel2-Jerry</h1>
<p>接收消息:</p>
<p id="message" style="white-space: pre;"></p>
<input type="text" name="message" id="messageInput">
<button onclick="postMessage()">发送消息</button>
<script>
const messageElement = document.getElementById('message');
const messageInput = document.getElementById('messageInput');
const bc = new BroadcastChannel('channel');
bc.onmessage = function(e) {
console.log('收到消息:', e.data);
messageElement.textContent += '\n'+e.data;
}
function postMessage() {
console.log('发送消息');
const message = messageInput.value;
bc.postMessage(message);
messageInput.value = '';
}
</script>
</body>
</html>
方案三:ServiceWorker
Service worker 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。
sw.js文件-代理服务器
// 消息会先到达这里,然后发送到其他客户端
self.addEventListener('message', async (event)=> {
// 首先获取所有注册了serviceWorker的客户端
self.clients.matchAll().then((clients)=>{
// 遍历所有客户端
clients.forEach((client)=>{
// 向每个客户端发送消息
client.postMessage(event.data);
})
})
});
service1.html文件-客户端1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>service1</h1>
<input type="text" id="content">
<button id="bt">发送</button>
<script>
/**注册serviceWorker*/
navigator.serviceWorker.register('sw.js').then(function(registration){
console.log('service worker 注册成功');
})
const bt = document.getElementById('bt');
bt.addEventListener('click',function(){
const message = document.getElementById('content').value;
// controller控制器发送消息
navigator.serviceWorker.controller.postMessage(message);
})
</script>
</body>
</html>
service2.html文件-客户端2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>service1</h1>
<script>
/**注册同一serviceWorker*/
navigator.serviceWorker.register('sw.js').then(function(registration){
console.log('service worker 注册成功');
})
//监听onmessage事件
navigator.serviceWorker.onmessage = function(event){
console.log('收到消息',event.data);
}
</script>
</body>
</html>
方案四:SharedWorker + 轮询
SharedWorker
是Worker
的一种,它允许你在多个页面之间共享一个Worker
。
shared.js(worker)
let data = "";//存储用户发送的信息
onconnect = (event) => {
const port = event.ports[0];//获取客户端端口
port.onmessage = (event) => {
if (event.data==='get') {
port.postMessage(data);//向客户端发送消息
}else{
data = event.data;//将用户发送的信息存储到data变量中
}
}
}
shared1.html(页面一)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>shared1</h1>
<input type="text" id="content">
<button id="btn">发送</button>
<script>
const btn = document.getElementById('btn');
const content = document.getElementById('content');
const message = document.getElementById('message')
// 创建SharedWorker
const shared = new SharedWorker('shared.js');
btn.onclick = function(){
// 向SharedWorker发送消息
shared.port.postMessage(content.value);
content.value = '';
}
</script>
</body>
</html>
shared2.html(页面二)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>shared1</h1>
<input type="text" id="content">
<button id="btn">发送</button>
<script>
const btn = document.getElementById('btn');
const content = document.getElementById('content');
const message = document.getElementById('message')
// 创建SharedWorker
const shared = new SharedWorker('shared.js');
btn.onclick = function(){
// 向SharedWorker发送消息
shared.port.postMessage(content.value);
content.value = '';
}
</script>
</body>
</html>
方案五:IndexedDB+轮询
对IndexedDB封装:
db.js
/**
* 打开/创建数据库
* @param {string} dbName 数据库名称
* @param {number} version 数据库版本
* @returns
*/
function openDB(dbName, version = 1) {
return new Promise((resolve, reject) => {
let db; // 存储数据库对象
//打开数据库,如果没有就是创建操作
const request = indexedDB.open(dbName, version);
//数据库打开或者创建的时候
request.onsuccess = function (event) {
db = event.target.result;
console.log("数据库打开成功");
resolve(db);
};
//打开失败
request.onerror = function (event) {
console.log("数据库打开失败");
};
//数据库发生更新的时候
//1.版本号更新 2.添加或者删除了表(对象仓库)的时候
//当我们第一次调用open方法时,会触发这个事件
request.onupgradeneeded = function (event) {
console.log("数据库需要更新");
db = event.target.result;
//创建一个对象仓库(表),用于存储数据
let objectStore = db.createObjectStore("stu", {
keyPath: "stuId", //主键
autoIncrement: true, //自增
});
//创建索引,有了索引之后,查询速度大大增快
objectStore.createIndex("stuId", "stuId", { unique: true });
objectStore.createIndex("stuName", "stuName", { unique: false });
objectStore.createIndex("stuAge", "stuAge", { unique: false });
};
});
}
/**
* 关闭数据库
* @param {object} db 数据库实例
*/
function closeDB(db) {
db.close();
console.log("数据库已关闭");
}
/**
* 删除数据库
* @param {string} dbName 数据库名称
*/
function deleteDB(dbName) {
console.log(dbName);
const deleteRequest = window.indexedDB.deleteDatabase(dbName);
deleteRequest.onerror = function (event) {
console.log("删除数据库失败");
};
deleteRequest.onsuccess = function (event) {
console.log("数据库删除成功");
};
}
/**
* 通过主键来读取数据(精确查询)
* @param {object} db 数据库实例
* @param {*} storeName 数据仓库名称
* @param {*} key 主键
*/
function getDataByKey(db, storeName, key) {
return new Promise((resolve, reject) => {
const request = db
.transaction([storeName], "readonly")
.objectStore(storeName)
.get(key);
request.onsuccess = function (event) {
resolve(request.result);
};
request.onerror = function (event) {
console.log("数据查询失败");
};
});
}
/**
* 通过索引来读取数据(精确查询-命中的第一条数据)
* @param {object} db 数据库实例
* @param {string} storeName 数据仓库名称
* @param {string} indexName 索引名称
* @param {any} key 索引对应的值
* @returns
*/
function getDataByIndex(db, storeName, indexName, key) {
return new Promise((resolve, reject) => {
const request = db
.transaction([storeName], "readonly")
.objectStore(storeName)
.index(indexName)
.get(key);
request.onsuccess = function (event) {
resolve(request.result);
};
});
}
/**
* 通过索引和指针获取匹配数据(精确查询-所有命中的数据)
* @param {object} db 数据库实例
* @param {string} storeName 数据仓库实例
* @param {string} indexName 索引名称
* @param {any} key 关键字
* @returns
*/
function getDataByIndexRange(db, storeName, indexName, key) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], "readonly");
const objectStore = transaction.objectStore(storeName);
const index = objectStore.index(indexName);
const request = index.openCursor(IDBKeyRange.only(key));
const data = [];
request.onsuccess = function (event) {
const cursor = event.target.result;
if (cursor) {
data.push(cursor.value);
cursor.continue();
} else {
resolve(data);
}
};
});
}
/**
* 根据游标指针获取数据(全部数据)
* @param {object} db 数据库实例
* @param {string} storeName 数据仓库名称
* @returns
*/
function getDataByCursor(db, storeName) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], "readonly"); //事务
const objectStore = transaction.objectStore(storeName); //仓库
const request = objectStore.openCursor(); //游标
const data = []; //所有数据
request.onsuccess = function (event) {
const cursor = event.target.result;
if (cursor) {
data.push(cursor.value);
cursor.continue();
} else {
resolve(data);
}
};
});
}
/**
* 获取全部数据
* @param {object} db 数据库实例
* @param {string} storeName 数据仓库名称
* @returns
*/
function getAllData(db, storeName) {
return new Promise((resolve, reject) => {
const request = db
.transaction([storeName], "readonly")
.objectStore(storeName)
.getAll();
request.onsuccess = function (event) {
resolve(request.result);
};
request.onerror = function (event) {
console.log("数据查询失败");
};
});
}
/**
* 新增数据
* @param {object} db 数据库实例
* @param {string} storeName 数据仓库名称
* @param {*} data 数据
*/
function addData(db, storeName, data) {
const request = db
.transaction([storeName], "readwrite") //事务
.objectStore(storeName) //获取对象仓库
.add(data); //添加数据
request.onsuccess = function (event) {
console.log("数据添加成功");
};
request.onerror = function (event) {
console.log("数据添加失败");
};
}
/**
* 数据更新(根据主键更新某条数据)
* @param {object} db 数据库实例
* @param {string} storeName 数据仓库名称
* @param {*} data 数据
*/
function upData(db, storeName, data) {
const transaction = db.transaction([storeName], "readwrite");
const objectStore = transaction.objectStore(storeName);
const request = objectStore.put(data);
request.onsuccess = function (event) {
console.log("数据更新成功");
}
request.onerror = function (event) {
console.log("数据更新失败");
}
}
db1.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./db.js"></script>
<script>
let db;
async function initData() {
db = await openDB("stuDB", 1);
addData(db, "stu", { stuId: 1, stuName: "张三", stuAge: 18 });
}
initData();
</script>
</body>
</html>
db2.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./db.js"></script>
<script>
let db;
let dataLength;
async function initDB(name, version) {
db = await openDB("stuDB", 1);
}
initDB();
setInterval(async () => {
if (db) {
const data = await getAllData(db, "stu");
dataLength !== data.length &&console.log(data);
dataLength = data.length
}
}, 1000);
</script>
</body>
</html>
方案六:websocket
这个方案是借助后端,通过websocket通讯。
socket1.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket1</title>
<style>
.message-block { list-style-type: none; margin: 0; padding: 0; }
.message-block li { margin-bottom: 10px; }
.message-block li:before { content: ">"; color: blue; margin-right: 5px; }
</style>
</head>
<body>
<h1>WebSocket1</h1>
<ul class="message-block"></ul>
<form action="" submit="sendMessage">
<input type="text" name="message" id="messageInput">
<button type="submit" id="send">发送</button>
</form>
<script>
const ws = new WebSocket('ws://localhost:8080/socket?userId=Tom');
ws.onopen = function () {
console.log('连接WebSocket');
ws.send('Hello, WebSocket!');
}
ws.onmessage = function (event) {
console.log('接收到消息:', event.data);
const messageBlock = document.querySelector('.message-block');
const li = document.createElement('li');
li.textContent = event.data;
messageBlock.appendChild(li);
}
function sendMessage(e) {
e.preventDefault();
const messageInput = document.getElementById('messageInput');
const message = messageInput.value;
// ws.send(message);
messageInput.value = '';
fetch(`//localhost:8080/message?userId=Jerry&message=${message}`,).then(res=>res.text()).then(data=>{
console.log(data);
})
}
document.getElementById('send').addEventListener('click', sendMessage);
</script>
</body>
</html>
socket2.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket2</title>
<style>
.message-block { list-style-type: none; margin: 0; padding: 0; }
.message-block li { margin-bottom: 10px; }
.message-block li:before { content: ">"; color: blue; margin-right: 5px; }
</style>
</head>
<body>
<h1>WebSocket2</h1>
<ul class="message-block"></ul>
<form action="" submit="sendMessage">
<input type="text" name="message" id="messageInput">
<button type="submit" id="send">发送</button>
</form>
<script>
const ws = new WebSocket('ws://localhost:8080/socket?userId=Jerry');
ws.onopen = function () {
console.log('连接WebSocket');
ws.send('Hello, WebSocket!');
}
ws.onmessage = function (event) {
console.log('接收到消息:', event.data);
const messageBlock = document.querySelector('.message-block');
const li = document.createElement('li');
li.textContent = event.data;
messageBlock.appendChild(li);
}
function sendMessage(e) {
e.preventDefault();
const messageInput = document.getElementById('messageInput');
const message = messageInput.value;
// ws.send(message);
messageInput.value = '';
fetch(`//localhost:8080/message?userId=Tom&message=${message}`,).then(res=>res.text()).then(data=>{
console.log(data);
})
}
document.getElementById('send').addEventListener('click', sendMessage);
</script>
</body>
</html>
node后端服务:
websocket.js
const WebSocket = require("ws");
const express = require("express");
const expressWs = require("express-ws");
const cors = require("cors");
const app = express();
const wsClients = new Set();
app.use(cors());
expressWs(app);
app.ws("/socket", (ws, req) => {
console.log("connected", req.query);
// wsClients[req.query.userId] = ws;
wsClients.add({ userId: req.query.userId, ws: ws });
console.log("🚀 ~ app.ws ~ wsClients:", wsClients);
ws.send("已连接");
});
app.get("/message", (req, res) => {
res.send("服务端收到消息:" + req.query.message);
wsClients.forEach((item) => {
if (item.userId === req.query.userId) {
item.ws.send(req.query.message);
}
});
});
app.listen(8080, () => {
console.log("listening on *:8080");
});
以上所有代码:github