Skip to content

跨标签页通信

顾名思义,就是一个标签页发送消息给另一个标签页。

实现跨标签页通信的方式有以下几种:

  • BroadcastChannel
  • Service Worker
  • LocalStorage window.onstorage监听
  • Shared Worker 定时器轮询(setInterval)
  • IndexDB 定时器轮询(setInterval)
  • cookie 定时器轮询(setInterval)
  • window.open window.postMessage
  • Websocket
  • Web Worker

BroadcastChannel

BroadcastChannel可以帮我们创建一个用于广播的通信频道。当所有页面都监听同一频道的信息时,其中某一个页面通过它发送的消息就会被其他所有页面监听到,但前提是同源页面。

html
<!-- index1.html -->
<html>
	<head>
		<title>页面1</title>
	</head>
	<body>
		<input type="text" id="ipt">
		<button id="btn">发送数据</button>
		<script>
        	const ipt = document.querySelector("#ipt");
            const btn = document.querySelector("#btn");
            // 创建一个名字叫load1的BroadcastChannel实例
            const bc = new BroadcastChannel('load1');
            btn.onclick = function () {
                bc.postMessage({
                    value: ipt.value
                });
            }
        </script>
	</body>
</html>
html
<!-- index2.html -->
<html>
	<head>
		<title>页面2</title>
	</head>
	<body>
		<script>
            // 创建一个名字叫load1的BroadcastChannel实例
            const bc = new BroadcastChannel('load1');
            bc.onmessage = function (e) {
                console.log(e.data);
            }
        </script>
	</body>
</html>

Service Worker

Service Worker实际上是浏览器和服务器之间的代理服务器,它最大的特点是在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和拦截作用域范围内的所有页面的HTTP请求。

Service Worker的目的在于离线缓存,转发请求和网络代理。

在已经支持Service Worker的浏览器的版本中,很多特性没有默认开启。如果你发现示例代码在当前版本的浏览器中怎么样都无法正常运行,你可能需要开启一下浏览器的相关配置:

  • Firefox Nightly: 访问 about:config 并设置 dom.serviceWorkers.enabled 的值为 true; 重启浏览器;
  • Chrome Canary: 访问 chrome://flags 并开启 experimental-web-platform-features; 重启浏览器 (注意:有些特性在Chrome中没有默认开放支持);
  • Opera: 访问 opera://flags 并开启 ServiceWorker 的支持; 重启浏览器。
html
<!-- index1.html -->
<html>
	<head>
		<title>页面1</title>
	</head>
	<body>
        <input type="text" id="ipt">
		<button id="btn">发送数据</button>
		<script>
            const ipt = document.querySelector("#ipt");
            const btn = document.querySelector("#btn");
			window.navigator.serviceWorker.register('sw.js').then(() => {
                console.log('service worker注册成功');
            })			
            btn.onclick = function () {
                window.navigator.serviceWorker.controller.postMessage({
                    value: ipt.value
                });
            }
        </script>
	</body>
</html>
js
// sw.js
// 消息会到达这里
self.addEventListener("message", async event => {
    // 首先获取所有注册了service worker的客户端
    const clients = await self.clients.matchAll();
    // 然后遍历所有客户端,将消息转发出去
    clients.forEach(client => {
        client.postMessage(event.data);
    })
})
html
<!-- index2.html -->
<html>
	<head>
		<title>页面2</title>
	</head>
	<body>
		<script>
			window.navigator.serviceWorker.register('sw.js').then(() => {
                console.log('service worker注册成功');
            })	
            window.navigator.serviceWorker.onmessage = function (res) {
                console.log(res);
            }
        </script>
	</body>
</html>

LocalStorage window.onstorage监听

在Web Storage中,每次将一个值存储到本地时,就会触发一个storage事件,该事件只在同一个域下的任何窗口或标签上触发,并且只在已存在的键值发送改变时触发。

由事件监听器发送给回调函数的事件对象有几个属性如下:

  • key:被修改的键
  • newValue:被修改后的新值
  • oldValue:修改前的值
  • storageArea:指向事件监听的storage对象
  • url:触发storage事件的地址
js
// 页面1
localStorage.setItem('name', 'tom')
js
// 页面2
window.onstorage = function (e) {
    console.log(e.key);
    console.log(e.newValue);
    console.log(e.oldValue);
    console.log(e.storageArea);
    console.log(e.url);
}

SharedWorker 定时器轮询(setInterval)

SharedWorker是一种特定类型的worker,它是Web Worker的一种,可以连接多个同源的页面。

html
<!-- index1.html -->
<html>
	<head>
		<title>页面1</title>
	</head>
	<body>
        <input type="text" id="ipt">
		<button id="btn">发送数据</button>
		<script>
            const ipt = document.querySelector("#ipt");
            const btn = document.querySelector("#btn");
			const worker = new SharedWorker('worker.js');
            btn.onclick = function () {
                console.log(ipt.value)
                worker.port.postMessage(ipt.value);
            }
        </script>
	</body>
</html>
js
// worker.js
let result; // 用来存储用户发送过来的信息
onconnect = function (e) {
    let port = e.ports[0];
    port.onmessage = function (events) {
        if (events.data === 'get') {
            port.postMessage(result);
        } else {
            result = events.data;
        }
    }
}
html
<!-- index2.html -->
<html>
	<head>
		<title>页面2</title>
	</head>
	<body>
		<script>
            const worker = new SharedWorker('worker.js');
            worker.port.start();
            worker.port.onmessage = function (e) {
                if(e.data) {
                    console.log('来自worker的数据', e.data);
                }
            }
            setInterval(() =>{
                worker.port.postMessage('get');
            }, 1000)
        </script>
	</body>
