前端跨页面通信

什么是前端跨页面通信

在浏览器中,我们同时打开多个tab页,每个tab页可以粗略的理解为一个独立的运行环境,即使是全局对象也不会在多个tab间共享。然后我们希望这些独立的tab页之间同步数据、信息或状态。

比如:我们同时打开了列表页和a的详情页,在列表页对a的状态进行操作后,我们切到详情的tab页,在不进行刷新页面时,要怎么把在列表页操作后的状态同步过来?

Broadcast channel

BroadcastChannel 接口代理了一个命名频道,可以让指定 origin 下的任意 browsing context 来订阅它。它允许同源的不同浏览器窗口,Tab页,frame或者 iframe 下的不同文档之间相互通信。通过触发一个 message 事件,消息可以广播到所有监听了该频道的 BroadcastChannel 对象。

  • 可以通过 BroadcastChannel() 创建一个链接到命名频道的对象
  • 属性: name
  • Event: message\ messageerror
  • 方法: close()/postMessage()
    // 创建一个 test1 的频道
    const bc = new BroadcastChannel('test1')
    console.log(bc.name) // test1

    // 各个页面可以通过 onmessage 来监听被广播的消息
    bc.onmessage = function(e) {
        const data = e.data
    }

    // 发送消息时调用 postMessage 方法
    bc.postMessage({from: 'other', msg});

    // 关闭频道对象,告诉它不要再接收新的消息,并允许它最终被垃圾回收
    bc.close()

注:同一个tab也发送的消息,该tab页监听不到

localStorage

当前页面使用的 storage 被其他页面修改时会触发 StorageEvent 事件。利用这个特性,我们可以在发送消息时,把消息写到 localStorage/sessionStorage 中,然后在各个页面内,通过监听 storage 事件即可收到通知。

属性:

  • key:该属性代表被修改的键值。当被clear()方法清除之后该属性值为null。
  • newValue:该属性代表修改后的新值。当被clear()方法清理后或者该键值对被移除,newValue 的值为 null
  • oldValue:该属性代表修改前的原值。在设置新键值对时由于没有原始值,该属性值为 null
  • storageArea:被操作的storage对象
  • url:key发生改变的对象所在文档的URL地址
// 在各页面去通过监听 storage 事件去接收消息
window.addEventListener('storage', (e) => {
    if (e.key === 'localStorage-msg') {
        let data = JSON.parse(e.newValue)
        let msgEle = document.createElement('p')
        msgEle.innerHTML = data.date + ' ' + data.from + ': ' + data.msg
        receiveEle.appendChild(msgEle)
    }
})

// localStorage
window.localStorage.setItem('localStorage-msg', msg)
// sessionStorage
// window.sessionStorage.setItem('sessionStorage-msg', msg)

localStorage和sessionStorage区别:

  • 存储在localStorage的数据可以长期保存;sessionStorage的数据在关闭当前页面时就会被清除。localStorage 可能因清除不及时引发问题
  • 打开多个相同的URL tabs页时,会创建各自的sessionStorage,而localstorage只要在相同的协议、主机名和端口号,就能读取/修改同一份localStorage数据。sessionStorage只适用于iframe 的数据同步。

Service Worker

Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器,可以长期运行在后台,能够实现与页面的双向通信。多页面共享间的 Service Worker 可以共享,将 Service Worker 作为消息的处理中心(中央站)即可实现广播效果。

Service Worker 并不自动具备“广播通信”的功能,需要改造 Service Worker 的运行脚本,将其改造成消息中转站。在 Service Worker 中监听message事件,获取页面发送的信息。然后通过 self.clients.matchAll() 获取当前注册了 Service Worker 的所有页面,通过调用每个client的 postMessage 方法,向页面发送消息。这样就把从一处(某个Tab页面)收到的消息通知给了其他页面。

简单来说:就是通过worker和主线程之间能双向通信的特性,在worker脚本中接收到来自主线程的消息时,就将接收的消息发送给所有注册了该线程的页面

// page1.html
// 在页面注册 service worker
    navigator.serviceWorker.register('./serviceWorker.js').then(() => {
        console.log('Service Worker 注册成功');
    })

    // 在页面中监听 service Worker 发来的消息
    navigator.serviceWorker.addEventListener('message', (e) => {
        const data = e.data;
        let msgEle = document.createElement('p')
        msgEle.innerHTML = data.date + ' ' + data.from + ': ' + data.msg
        receiveEle.appendChild(msgEle)
    })

    //  发送消息
    btnSend.addEventListener('click', () => {
        let msg = {
            from: 'page 1',
            msg: sendEle.value,
            date: new Date().toLocaleString()
        }
        // 调用service worker 的 postMessage方法发送消息
        navigator.serviceWorker.controller.postMessage(msg)
    })
// serviceworker.js
self.addEventListener('message', (e) => {
    console.log('serviceworker message', e)
    e.waitUntil(
        // self.clients.matchAll() 获取当前注册了该 service Worker 的所有页面
        self.clients.matchAll().then((clients) => {
            if(!clients || clients.length === 0) {
                return
            }
            //  clients 中记录了每一个tab页/iframe都记录在里面
            clients.forEach((client) => {
                console.log(7777, client)
                client.postMessage(e.data)
            })
        })
    )
})

