浏览器-跨页面通信

Posted natsu-cc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浏览器-跨页面通信相关的知识,希望对你有一定的参考价值。

前言:

跨页面通信是为了两个不同的页面之间的通信问题,在浏览器中,我们可以同时打开多个Tab页,每个Tab页可以粗略理解为一个“独立”的运行环境,即使是全局对象也不会在多个Tab间共享。然而有些时候,我们希望能在这些“独立”的Tab页面之间同步页面的数据、信息或状态。

同源跨页面通信方法:

BroadcastChannel:

BrocastChannel可以实现同源下浏览器不同窗口,tab页,frame或frame下的浏览器上下文之间的通信,可以实现在同源的多个页面之间广播消息的机制。

// a页面
const bc = new BroadcastChannel('broadcast')
bc.postMessage('发送数据')

// b页面
const bc = new BroadcastChannel('broadcast')
bc.onmessage = function(e) {
  console.log(e)
}
/** 消息发送后,所有连接到该频道的 BrocastChannel对象上都会触发 meaasge事件,
该事件没有默认行为,可以使用`onmessage`事件处理程序来定义一个函数处理消息
**/

localStorage:

localStorage是html5引入的客户端存储方案,通过localStorage存储的内容会一直保存在客户端,除非调用removeItem方法显式移除,否则内容将永久保留。MDN上对localStorage的介绍也提到了一种通过cookie在不支持localStorage的浏览器上实现localStorage的方法,通过将cookie的过期时间设置为未来很长之后的一个时间点可以模拟localStorage永久保留的特性,而在模拟localStorage移除存储内容时则将对应的cookie删除。更进一步,如果不设置cookie的过期时间,还可以用来模拟浏览器中的另一种客户端存储方案–sessionStorage。和cookie不同的是,localStorage提供的存储容量上限更大。

// a页面
window.addEventListener('storage', function(e) {
  console.log(e)
})

// b页面
localStorage.setItem('data', 10000)

在浏览器的多个标签页中分别打开多个同源页面,当其中某个页面在localStorage中添加、修改或删除某些字段或清空存储的内容时,会触发其他页面中的window对象监听的storage事件(当前修改localStorage的页面不触发),通过这种方式也可以实现跨页面通信。

Service Worker:

Service Worker 是一个可以长期运行在后台的Worker,能够实现与页面的双向通信。多页面共享间的 Service Worker 可以共享,将 Service Worker 作为消息的处理中心(中央站)即可实现广播效果。

  1. 首先在页面注册 Service Worker:
/* 页面逻辑 */
navigator.serviceWorker.register('./sw.js').then(function () {
    console.log('Service Worker 注册成功');
});
  1. 其中./sw.js是对应的 Service Worker 脚本。Service Worker 本身并不自动具备“广播通信”的功能,需要我们添加些代码,将其改造成消息中转站:
/* ./sw.js Service Worker 逻辑 */
self.addEventListener('message', function (e) {
    console.log('service worker receive message', e.data);
    e.waitUntil(
        self.clients.matchAll().then(function (clients) {
            if (!clients || clients.length === 0) {
                return;
            }
            clients.forEach(function (client) {
                client.postMessage(e.data);
            });
        })
    );
});
  1. 在 Service Worker 中监听了message事件,获取页面(从 Service Worker 的角度叫 client)发送的信息。然后通过self.clients.matchAll()获取当前注册了该 Service Worker 的所有页面,通过调用每个client(即页面)的postMessage方法,向页面发送消息。这样就把从一处(某个Tab页面)收到的消息通知给了其他页面。

  2. 在页面监听 Service Worker 发送来的消息:

/* 页面逻辑 */
navigator.serviceWorker.addEventListener('message', function (e) {
    const data = e.data;
    const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
    console.log('[Service Worker] receive message:', text);
});
  1. 需要同步消息时,可以调用 Service Worker 的postMessage方法:
/* 页面逻辑 */
navigator.serviceWorker.controller.postMessage(data);

Shared Worker:

Shared Worker可以开启后台线程运行脚本,并且多个页面之间可以共享,通过port.postMessage发送消息,通过监听message事件实现。如果a页面向后台线程发送消息,后台线程触发message事件后再发送的消息只能被a页面接收,即Shared Worker可以一个线程处理不同的页面逻辑,然后将结果返回对应的页面。

