# 跨标签页通信的方案与实践(下)
# IndexDB + 轮询
IndexedDB - Web API | MDN (mozilla.org) (opens new window)
IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。该 API 使用索引实现对数据的高性能搜索。
IndexedDB 使用同源原则,这意味着它把存储绑定到了创建它的站点的源(典型情况下,就是站点的域或是子域),所以它不能被任何其他来源访问。
这种方案中,我们在一个页面中向 IndexDB 中存储数据,但是目前浏览器没有提供监听 IndexDB 数据存储的事件,所以,在另一个页面中,我们采取轮询的方式获取 IndexDB 中的数据。
首先我们在 tab1 中向 IndexDB 中存储数据:
<!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>新增学生</h1>
<div>
<span>学生学号:</span>
<input type="text" name="stuId" id="stuId">
</div>
<div>
<span>学生姓名:</span>
<input type="text" name="stuName" id="stuName">
</div>
<div>
<span>学生年龄:</span>
<input type="text" name="stuAge" id="stuAge">
</div>
<button id="addBtn">新增学生</button>
<script src="./db.js"></script>
<script>
openDB('stuDB', 1)
.then((db) => {
document.getElementById("addBtn").onclick = function () {
addData(db, "stu", { "stuId": stuId.value, "stuName": stuName.value, "stuAge": stuAge.value });
stuId.value = stuName.value = stuAge.value = "";
}
})
</script>
</body>
</html>
在 tab2 中,使用轮询的方式获取 IndexDB 中的数据:
<!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>学生表</h1>
<table id="tab"></table>
<script src="./db.js"></script>
<script>
function render(arr) {
let tab = document.querySelector("#tab");
tab.innerHTML = `
<tr>
<td>学号</td>
<td>姓名</td>
<td>年龄</td>
</tr>
`;
var str = arr.map((item) => {
return `
<tr>
<td>${item.stuId}</td>
<td>${item.stuName}</td>
<td>${item.stuAge}</td>
</tr>
`
}).join("");
tab.innerHTML += str;
}
async function renderTable() {
let db = await openDB('stuDB', 1);
let stuInfo = await getDataByKey(db, "stu");
render(stuInfo);
setInterval(async function () {
let stuInfo2 = await getDataByKey(db, "stu");
if (stuInfo2.length !== stuInfo.length) {
stuInfo = stuInfo2;
render(stuInfo);
}
}, 1000);
}
renderTable();
</script>
</body>
</html>
db.js
/**
* 打开数据库
* @param {object} dbName 数据库的名字
* @param {string} storeName 仓库名称
* @param {string} version 数据库的版本
* @return {object} 该函数会返回一个数据库实例
*/
function openDB(dbName, version = 1) {
return new Promise((resolve, reject) => {
var 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("数据库打开报错");
};
// 数据库有更新时候的回调
request.onupgradeneeded = function (event) {
// 数据库创建或升级的时候会触发
console.log("onupgradeneeded");
db = event.target.result; // 存储数据库对象
var objectStore;
// 创建存储库
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 数据库实例
* @param {string} storeName 仓库名称
* @param {string} data 数据
*/
function addData(db, storeName, data) {
var 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 {string} key 主键值
*/
function getDataByKey(db, storeName, key) {
return new Promise((resolve, reject) => {
var transaction = db.transaction([storeName]); // 事务
var objectStore = transaction.objectStore(storeName); // 仓库对象
var request = objectStore.getAll(); // 通过主键获取数据
request.onerror = function (event) {
console.log("事务失败");
};
request.onsuccess = function (event) {
// console.log("主键查询结果: ", request.result);
resolve(request.result);
};
});
}
测试效果:

# Cookie + 轮询
HTTP Cookie - HTTP | MDN (mozilla.org) (opens new window)
Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据。浏览器会存储 cookie 并在下次向同一服务器再发起请求时携带并发送到服务器上。
与 indexDB 和 localStorage 一样,Cookie 也是受同源策略影响的。但是,Cookie 有一点比较特殊,两个网页一级域名相同,只是二级域名不同,浏览器也允许通过设置document.domain共享 Cookie。
Domain 指定了哪些主机可以接受 Cookie。如果不指定,该属性默认为同一域名设置 cookie,不包含子域名。如果指定了 Domain,则一般包含子域名。因此,指定 Domain 比省略它的限制要少。
例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。
再看我们这个方案,其实就是利用 Cookie 在同源页面中共享的机制来进行通信的。tab1 页面中向 Cookie 中存储数据或删除数据,tab2 中轮询获取 Cookie 并与之前保存的 Cookie 对比,如果不一致则说明 Cookie 被修改过了。
tab1 中向 Cookie 添加数据:
<!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>
<input type="text" class="ipt">
<button class="btn">发送</button>
<script>
const btn = document.querySelector('.btn')
const ipt = document.querySelector('.ipt')
btn.addEventListener('click', function () {
document.cookie = `username=${ipt.value}`
console.log('添加 cookie 成功');
})
</script>
</body>
</html>
tab2 中轮询获取 Cookie 数据:
<!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>
let oldCookie = null
setInterval(() => {
const newCookie = document.cookie
if (newCookie && oldCookie !== newCookie) {
oldCookie = newCookie
alert(newCookie)
}
}, 1000);
</script>
</body>
</html>
测试结果:

# window.postMessage + onMessage
参考:window.postMessage - Web API | MDN (mozilla.org) (opens new window)
window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为 https),端口号(443 为 https 的默认值),以及主机 (两个页面的模数 Document.domain设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。
从广义上讲,一个窗口可以获得对另一个窗口的引用(比如 targetWindow = window.opener),然后在窗口上调用 targetWindow.postMessage() 方法分发一个 MessageEvent消息。接收消息的窗口可以根据需要自由处理此事件。传递给 window.postMessage() 的参数(比如 message)将通过消息事件对象暴露给接收消息的窗口
所以,我们想利用 window.postMessage 通过一个窗口给另一个窗口发送消息的话,只需要获得目标窗口的引用即可。然后通过 window.postMessage 向目标窗口发送消息,并且在目标窗口中监听 message 事件。
在 tab1 中通过 window.open 打开 tab2 并保存 tab2 窗口的引用,通过 引用.postMessage 向 tab2 发送消息
<!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>
<button class="openBtn">打开新窗口</button>
<br />
<input type="text" class="ipt">
<button class="btn">发送</button>
<script>
const btn = document.querySelector('.btn')
const openBtn = document.querySelector('.openBtn')
const ipt = document.querySelector('.ipt')
// 保存 tab2 的引用
let opener = null
openBtn.addEventListener('click', function () {
opener = window.open('tab2.html', "666", "height=400,width=400,top=10,resizable=yes")
})
btn.addEventListener('click', function () {
opener.postMessage(ipt.value, '*')
console.log('已发送');
})
</script>
</body>
</html>
在 tab2 中,监听 message 事件,从事件对象的 data 属性上获取数据:
<!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>
<h3>tab2</h3>
<script>
window.addEventListener('message', function (e) {
console.log('收到消息');
alert(`收到消息:${e.data}`)
})
</script>
</body>
</html>
测试结果:

# Webscoket
WebSocket - Web API | MDN (mozilla.org) (opens new window)
WebSocket 是一种在单个TCP连接上进行全双工通信的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
使用 Webscoket(以下简称 ws),我们可以做到跨页面通信。主要的步骤为:
- 开启 ws 服务,接收客户端的消息
tab1客户端发送消息给 ws- ws 接收到消息之后,将消息主动推送给
tab2 - 在
tab2中监听message事件,获取 ws 主动推送过来的数据
下面我们来实现一个 ws 跨页面通信的 demo
首先编写一个 ws 服务器,在每个客户端连接上 ws 的时候, ws 都会保存这个客户端对象到队列中。当有客户端向 ws 发送消息时,ws 就会将消息转发给客户端队列中的其他客户端。
// 初始化一个 node 项目 npm init -y
// 安装依赖 npm i -save ws
// 获得 WebSocketServer 类型
var WebSocketServer = require('ws').Server;
// 创建 WebSocketServer 对象实例,监听指定端口
var wss = new WebSocketServer({
port: 8080
});
// 创建保存所有已连接到服务器的客户端对象的数组
var clients = [];
// 为服务器添加 connection 事件监听,当有客户端连接到服务端时,立刻将客户端对象保存进数组中
wss.on('connection', function (client) {
// 如果是首次连接
if (clients.indexOf(client) === -1) {
// 就将当前连接保存到数组备用
clients.push(client)
console.log("有" + clients.length + "客户端在线");
// 为每个 client 对象绑定 message 事件,当某个客户端发来消息时,自动触发
client.on('message', function (msg) {
console.log(msg, typeof msg);
console.log('收到消息' + msg)
// 遍历 clients 数组中每个其他客户端对象,并发送消息给其他客户端
for (var c of clients) {
// 排除自己这个客户端连接
if (c !== client) {
// 把消息发给别人
c.send(msg.toString());
}
}
});
// 当客户端断开连接时触发该事件
client.onclose = function () {
var index = clients.indexOf(this);
clients.splice(index, 1);
console.log("有" + clients.length + "客户端在线")
}
}
});
console.log("服务器已启动...");
在 tab1 中,我们主动向 ws 发送消息:
<!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>
<input type="text" id="msg">
<button id="send">发送</button>
<script>
// 建立到服务端 webSoket 连接
var ws = new WebSocket("ws://localhost:8080");
send.onclick = function () {
// 如果 msg 输入框内容不是空的
if (msg.value.trim() != '') {
// 将 msg 输入框中的内容发送给服务器
ws.send(msg.value.trim())
}
}
// 断开 websoket 连接
window.onbeforeunload = function () {
ws.close()
}
</script>
</body>
</html>
在 tab2 中,监听 message 方法,以接收 ws 推送过来的数据:
<!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>
//建立到服务端webSoket连接
var ws = new WebSocket("ws://localhost:8080");
var count = 1;
ws.onopen = function (event) {
// 当有消息发过来时,就将消息放到显示元素上
ws.onmessage = function (event) {
var oP = document.createElement("p");
oP.innerHTML = `第${count}次接收到的消息:${event.data}`;
document.body.appendChild(oP);
count++;
}
}
// 断开 websoket 连接
window.onbeforeunload = function () {
ws.close()
}
</script>
</body>
</html>
测试效果:

# HTTP + 轮询
HTTP | MDN (mozilla.org) (opens new window)
与 ws 不同,HTTP 是无状态的协议,在一次通信之后就会断开连接。
因此,我们可以在 tab1 中发送消息,在 tab2 中轮询发起 http 请求,询问服务器是否有新的消息。实现原理与 ws 类似,这里不再演示。