SharedWorker

SharedWorker 接口代表一种特定类型的 worker,可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker。它们实现一个不同于普通 worker 的接口,具有不同的全局作用域,如果要使 SharedWorker 连接到多个不同的页面,这些页面必须是同源的。同一个 js url 只会创建一个SharedWorker,其他页面内使用同样的url创建sharedWorker,会复用已创建的worker。

  • 通过SharedWorker()构造函数创建一个执行指定url脚本的共享 web worker
  • 创建完SharedWorker() 会返回一个MessagePort 对象来访问worker,这个对象用SharedWorker.port 属性获得
  • 如果已经用 addEventListener 监听了 onmessage 事件,则可以使用 start() 方法手动启动端口
  • 在执行脚本中使用 onconnect 处理程序连接到相同端口

实现思路:简单来说,我们创建的这个共享线程是可以跟主线程进行双向通信的,利用这个特性,我们在worker 脚本里去收集所有共享了这个worker线程的端口,每当收到来自主线程的消息时,就就将收到的消息发送给所有共享了这个脚本的端口

 // page.html 页面
    if (!window.SharedWorker) {
        alert("浏览器不支持SharedWorkder!")
    } else {
        // 创建一个共享进场对象
        let myWorker = new SharedWorker("./worker.js")

        // 通过监听 port.onmessage 监听worker的返回消息
        myWorker.port.onmessage = function(e) {
            console.log(5555, e)
            let msgEl = document.createElement("p")
            let data = e.data;
            msgEl.innerText = data.date + " " + data.from + ":" + data.message
            messagesEle.appendChild(msgEl)
        };

    btnSend.addEventListener("click", () => {
        let message = messageEl.value
        // 发送消息,将消息发送到 worker
        myWorker.port.postMessage({
        date: new Date().toLocaleString(),
        message,
        from: "page 1"
        })
    });

    // 手动开启端口:启动发送该端口的消息队列;在未调用之前,发往该端口的消息都在队列中等待
    myWorker.port.start()
// worker.js
// 通过这个worke.js 的脚本,实现让worker向多个页面发送消息, 讲port缓存在一个数组里,当需要向所有页面广播消息时,就遍历数组发送消息
let portList = []
onconnect = function(e) {
    let port = e.ports[0]
    // 在connect时将 port添加到 portPool中
    ensurePorts(port)
    port.onmessage = function(e) {
        let data = e.data
        // 将消息发送到每一个注册了该脚本的页面
        broadcast(data)
    }
    port.start()
}

function ensurePorts(port) {
    if (portList.indexOf(port) < 0) {
        portList.push(port);
    }
}

// 一个tab页是一个port,相同的两个tab页,在一个页面发送消息时,另一个页面也可以收到消息
function broadcast(msg) {
    portList
        .filter(port => selfPort !== port)
        .forEach(port => port.postMessage(msg))
}

window.open + window.opener

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

let windowObjectReference = window.open(strUrl, strWindowName, [strWindowFeatures])

  • strUrl:要在新打开的窗口中加载的URL
  • strWindowName:新窗口的名称
  • strWindowFeatures:一个可选参数,列出新窗口的特征(大小,位置,滚动条等)作为一个DOMString
  • windowObjectReference : 打开的新窗口对象的引用,如果父子窗口满足“同源策略”,你可以通过这个引用访问新窗口的属性或方法
<body>
    <h3>windowopen Page 1</h3>
    <input type="button" value="page2" id="open-p2" />
    <input type="button" value="page2-2" id="open-p2-2" />
    <section style="margin-top:50px; text-align: center">
        <input id="inputMessage" value="page 1的测试消息" />
        <input type="button" value="发送消息" id="btnSend" />
        <section id="messages">
            <p>收到的消息:</p>
        </section>
    </section>
    <script>
        let receiveEle = document.getElementById("messages");
        let sendEle = document.getElementById("inputMessage");
        let btnSend = document.getElementById("btnSend");
        let btnOpen = document.getElementById('open-p2')
        let btnOpen2 = document.getElementById('open-p2-2')

        // 把我们用window.open 打开的页面用pageList收集起来
        let pageList = []
        btnOpen.addEventListener('click', () => {
            const win = window.open('./page2.html')
            pageList.push(win)
        })

        btnOpen2.addEventListener('click', () => {
            const win = window.open('./page2-2.html')
            pageList.push(win)
        })

        // 发送消息
        btnSend.addEventListener('click', () => {
            let msg = sendEle.value
            sendParents(msg)
            sendChile(msg)
        })
        // 传消息给用 window.open 打开该页面的页面-- 父页面
        function sendParents(msg) {
            if(window.opener && !window.opener.closed){
                msg.fromOpener = true
                window.opener.postMessage({
                    fromOpener: true,
                    msg
                })
            }
        }
        // 传消息给被该页面用 window.open 打开的页面
        function sendChile(msg) {
            pageList
            .filter(w => !w.closed)
            .forEach(w => {
                console.log('send', w)
                w.postMessage({
                    fromOpener: false,
                    msg
                })
            })
        }

        // 页面监听 message,当接收到消息后,在向其他页面传递改消息
        window.addEventListener('message', (e) => {
            console.log('message', e)
            const data = e.data
            if(data.fromOpener) {
                sendParents(data.msg)
            } else {
                sendChile(data.msg)
            }
            let msgEle = document.createElement('p')
            msgEle.innerHTML = `${data.fromOpener ? '来自子页面' : '来自父页面'}${data.msg}`
            receiveEle.appendChild(msgEle)
        })
    </script>
</body>

注:页面只监听得到父页面和子页面发送的消息,祖先页面和兄弟页面是监听不到的,所以在接收消息时要进行特殊处理。在页面进行强制刷新后,页面间会失去关联,导致无法再进行通信。

postMessage

window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为https),端口号,以及主机时,这两个脚本才能相互通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。

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

  • otherWindow:其他窗口的一个引用,比如 iframe 的 contentWindow 属性、执行 window.open 返回的窗口对象、或者是命名过或数值索引的 window.frames
  • message:将要发送到其他 window的数据
  • targetOrigin:指定哪些窗口能接收到消息事件,其值可以是 *(表示无限制)或者一个 URI
  • transfer:可选,是一串和 message 同时传递的 Transferable 对象。这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