Shared Worker 中支持 get 与 post 形式的消息:

/* ./shared.js: Shared Worker 代码 */
let data = null;
self.addEventListener('connect', function (e) {
    const port = e.ports[0];
    port.addEventListener('message', function (event) {
        // get 指令则返回存储的消息数据
        if (event.data.get) {
            data && port.postMessage(data);
        }
        // 非 get 指令则存储该消息数据
        else {
            data = event.data;
        }
    });
    port.start();
});
  • 页面a的代码:
const sharedWorker = new SharedWorker('./shared.js', 'cc')
sharedWorker.port.postMessage({
 	name: 'natsu'
})
 // 发送数据,此时共享数据的值已经被更改
  • 页面b的代码:
const sharedWorker = new SharedWorker('./shared.js', 'cc')
sharedWorker.port.postMessage({ get: true })

// 监听 get 消息的返回数据
sharedWorker.port.addEventListener('message', (e) => {}, false);
sharedWorker.port.start();

// 如果需要实时更新的话,可能需要`setInterval`进行轮询
setInterval(function () {
    sharedWorker.port.postMessage({get: true});
}, 1000)

IndexedDB:

通俗地说,IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。

与 Shared Worker 方案类似,消息发送方将消息存至 IndexedDB 中;接收方(例如所有页面)则通过轮询去获取最新的信息。

简单来说,就是同源页面可以访问到相同的indexedDB本地数据库,通过轮询方式去查询数据库的数据进行数据更新。这里不做过多的篇幅介绍,有兴趣的可以去了解下indexedDB:

浏览器数据库 IndexedDB 入门教程 - 阮一峰的网络日志


Cookie:

Cookie 是一些数据, 存储于你电脑上的文本文件中。web服务器向浏览器发送web页面时,在连接关闭后,服务端不会记录用户的信息。Cookie 的作用就是用于解决 “如何记录客户端的用户信息”。

Cookie实现页面通信的方式跟indexedDB如出一辙,凭借同源页面可以访问到cookie从而进行通信。不如Cookie的大小有限(4MB),而且大量存储数据在其中会影响到请求头的大小,从未影响接口请求速度。


window.open + window.opener:

使用window.open打开页面时,方法会返回一个被打开页面window的引用。而在未显示指定noopener时,被打开的页面可以通过window.opener获取到打开它的页面的引用 —— 通过这种方式我们就将这些页面建立起了联系(一种树形结构)。

  • 把window.open打开的页面的window对象收集起来:
let childWins = [];
document.getElementById('btn').addEventListener('click', function () {
    const win = window.open('./some/sample');
    childWins.push(win);
});
  • 需要发送消息的时候,作为消息的发起方,一个页面需要同时通知它打开的页面与打开它的页面:
/ 过滤掉已经关闭的窗口
childWins = childWins.filter(w => !w.closed);
if (childWins.length > 0) {
    mydata.fromOpenner = false;
    childWins.forEach(w => w.postMessage(mydata));
}
if (window.opener && !window.opener.closed) {
    mydata.fromOpenner = true;
    window.opener.postMessage(mydata);
}
  • 收到消息的页面就不能那么自私了,除了展示收到的消息,它还需要将消息再传递给它所“知道的人”(打开与被它打开的页面):
window.addEventListener('message', function (e) {
    const data = e.data;
    // 避免消息回传
    if (window.opener && !window.opener.closed && data.fromOpenner) {
        window.opener.postMessage(data);
    }
    // 过滤掉已经关闭的窗口
    childWins = childWins.filter(w => !w.closed);
    // 避免消息回传
    if (childWins && !data.fromOpenner) {
        childWins.forEach(w => w.postMessage(data));
    }
});

简单来说,就是一层一层地传递信息(口口相传)。


url:

通过url的参数进行信息的传递也是可以的,不过使用场景比较局限。

非同源页面之间的通信:

非同源页面之间的通信就需要一个桥梁进行链接页面之间的通信,而这个桥梁就是“iframe”,这是因为 iframe 与父页面间可以通过指定origin来忽略同源限制。

在这里插入图片描述

document.domain:

