web跨页面通信的几种方式

最近在处理一个weex三端的项目,在weex文档中提到,不同的 Weex 页面使用的是不同的执行环境,即使全局变量也是互相隔离的,官方推荐使用BroadcastChannel 来实现实现跨页面通信。由于移动端safri中BroadcastChannel存在兼容问题,因此决定研究下web跨页面通信的其他方法。

<!--more-->

参考

1. 使用BroadcastChannel

每个页面通过创建一个具有相同频道名称的 BroadcastChannel 对象来加入特定频道。 然后实现 onmessage 接口来监听消息事件。通过调用 BroadcastChannel 对象上的 postMessage() 方法可以在频道中广播一条消息给所有订阅者。

<!--1.html-->
<script>
    var bc = new fg("test_channel");

    bc.onmessage = function(ev) {
        console.log(ev);
        bc.postMessage({
            msg: `receive message : ${ev.data.msg}`
        });
    };
</script>

另外的页面

<!--2.html-->
<button id="bcBtn">bcBtn click</button>

<script>
    var bc = new BroadcastChannel("test_channel");

    bcBtn.onclick = function() {
        bc.postMessage({
            msg: "helloWorld"
        });
    };
    bc.onmessage = function(ev) {
        console.log(ev.data.msg);
    };
</script>

需要注意的是

  • 这种方式存在兼容问题,移动端safri不支持BroadcastChannel

参考:https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API

2. storage事件

当存储域发生改变时会触发事件。(例如: 有新的项被存储),因此可以通过调用localStorage.setItem等方式触发storage事件,然后通知其他监听了改事件的页面

由于onstorage事件是浏览器触发的,所以如果我们打开了多个相同域名下的页面,并在其中任一一个页面执行window.localStorage.setItem方法(还要保证满足文章开头提到的第二个条件),那么其他页面如果监听了onstorage事件,则这些页面中的onstorage事件回调都会被执行

// 1.html
 window.addEventListener("storage", function(e){
    let msg = e.key +
        " 键已经从 " +
        e.oldValue +
        " 改变为 " +
        e.newValue +
        "."
    console.log(msg)
    outputScreen.innerHTML = msg
})
// 2.html
localStorage.setItem("test", Math.floor(Math.random()*10000))

需要注意

  • 只有当存储的值改变时才会触发storage事件,即新值与旧值不同
  • 触发写入操作的页面下的storage listener不会被触发
  • 即使页面不再同一个浏览器窗口(比如打开两个Chrome浏览器实例),storage也能够触发
  • safari隐身模式下无法设置localStorage值

参考

3. shareWorker

SharedWorker可以被多个window共同使用,但必须保证这些标签页都是同源的

// 1.html
var sharedworker = new SharedWorker('worker.js')
sharedworker.port.start()

workerBtn.onclick = function(){
    // 发送消息
    sharedworker.port.postMessage('hello')
}
// 2.html
var sharedworker = new SharedWorker('worker.js')
sharedworker.port.start()
// 接收消息
sharedworker.port.onmessage = evt => {
    // evt.data
    console.log(evt)
}

然后还需要一个worker文件

// worker.js
const ports = [];
onconnect = e => {
    const port = e.ports[0];
    ports.push(port);
    port.onmessage = evt => {
        ports
            .filter(v => v !== port) // 此处为了贴近其他方案的实现,剔除自己
            .forEach(p => p.postMessage(evt.data));
    };
};

参考

4. 获取对应窗口引用

4.1. postMessage

只要获取了对应窗口的window对象,如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames,就可以调用

otherWindow.postMessage(message, targetOrigin, [transfer]);

向该窗口发送消息,在该页面上,只要监听了message事件即可

<!--1.html-->
<button id="postBtn">postBtn click</button>

<iframe src="http://phptest2.com/test.html" id="otherPage" frameborder="0"></iframe>

<script>

    postBtn.onclick = function() {
        document.getElementById("otherPage").contentWindow.postMessage("hello","http://phptest2.com"); 
    }

    window.addEventListener("message", function receiveMessage(event) {
        console.log(event.data);
    }, false);

</script>
// test.html
function receiveMessage(event) {
    outputScreen.innerHTML = event.data;

    event.source.postMessage(
        "hi there yourself!  the secret response " +
            "is: rheeeeet!",
        event.origin
    );
}

window.addEventListener("message", receiveMessage, false);

postMessage的兼容性较好,可以跨域传递消息,但需要获取到目标窗口引用才行,因此使用有局限性。

参考

4.2. window.opener

window.opener返回的是打开当前窗口的那个窗口的引用,在某些时候如果需要与前一个窗口进行单项通信,则可以使用该属性实现,考虑下面打开新窗口的场景

  • 使用window.open打开了一个新的窗口
  • 使用a链接通过target="_blank"打开了一个新的窗口

在新打开的窗口中,可以通过window.opener获取前一个窗口的引用,然后就可以修改前一个窗口页面上的内容了

// 如果不是从任何窗口打开,则opener为null
if(window.opener){
    window.opener.document.getElementById("contentScreen").innerHTML = 'change from open window'
}

需要注意的是

  • 该方法受同源策略限制,即无法通过window.opener修改非同源的窗口文档内容

参考

5. 共享数据

多个页面之间,可以通过共享数据,然后检测数据的变化来判断是否需要执行相关逻辑

5.1. 本地存储数据,轮询

比如通过写入localStorage、cookie等共享数据存储空间,

由于cookie的改变没有事件通知,所以只能采取轮询脏检查来实现业务逻辑。localStorage修改存储内容有storage事件,可以直接处理(参考上面的:使用stroage事件)

然后检测数据的值是否发生变化,如果发生变化,则执行相关回调

  • 污染数据存储空间
  • 修改cookie会增加额外的网络请求成本

5.2. 服务端储存

前端定期保存修改的数据到服务器,然后通过onvisibilitychangewindow.onpageshow等事件回调时重新获取数据,更新页面数据内容

window.onvisibilitychange = () => {
    if (document.visibilityState === 'visible') {
        // AJAX更新数据
        reload()
    }
}

window.addEventListener("pageshow", reload)

6. 小结

目前的主流web应用均开始采用单页面应用,多个路由组件之间的通信可以通过vuex、redux等状态管理工具进行处理。web跨页面通信的使用场景并不是十分频繁,尤其是在移动端中,同时打开多个标签页,且需要这些标签页进行通信的场景是比较少的,在工作中主要遇见的情形有

  • 某个活动页面要求用户签到后才可以进行下一步操作,点击确认前往签到页面,从签到页面返回后当前页面需要更新用户的签到状态
  • 从个人中心前往信息编辑页面,返回后更新个人中心的用户数据状态

这些需求一般都是通过服务端保存数据信息,返回页面时重新调用接口更新页面数据即可。不过,了解跨页面通信还是很有必要的,这个在PC浏览器上的使用场景应该要频繁一些。