</html>

IndexDB 定时器轮询(setInterval)

js
// 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);
        };
    });
}
html
<!-- 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>
    <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>
        let btn = document.querySelector("#addBtn");
        let stuId = document.querySelector("#stuId");
        let stuName = document.querySelector("#stuName");
        let stuAge = document.querySelector("#stuAge");
        openDB("stuDB", 1)
            .then((db)=>{
                btn.onclick = function(){
                    addData(db,"stu",{
                        "stuId" : stuId.value,
                        "stuName" : stuName.value,
                        "stuAge" : stuAge.value,
                    });
                    stuId.value = stuName.value = stuAge.value = "";
                }
            })
    </script>
</body>

</html>
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>
    <style>
        table{
            border: 1px solid;
            border-collapse: collapse;
        }
        table td{
            border: 1px solid;
        }
    </style>
</head>

<body>
    <h1>学生表</h1>
    <table id="tab">
        <!-- <tr>
            <td>学号</td>
            <td>姓名</td>
            <td>年龄</td>
        </tr>
        <tr>
            <td>1</td>
            <td>謝傑</td>
            <td>18</td>
        </tr>
        <tr>
            <td>2</td>
            <td>张三</td>
            <td>19</td>
        </tr> -->
    </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>
            `;
            let 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>

我们可以通过定时器轮询的方式来监听cookie的变化,从而达到一个多标签也通信的目的。

js
// 页面1
document.cookie = "name=tom";
js
// 页面2
let cookie = document.cookie;
setInterval(() =>{
    if (cookie !== document.cookie) {
        console.log('cookie发生改变了');
        cookie = document.cookie;
    }
})

window.open window.postMessage

html
<!-- 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="popBtn">弹出新窗口</button>
    <input type="text" 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; // 用于保存 window.open 打开的窗口的引用
        popBtn.onclick = function(){
            opener = window.open(
                "index2.html", 
                "123123", 
                "height=400,width=400,top=20,resizeable=yes"
            );
        }
        btn.onclick = function(){
            let data = {
                value : content.value
            }
            // data 代表的是要发送的数据,第二个参数是 origin,使用 * 代表所有域
            opener.postMessage(data, "*")
        }
    </script>
</body>

</html>
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>
    <p>这是页面二</p>
    <script>
        window.addEventListener("message",function(e){
            console.log(e.data);
        })
    </script>
</body>

</html>

Websocket

Websocket协议在2008年诞生,在2011年成为国际标准。目前所有浏览器都已经支持了。

它的最大特点就是,客户端可以主动向服务器发送信息。

安装:npm i -save ws

js
// 首先获取到一个 WebSocketServer 类
var WebSocketServer = require("ws").Server;

// 创建 WebSocket 服务器
var wss = new WebSocketServer({
    port : 3000
})

// 该数组用于保存所有的客户端连接实例
var clients = [];

// 当客户端连接上 WebSocket 服务器的时候
// 就会触发 connection 事件,该客户端的实例就会传入此回调函数
wss.on('connection', function(client){
    // 将当前客户端连接实例保存到数组里面
    clients.push(client);
    console.log(`当前有${clients.length}个客户端在线...`);

    // 给传入进来的客户端连接实例绑定一个 message 事件
    client.on('message', function(msg){
        console.log("收到的消息为:" + msg);
        // 接下来需要将接收到的消息推送给其他所有的客户端
        for(var c of clients){
            if(c !== client){
                c.send(msg.toString());
            }
        }
    })

    client.on('close',function(){
        var index = clients.indexOf(this);
        clients.splice(index, 1);
        console.log(`当前有${clients.length}个客户端在线...`);
    })
})

console.log("Web Socket 服务器已经启动....");
html
<!-- 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>
    <input type="text" name="" id="msg">
    <button id="send">发送信息</button>
    <script>
        // 建立 websocket 连接
        var ws = new WebSocket("ws://localhost:3000");
        var send = document.querySelector("#send");
        var msg = document.querySelector("#msg");
        send.onclick = function(){
            if(msg.value.trim() !== ""){
                ws.send(msg.value.trim());
            }
        }
        // 关闭窗口或者窗口刷新的时候
        // 要关闭当前的 websocket 连接
        window.onbeforeunload = function(){
            ws.close();
        }
    </script>
</body>
</html>
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>Document</title>
</head>

<body>
    <script>
        // 建立 websocket 连接
        var ws = new WebSocket("ws://localhost:3000");
        var count = 1; // 用于计数
        ws.onopen = function () {
            ws.onmessage = function (event) {
                var oP = document.createElement("p");
                oP.innerHTML = `第${count}次接收到消息:${event.data}`;
                document.body.appendChild(oP);
                count++;
            }
        }
        // 关闭窗口或者窗口刷新的时候
        // 要关闭当前的 websocket 连接
        window.onbeforeunload = function () {
            ws.close();
        }
    </script>
</body>

</html>

Web Worker

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>Document</title>
</head>

<body>
    <p>计数:<output id="result"></output></p>
    <button id="startBtn">开始工作</button>
    <button id="stopBtn">停止工作</button>
    <script>
        const startBtn = document.getElementById("startBtn");
        const stopBtn = document.getElementById("stopBtn");
        let worker; // 用于存储 worker 线程
        startBtn.onclick = function(){
            worker = new Worker('worker.js');
            worker.onmessage = function(event){
                document.querySelector("#result").innerHTML = event.data;
            }
        }
        stopBtn.onclick = function(){
            worker.terminate();
            worker = null;
        }
    </script>
</body>

</html>
js
// woker.js
let count = 0;
setInterval(function(){
    count++;
    postMessage(count);
}, 1000);