通过这种方式跨域的两个源需要满足一定的条件的,即两个源的域名需要是父子域的关系或者是相同的域。因为页面设置document.domain的值只能是当前域本身,或者是父域,而不能是其他不相关的域名。只有两个页面的document.domain都设置成相同的值,嵌入iframe的页面和iframe加载的页面才能相互获取到彼此的页面信息(包括DOM结构、window对象等)。

注意:

  • 如果两个页面所在的源是一样的,可以访问到嵌入的window对象上的全局变量,但是如果两个页面所在的域名相同但端口不同或者是其他情况,则无法访问。
  • 需要在嵌入的同源iframe加载完成之后获取子页面window对象上的数据,否则拿到的值还是undefined。

window.name:

浏览器具有这样一个特性:同一个标签页或者同一个iframe框架加载过的页面共享相同的window.name属性值,意味着只要是在同一个标签页里面打开过的页面(不管是否同源),这些页面上window.name属性值都是相同的。利用这个特性,就可以将这个属性作为在不同页面之间传递数据的介质。

如果是通过iframe+window.name这种方式在完全没有父子域关系的两个源之间传递数据(假设源A要获取源B中的数据),源A页面上的iframe在加载源B的目标页面(源B页面把数据设置在window.name属性上)之后还需要再跳转到源A的某个页面上,以便于嵌入iframe的页面通过(上面介绍的)和在iframe中的页面将document.domain都设置为源A的方式来获取iframe中的数据。

示例代码如下:

// www.a.com/getData.html
<script type="text/javascript">
function getData() {
    const frame = document.getElementsByTagName("iframe")[0];
    frame.onload = function () {
        const data = frame.contentWindow.name;
        // http://www.b.com/data.html中通过window.name设置了共享的数据;此处获取数据
        alert(data);
    };
    frame.contentWindow.location = "./aaa.html";
    // 加载完www.b.com/data.html之后就加载www.a.com/下随便一个页面,获取数据
}
</script>
<iframe src="http://www.b.com/data.html" style="display: none;" onload="getData();"></iframe>

HTML5 cross-document message(postMessage):

HTML5中引入了另外一种跨页面通信的方式,称为跨文档消息传送。同样可以实现主页面和嵌入的iframe子页面(或者由当前页面打开的页面)之间完成数据的传递,另外这种方法也可以用于当前JavaScript引擎线程和其他worker线程之间完成数据交换。如果是与通过iframe加载的子页面进行通信,则需要先获取到接收数据的目标页面的window对象(具体通过前面提到的设置相同的document.domain方法来获取),通过该对象的postMessage方法可以向目标页面发送数据。

<!--a.html-->
<iframe src="./b.html" id="iframe"></iframe>
<button id="send-btn">send message</button>
<script>
  const frame = document.getElementById('iframe')
    document.getElementById('send-btn').addEventListener('click', function() {
        frame.contentWindow.postMessage({
            name: 'natsu'
        }, 'http://localhost:8080') // 接收信息的页面所在的源
    })
</script>

<!--b.html-->
<script>
    window.addEventListener('message', function(e) {
        // 验证消息发送方所在的源
        if(e.origin === 'http://localhost:8080') {
            console.log(e.data)
            e.source.postMessage(...) // 回送消息
        }
    })
</script>

如果是需要和页面上的worker进行通信,直接调用创建出来的Worker实例的postMessage方法,在Worker实例执行的脚本中则通过self或者this来访问Worker实例,进而调用postMessage方法来完成通信。需要注意的是,构造Worker实例时传入的脚本必须和当前页面是同源。

总结:

同源页面通信:

  • 广播模式:Broadcast Channe / Service Worker / LocalStorage + StorageEvent
  • 共享存储模式:Shared Worker / IndexedDB / cookie
  • 口口相传模式:window.open + window.opener
  • 基于服务端:Websocket / Comet / SSE 等

非同源页面通信:

  • 通过嵌入同源 iframe 作为“桥”,将非同源页面通信转换为同源页面通信。

以上是关于浏览器-跨页面通信的主要内容,如果未能解决你的问题,请参考以下文章

使用window.postMessage()方法跨域通信

使用window.postMessage()方法跨域通信

使用window.postMessage()方法跨域通信

使用window.postMessage()方法跨域通信

vue 跨页面通信怎么弄

前端跨页面通信方案分析