什么是跨标签页通信
-
其实这个概念也不难理解,现在几乎所有的浏览器都支持多标签页的,可以在一个浏览器中打开多个标签页,每个标签页访问不同的网站内容:
-
因此,跨标签页通信也就非常好理解了,简单来讲就是一个标签页能够发送信息给另一个标签页;
-
常见的跨标签页方案如下:
- BroadCast Channel 广播频道
- Service Worker
- storage + onstorage 监听
- Shared Worker + 定时器轮询
- IndexedDB + 定时器轮询
- cookie + 定时器轮询
- window.open、window.postMessage
- Websocket
跨标签页通信方案
BroadCast Channel
-
BroadCast Channel 可以创建一个用于广播的通信频道;当所有页面都监听同一频道的消息时,其中某一个页面通过它发送的消息就会被其他所有页面收到;但是前提是同源页面;
-
示例代码:
htmlhtml<!-- index.html --> <body> <button id="btn">发送数据</button> <script> const btn = document.querySelector("#btn"); // 创建一个名字是 load1 的 BroadcastChannel 对象 var BroadcastChanne1 = new BroadcastChannel('load1'); btn.onclick = function () { BroadcastChanne1.postMessage({ value: '你好,这是测试数据' }); } </script> </body>
<!-- index2.html --> <body> <script> //要接收到数据,BroadcastChannel 对象的名字必须相同 var BroadcastChanne1 = new BroadcastChannel('load1'); BroadcastChanne1.onmessage = function (e) { console.log(e.data); // 发送的数据 }; </script> </body>
Service Worker
-
Service Worker 实际上是浏览器和服务器之间的代理服务器,它最大的特点是在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和截拦作用域范围内所有页面的 HTTP 请求;
-
Service Worker 的目的在于离线缓存,转发请求和网络代理 (主要应用于 pwa);
-
示例代码:
htmlhtmlJavaScript<!-- index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>页面一</title> </head> <body> <button id="btn">发送数据</button> <script> // 注册 service worker navigator.serviceWorker.register('sw.js') .then(() => { console.log("service worker 注册成功"); }); btn.onclick = function () { navigator.serviceWorker.controller.postMessage({ value: '你好,这是测试数据' }) } </script> </body> </html>
<!-- index2.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>页面二</title> </head> <body> <script> // 注册 service worker navigator.serviceWorker.register('sw.js') .then(() => { console.log("service worker 注册成功"); }); navigator.serviceWorker.onmessage = function ({ data }) { console.log(data); } </script> </body> </html>
// sw.js(中转站) // 消息就会到达这里 self.addEventListener("message", async event => { // 首先获取到所有注册了 service worker 的客户端 const clients = await self.clients.matchAll(); clients.forEach(function (client) { client.postMessage(event.data.value); }) })
storage + onstorage
-
在 Web Storage 中,每次将一个值存储到本地存储时,就会触发一个 storage 事件,由事件监听器发送给回调函数的事件对象有几个自动填充的属性如下:
- key:被修改的条目的键;
- newValue:被修改后的新值;
- oldValue:修改前的值;
- storageArea:指向事件监听对应的 Storage 对象;
- url:原始触发 storage 事件的那个网页的地址;
注意:这个事件只在同一域下的任何窗口或者标签上触发,并且只在被存储的条目改变时触发;
-
示例如下:这里需要打开服务器进行演示,本地文件无法触发 storage 事件
htmlhtml<!-- index.html --> <body> <script> localStorage.name = "张三"; localStorage.age = 20; console.log("信息已经设置!"); </script> </body>
<!-- index2.html --> <body> <script> let name = localStorage.getItem("name"); let age = localStorage.age; console.log(`姓名为${name},年龄为${age}`); window.addEventListener("storage", (e) => { console.log(`${e.key}从${e.oldValue}修改为${e.newValue}`); console.log(e.storageArea); console.log(`被改变的url为${e.url}`); }, true); </script> </body>
Shared Worker + 定时器轮询
-
web Worker 可分为两种类型:
- 专用线程 dedicated web worker:随当前页面的关闭而结束,这意味着 Dedicated web worker 只能被创建它的页面访问;
- 共享线程 shared web worker:共享线程可以同时有多个页面的线程链接;例如几个窗口、iframe 或其他 worker;它们实现一个不同于普通 worker 的接口,具有不同的全局作用域,如果要使 SharedWorker 连接到多个不同的页面,这些页面必须是同源的(相同的协议、host 以及端口);
-
示例代码:
htmlhtmlJavaScript<!-- index.html --> <body> <button id="btn">发送</button> <script> const btn = document.querySelector("#btn"); const worker = new SharedWorker('worker.js'); btn.onclick = function () { worker.port.postMessage('你好,这是测试数据'); } </script> </body>
<!-- index2.html --> <body> <script> const btn = document.querySelector("#btn"); var worker = new SharedWorker('worker.js'); worker.port.start(); worker.port.addEventListener('message', (e) => { if(e.data){ console.log('来自worker的数据:', e.data) } }, false); setInterval(function(){ // 获取和发送消息都是调用 postMessage 方法,这里约定的是传递'get'表示获取数据 worker.port.postMessage('get') },1000); </script> </body>
// worker.js(中转) var data = ''; onconnect = function (e) { var port = e.ports[0] port.onmessage = function (e) { // 如果是 get 则返回数据给客户端 if (e.data === 'get') { port.postMessage(data); data = ""; } else { // 否则把数据保存 data = e.data } } }
IndexedDB + 定时器轮询
-
IndexedDB 是一种底层 API ,用于在客户端存储大量的结构化数据,也包括文件/二进制大型对象 blobs;该 API 使用索引实现对数据的高性能搜索;通过对 IndexedDB 进行定时器轮询的方式,也能够实现跨标签页的通信;
-
示例代码:
htmlhtmlJavaScript<!-- index.html --> <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>
<!-- index2.html --> <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>
// 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 + 定时器轮询
-
同样可以通过定时器轮询的方式来监听 Cookie 的变化,从而达到一个多标签页通信的目的;
-
示例代码:
htmlhtml<!-- index.html --> <body> <script> // 设置 cookie document.cookie = "name=zhangsan"; console.log("cookie 已经设置"); </script> </body>
<!-- index2.html --> <body> <script> // 获取当前的 cookie var cookie = document.cookie; console.log(`当前的 cookie 值为 ${document.cookie}`); setInterval(function(){ if(cookie !== document.cookie){ console.log(`cookie 信息已经改变,最新的 cookie 值为${document.cookie}`); cookie = document.cookie; console.log("最新的 cookie 值已经保存"); } },1000); </script> </body>
window.open、window.postMessage
-
window.postMessage() 方法可以安全地实现跨源通信;
- 通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为 https),端口号(443 为 https 的默认值),以及主机 (两个页面的模数 Document.domain 设置为相同的值) 时,这两个脚本才能相互通信;
- window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全;
-
从广义上讲,一个窗口可以获得对另一个窗口的引用(比如
targetWindow = window.opener
),然后在窗口上调用 targetWindow.postMessage() 方法分发一个 MessageEvent 消息;接收消息的窗口可以根据需要自由处理此事件;传递给 window.postMessage() 的参数(比如 message)将通过消息事件对象暴露给接收消息的窗口; -
示例代码
htmlhtml<!-- index.html --> <body> <button id="popBtn">弹出新的窗口</button> <input type="text" name="" id="content"> <button id="btn">发送数据</button> <script> const popBtn = document.querySelector('#popBtn'); const content = document.querySelector("#content"); const btn = document.querySelector("#btn"); let opener = null; // 保存打开窗口的引用 popBtn.onclick = function () { opener = window.open("index2.html", "123", "height=400,width=400,top=10,resizable=yes"); } btn.onclick = function () { let data = { value: content.value } // data 代表的是发送是数据,origin 用来限制访问来源,也可以用 * 代替 opener.postMessage(data, "*"); } </script> </body>
<!-- index2.html --> <body> <p>这是弹出页面</p> <script> // 事件监听 window.addEventListener('message', function (e) { console.log(e.data); }, false); </script> </body>
Websocket
-
WebSocket 协议在 2008 年诞生,2011 年成为国际标准;所有浏览器都已经支持了;
-
它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种;
-
示例代码
JavaScripthtmlhtml// server.js // 初始化一个 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("服务器已启动...");
<!-- index.html --> <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>
<!-- index2.html --> <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>
浏览器🧑💻 浏览器的缓存
上一篇