上面window.open 实现同源的跨页面通信就是通过 postMessage 来实现的,下面我们来看一下如何实现非同源多页面之间的通信呢?

注: postMessage 实现跨页面通信的前提条件是两个tab也之间有 “父子”关系,所以单靠 postMessage 是无法实现的。

实现思路:在需要实现非同源跨页面通信的页面都去用 iframe 标签内嵌一个不可见的 iframe.html 页面,在页面与 iframe.html 页面间,我们使用 postMessage 来实现父子页面间的非同源跨页面通信,在 iframe.html 里实现同源的跨页面通信,从而实现非同源的多页面间的跨页面通信。

<!-- // 父页面 -->
<body>
    <h3>跨域 Page 1</h3>
    <section style="margin-top:50px; text-align: center">
      <input id="inputMessage" value="page 1的测试消息" />
      <input type="button" value="发送消息" id="btnSend" />
      <section id="messages">
        <p>收到的消息:</p>
      </section>
    </section>

    <iframe src="http://www.kuayu-iframe.com/iframe.html" id="child" style="opacity: 0;"></iframe>

    <script>
        
        // 创建一个标识为 bcc-msg 的频道
        let receiveEle = document.getElementById("messages");
        let sendEle = document.getElementById("inputMessage");
        let btnSend = document.getElementById("btnSend");

        // 跨域传递消息时
        btnSend.addEventListener('click', () => {
            let msg = sendEle.value
            // 父页面给iframe子页面发送消息
            window.frames[0].postMessage({
                from: 'p1',
                msg
            },  'http://www.kuayu-iframe.com/')
        })
        window.addEventListener('message', (e) => {
            console.log('fromIframe', e)
            const data = e.data.data
            let msgEle = document.createElement('p')
            msgEle.innerHTML = data.from + ': ' + data.msg
            receiveEle.appendChild(msgEle);
        })

    </script>
</body>
<!-- // iframe.html 页面 -->
<body>
    <script>

        // 创建一个标识为 bcc-msg 的频道
        let bc = new BroadcastChannel('kuayu-msg')

        // 监听通过postMessage发送来的消息
        window.addEventListener('message', (e) => {
            const data = e.data
            // 将接收到的消息,通过 Broadcast Channel 广播给其他tab页里的iframe
            bc.postMessage({
                from: 'iframe',
                data
            })
        })
        // 监听个iframe tab 页 传来的消息
        bc.onmessage = function(e) {
            const data = e.data
            // 从iframe页面将消息传递给父页面
            window.parent.postMessage(e.data, '*')
        }

    </script>
</body>

总结:

  • 广播模式:Broadcast Channe / Service Worker / StorageEvent / Shared Worker
  • 口口相传模式:window.open + window.opener/postMessage
  • 基于服务端:websocket / 轮询请求

各API实现跨页面的优缺点:

  • Broadcast Channel:实时通信,随时关闭频道,功能全面;浏览器支持效果不好(IE-都不支持,chrome-54)
  • storageEvent:浏览器支持效果好、API直观。部分浏览器隐身模式下,无法设置localStorage(如safari)。可能因LocalStorage清理不及时而引起问题
  • shareworker:异步处理,不占用主线程,不用 ie 浏览器的话,还是非常推荐的(IE-都不支持)
  • serviceworker:浏览器支持度不高,开启service worker可能会导致浏览器的缓存数据大大增加(IE-都不支持)
  • window.open + window.opener:实现方式不优雅,无法形成统一的解决方案,且在强制刷新页面后会失去关联导致无法通信;实现简单,浏览器兼容性好
  • postMessage:点对点通信,局限性比较大,可扩展性比较差。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值