# 跨标签页通信的方案与实践(下)

# 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);
        };
    });
}

测试效果:

HTTP Cookie - HTTP | MDN (mozilla.org) (opens new window)

Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据。浏览器会存储 cookie 并在下次向同一服务器再发起请求时携带并发送到服务器上。

indexDBlocalStorage 一样,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 窗口的引用,通过 引用.postMessagetab2 发送消息

<!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 类似,这里不再演示。