该笔记的内容主要来自本人在学习渡一课程的配套资料和个人总结而成
浏览器跨标签页通信 本文主要包含以下内容:
什么是跨标签页通信
跨标签页通信常见方案
BroadCast Channel
Service Worker
LocalStorage window.onstorage 监听
Shared Worker 定时器轮询( setInterval )
IndexedDB 定时器轮询( setInterval )
cookie 定时器轮询( setInterval )
window.open、window.postMessage
Websocket
什么是跨标签页通信 面试的时候经常会被问到的一个关于浏览器的问题:
浏览器中如何实现跨标签页通信?
要回答这个问题,首先需要搞懂什么叫做跨标签通信。
其实这个概念也不难理解,现在几乎所有的浏览器都支持多标签页的,我们可以在一个浏览器中打开多个标签页,每个标签页访问不同的网站内容。
因此,跨标签页通信也就非常好理解了,简单来讲就是一个标签页能够发送信息给另一个标签页 。
常见的跨标签页方案如下:
BroadCast Channel
Service Worker
LocalStorage window.onstorage 监听
Shared Worker 定时器轮询( setInterval )
IndexedDB 定时器轮询( setInterval )
cookie 定时器轮询( setInterval )
window.open、window.postMessage
Websocket
跨标签页通信常见方案 下面我们将针对每一种跨标签页通信的方案进行介绍。
注:本文并不会对每一种方案的知识点本身进行详细介绍,只会介绍如何通过该方案实现跨标签页通信。
BroadCast Channel BroadCast Channel 可以帮我们创建一个用于广播 的通信频道。当所有页面都监听同一频道的消息时,其中某一个页面通过它发送的消息就会被其他所有页面收到。但是前提是同源页面 。
index1.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <body > <input type ="text" name ="" id ="content" /> <button id ="btn" > 发送数据</button > <script > const content = document .querySelector ("#content" ); const btn = document .querySelector ("#btn" ); const bc = new BroadcastChannel ("ak" ); btn.addEventListener ("click" , () => { bc.postMessage ({ value : content.value , }); }); </script > </body >
index2.htm 1 2 3 4 5 6 7 8 9 <body > <script > const bc = new BroadcastChannel ("ak" ); bc.onmessage = function (e ) { console .log (e.data ); }; </script > </body >
在上面的代码中,我们在页面一注册了一个名为 ak 的 BroadcastChannel 对象,之后所有的页面也创建同名的 BroadcastChannel 对象,然后就可以通过 postMessage 和 onmessage 方法进行相互通信了。
Service Worker Service Worker 实际上是浏览器和服务器之间的代理服务器 ,它最大的特点是在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和截拦作用域范围内所有页面的 HTTP 请求 。
Service Worker 的目的在于离线缓存,转发请求和网络代理 。
index1.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <body > <input type ="text" name ="" id ="content" /> <button id ="btn" > 发送数据</button > <script > const content = document .querySelector ("#content" ); const btn = document .querySelector ("#btn" ); navigator.serviceWorker .register ("sw.js" ).then (() => { console .log ("service worker 注册成功" ); }); btn.addEventListener ("click" , () => { navigator.serviceWorker .ready .then ((registration ) => { registration.active .postMessage ({ value : content.value , }); }); }); </script > </body >
index2.html 1 2 3 4 5 6 7 8 9 10 11 12 <body > <script > navigator.serviceWorker .register ("sw.js" ).then (() => { console .log ("service worker 注册成功" ); }); navigator.serviceWorker .addEventListener ("message" , (e ) => { console .log (e.data ); }); </script > </body >
sw.js 1 2 3 4 5 6 7 8 self.addEventListener ("message" , async (e) => { const clients = await self.clients .matchAll (); clients.forEach ((client ) => { client.postMessage (e.data .value ); }); });
为什么在 Service Worker 中使用的是 self ?
Service Worker 运行在独立的线程中,与网页的主线程分离
Service Worker 没有 DOM 访问权限,也没有 window 对象
self 提供了访问 Service Worker API 的入口点
LocalStorage window.onstorage 监听在 Web Storage 中,每次将一个值存储到本地存储时,就会触发一个 storage 事件。
由事件监听器发送给回调函数的事件对象有几个自动填充的属性如下:
注意:这个事件只在同一域下的任何窗口或者标签上触发,并且只在被存储的条目改变时触发。
示例如下:这里我们需要打开服务器进行演示,本地文件无法触发 storage 事件
index1.html 1 2 3 4 5 6 7 <body > <script > localStorage .setItem ("name" , "AK" ); localStorage .setItem ("age" , 20 ); console .log ("信息已经设置!" ); </script > </body >
在上面的代码中,我们在该页面下设置了两个 localStorage 本地数据。
index2.html 1 2 3 4 5 6 7 8 9 10 11 <body > <script > window .addEventListener ("storage" , (e ) => { console .log ("修改的键为:" , e.key ); console .log ("修改前的值为:" , e.oldValue ); console .log ("修改后的值为:" , e.newValue ); console .log ("修改的网址为:" , e.url ); console .log ("事件监听对应的Storage对象:" , e.storageArea ); }); </script > </body >
在该页面中我们安装了一个 storage 的事件监听器,安装之后只要是同一域下面的其他 storage 值发生改变,该页面下面的 storage 事件就会被触发 。
Shared Worker 定时器轮询( setInterval )下面是 MDN 关于 SharedWorker 的说明:
SharedWorker 接口代表一种特定类型的 worker ,可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker 。它们实现一个不同于普通 worker 的接口,具有不同的全局作用域,如果要使 SharedWorker 连接到多个不同的页面,这些页面必须是同源的(相同的协议、host 以及端口) 。
index1.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <body > <input type ="text" name ="" id ="content" /> <button id ="btn" > 发送数据</button > <script > const content = document .querySelector ("#content" ); const btn = document .querySelector ("#btn" ); const worker = new SharedWorker ("worker.js" ); btn.onclick = function ( ) { worker.port .postMessage (content.value ); }; </script > </body >
index2.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <body > <script > const worker = new SharedWorker ("worker.js" ); worker.port .start (); worker.port .onmessage = function (e ) { if (e.data ) { console .log (e.data ); } }; setInterval (function ( ) { worker.port .postMessage ("get" ); }, 1000 ); </script > </body >
worker.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 let data = "" ; self.onconnect = function (e ) { console .log ("页面连接上了" ); let port = e.ports [0 ]; port.onmessage = function (e ) { if (e.data === "get" ) { port.postMessage (data); data = "" ; } else { data = e.data ; } }; };
IndexedDB 定时器轮询( setInterval )IndexedDB 是一种底层 API ,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs ))。该 API 使用索引实现对数据的高性能搜索。
通过对 IndexedDB 进行定时器轮询的方式,我们也能够实现跨标签页的通信。
本节需要利用在浏览器离线存储之IndexedDB 中封装的 db.js 文件。
db.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 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 ("数据库打开失败" ); reject (event); }; 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 }); }; }); } function addData (db, storeName, data ) { let request = db .transaction ([storeName], "readwrite" ) .objectStore (storeName) .add (data); request.onsuccess = function (event ) { console .log ("数据写入成功" ); }; request.onerror = function (event ) { console .log ("数据写入失败" ); }; } function getAllData (db, storeName ) { if (!db || !storeName) { return Promise .reject (new Error ("参数无效" )); } return new Promise ((resolve, reject ) => { let transaction = db.transaction ([storeName], "readonly" ); transaction.oncomplete = function ( ) { console .log ("事务完成" ); }; transaction.onerror = function (event ) { console .error ("事务失败:" , event.target .error ); reject (event.target .error ); }; let objectStore = transaction.objectStore (storeName); let request = objectStore.getAll (); request.onsuccess = function (event ) { resolve (request.result ); }; request.onerror = function (event ) { reject (event.target .error ); }; }); }
index1.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <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 > const dom = { stuId : document .getElementById ("stuId" ), stuName : document .getElementById ("stuName" ), stuAge : document .getElementById ("stuAge" ), addBtn : document .getElementById ("addBtn" ), }; openDB ("stuDB" , 1 ).then ((db ) => { dom.addBtn .onclick = function ( ) { addData (db, "stu" , { stuId : dom.stuId .value , stuName : dom.stuName .value , stuAge : dom.stuAge .value , }); dom.stuId .value = dom.stuName .value = dom.stuAge .value = "" ; }; }); </script > </body >
index2.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <title > Document</title > <style > table { border-collapse : collapse; } th , td { border : 1px solid #000 ; padding : 5px 10px ; } </style > </head > <body > <h1 > 学生表</h1 > <table id ="tab" > </table > <script src ="./db.js" > </script > <script > function render (data ) { const table = document .querySelector ("#tab" ); table.innerHTML = ` <tr> <th>学号</th> <th>姓名</th> <th>年龄</th> </tr> ` ; let str = data .map ((stu ) => { return ` <tr> <td>${stu.stuId} </td> <td>${stu.stuName} </td> <td>${stu.stuAge} </td> </tr> ` ; }) .join ("" ); table.innerHTML += str; } async function renderTable ( ) { let db = await openDB ("stuDB" , 1 ); let stuInfo = await getAllData (db, "stu" ); render (stuInfo); setInterval (async function ( ) { let stuInfoNew = await getAllData (db, "stu" ); if (stuInfo.length !== stuInfoNew.length ) { stuInfo = stuInfoNew; render (stuInfo); } }, 1000 ); } renderTable (); </script > </body > </html >
db.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 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 }); }; }); } 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 ("数据写入失败" ); }; } 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 ) { resolve (request.result ); }; }); }
cookie 定时器轮询( setInterval )我们同样可以通过定时器轮询的方式来监听 Cookie 的变化,从而达到一个多标签页通信的目的。
index1.html 1 2 3 4 5 6 7 <body > <script > document .cookie = "name=zhangsan" ; console .log ("cookie 已经设置" ); </script > </body >
index2.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <body > <script > let 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 >
在上面的代码中,我们为 index2.html 设置了一个定时器,之后每过一秒钟都会重新去读取本地的 Cookie 信息,并比较和之前获取到的 Cookie 信息有没有变化,如果有变化就进行更新操作。
window.open、window.postMessage MDN 上是这样介绍 window.postMessage 的:
window.postMessage( ) 方法可以安全地实现跨源通信 。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为 https),端口号(443 为 https 的默认值),以及主机 (两个页面的模数 Document.domain 设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage( ) 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。
从广义上讲,一个窗口可以获得对另一个窗口的引用(比如 targetWindow = window.opener),然后在窗口上调用 targetWindow.postMessage( ) 方法分发一个 MessageEvent 消息。接收消息的窗口可以根据需要自由处理此事件。传递给 window.postMessage( ) 的参数(比如 message )将通过消息事件对象暴露给接收消息的窗口。
index1.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <body > <button id ="popWindowBtn" > 打开新窗口</button > <input type ="text" name ="" id ="content" /> <button id ="sendBtn" > 发送数据</button > <script > const dom = { popWindowBtn : document .querySelector ("#popWindowBtn" ), content : document .querySelector ("#content" ), sendBtn : document .querySelector ("#sendBtn" ), }; let popWindow; dom.popWindowBtn .onclick = function ( ) { popWindow = window .open ( "index2.html" , "页面二" , "width=300,height=300,resizable=yes,top=10" ); }; dom.sendBtn .onclick = function ( ) { let data = { value : dom.content .value , }; popWindow.postMessage (data, "*" ); }; </script > </body >
index2.html 1 2 3 4 5 6 7 8 9 <body > <p > 这是弹出页面</p > <script > window .addEventListener ("message" , (event ) => { console .log (event.data ); }); </script > </body >
在上面的代码中,我们在页面一通过 open 方法打开页面二,然后通过 postMessage 的方式向页面二传递信息。页面二通过监听 message 事件来接收信息。
WebSocket WebSocket 协议在 2008 年诞生,2011 年成为国际标准。所有浏览器都已经支持了。
它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息 ,是真正的双向平等对话,属于服务器推送技术的一种。
server.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 const WebSocketServer = require ("ws" ).Server ;const wss = new WebSocketServer ({ port : 3000 });const clients = [];wss.on ("connection" , function (client ) { clients.push (client); console .log (`当前有${clients.length} 个客户端连接上服务器` ); client.on ("message" , function (msg ) { clients.forEach ((c ) => { if (c !== client) { c.send (msg.toString ()); } }); }); client.on ("close" , function ( ) { let index = clients.indexOf (client); clients.splice (index, 1 ); console .log (`当前有${clients.length} 个客户端连接上服务器` ); }); }); console .log ("Web Socket 服务器启动成功!!!" );
在上面的代码中,我们创建了一个 Websocket 服务器,监听 8080 端口。每一个连接到该服务器的客户端,都会触发服务器的 connection 事件,并且会将此客户端连接实例作为回调函数的参数传入。
我们将所有的客户端连接实例保存到一个数组里面。为该实例绑定了 message 和 close 事件,当某个客户端发来消息时,自动触发 message 事件,然后遍历 clients 数组中每个其他客户端对象,并发送消息给其他客户端。
close 事件在客户端断开连接时会触发,我们要做的事情就是从数组中删除该连接。
index1.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <body > <input type ="text" name ="msg" id ="msg" /> <button id ="send" > 发送信息</button > <script > const doms = { msg : document .querySelector ("#msg" ), send : document .querySelector ("#send" ), }; const ws = new WebSocket ("ws://localhost:3000" ); doms.send .onclick = function ( ) { if (doms.msg .value .trim () !== "" ) { ws.send (doms.msg .value .trim ()); } }; window .onbeforeunload = function ( ) { ws.close (); }; </script > </body >
index2.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <body > <script > const ws = new WebSocket ("ws://localhost:3000" ); let content = 1 ; ws.onopen = function ( ) { ws.onmessage = function (event ) { const pTag = document .createElement ("p" ); pTag.innerHTML = `这是第${content++} 条消息:${event.data} ` ; document .body .appendChild (pTag); }; }; window .onbeforeunload = function ( ) { ws.close (); }; </script > </body >