美文网首页
前端跨tab页通信

前端跨tab页通信

作者: 白小纯kl | 来源:发表于2021-10-12 15:05 被阅读0次

    一、同源页面间的跨页面通信

    1. BroadCast Channel
    const bc = new BroadcastChannel('AlienZHOU');
    // 各个页面可以通过onmessage来监听被广播的消息:
    bc.onmessage = function (e) {
        const data = e.data;
        const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
        console.log('[BroadcastChannel] receive message:', text);
    };
    // 要发送消息时只需要调用实例上的postMessage方法即可:
    bc.postMessage(mydata);
    
    
    1. Service Worker
      Service Worker 是一个可以长期运行在后台的 Worker,能够实现与页面的双向通信。多页面共享间的 Service Worker 可以共享,将 Service Worker 作为消息的处理中心(中央站)即可实现广播效果
    // 首先,需要在页面注册 Service Worker:
    /* 页面逻辑 */
    navigator.serviceWorker.register('../util.sw.js').then(function () {
        console.log('Service Worker 注册成功');
    });
    // 其中../util.sw.js是对应的 Service Worker 脚本。Service Worker 本身并不自动具备“广播通信”的功能,需要我们添加些代码,将其改造成消息中转站:
    
    /* ../util.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);
                });
            })
        );
    });
    

    我们在 Service Worker 中监听了message事件,获取页面(从 Service Worker 的角度叫 client)发送的信息。然后通过self.clients.matchAll()获取当前注册了该 Service Worker 的所有页面,通过调用每个client(即页面)的postMessage方法,向页面发送消息。这样就把从一处(某个Tab页面)收到的消息通知给了其他页面。
    处理完 Service Worker,我们需要在页面监听 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);
    });
    
    

    最后,当需要同步消息时,可以调用 Service Worker 的postMessage方法:

    /* 页面逻辑 */
    navigator.serviceWorker.controller.postMessage(mydata);
    
    
    1. LocalStorage
      当 LocalStorage 变化时,会触发storage事件。利用这个特性,我们可以在发送消息时,把消息写入到某个 LocalStorage 中;然后在各个页面内,通过监听storage事件即可收到通知。
    window.addEventListener('storage', function (e) {
        if (e.key === 'ctc-msg') {
            const data = JSON.parse(e.newValue);
            const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
            console.log('[Storage I] receive message:', text);
        }
    });
    
    

    在各个页面添加如上的代码,即可监听到 LocalStorage 的变化。当某个页面需要发送消息时,只需要使用我们熟悉的setItem方法即可:

    mydata.st = +(new Date);
    window.localStorage.setItem('ctc-msg', JSON.stringify(mydata));
    
    

    注意,这里有一个细节:我们在mydata上添加了一个取当前毫秒时间戳的.st属性。这是因为,storage事件只有在值真正改变时才会触发。举个例子:

    window.localStorage.setItem('test', '123');
    window.localStorage.setItem('test', '123');
    
    

    由于第二次的值'123'与第一次的值相同,所以以上的代码只会在第一次setItem时触发storage事件。因此我们通过设置st来保证每次调用时一定会触发storage事件。

    小结:

    上面我们看到了三种实现跨页面通信的方式,不论是建立广播频道的 Broadcast Channel,还是使用 Service Worker 的消息中转站,抑或是些 tricky 的storage事件,其都是“广播模式”:一个页面将消息通知给一个“中央站”,再由“中央站”通知给各个页面。

    在上面的例子中,这个“中央站”可以是一个 BroadCast Channel 实例、一个 Service Worker 或是 LocalStorage。

    下面我们会看到另外两种跨页面通信方式,我把它称为“共享存储+轮询模式”。

    1. Shared Worker
      多个 Tab 注册的 Shared Worker 则可以实现数据共享
      用户可使用轮询的方式,来拉取最新的数据

    让 Shared Worker 支持两种消息。一种是 post,Shared Worker 收到后会将该数据保存下来;另一种是 get,Shared Worker 收到该消息后会将保存的数据通过postMessage传给注册它的页面。也就是让页面通过 get 来主动获取(同步)最新消息。具体实现如下:
    首先,我们会在页面中启动一个 Shared Worker,启动方式非常简单:

    // 构造函数的第二个参数是 Shared Worker 名称,也可以留空
    const sharedWorker = new SharedWorker('../util.shared.js', 'ctc');
    
    

    然后,在该 Shared Worker 中支持 get 与 post 形式的消息:

    /* ../util.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();
    });
    
    

    之后,页面定时发送 get 指令的消息给 Shared Worker,轮询最新的消息数据,并在页面监听返回信息:

    // 定时轮询,发送 get 指令的消息
    setInterval(function () {
        sharedWorker.port.postMessage({get: true});
    }, 1000);
    
    // 监听 get 消息的返回数据
    sharedWorker.port.addEventListener('message', (e) => {
        const data = e.data;
        const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
        console.log('[Shared Worker] receive message:', text);
    }, false);
    sharedWorker.port.start();
    
    

    最后,当要跨页面通信时,只需给 Shared Worker postMessage即可:

    sharedWorker.port.postMessage(mydata);
    
    

    注意,如果使用addEventListener来添加 Shared Worker 的消息监听,需要显式调用MessagePort.start方法,即上文中的sharedWorker.port.start();如果使用onmessage绑定监听则不需要。

    1. IndexedDB
      除了可以利用 Shared Worker 来共享存储数据,还可以使用其他一些“全局性”(支持跨页面)的存储方案。例如 IndexedDB 或 cookie。

    其思路很简单:与 Shared Worker 方案类似,消息发送方将消息存至 IndexedDB 中;接收方(例如所有页面)则通过轮询去获取最新的信息。在这之前,我们先简单封装几个 IndexedDB 的工具方法。
    打开数据库连接:

    function openStore() {
        const storeName = 'ctc_aleinzhou';
        return new Promise(function (resolve, reject) {
            if (!('indexedDB' in window)) {
                return reject('don\'t support indexedDB');
            }
            const request = indexedDB.open('CTC_DB', 1);
            request.onerror = reject;
            request.onsuccess =  e => resolve(e.target.result);
            request.onupgradeneeded = function (e) {
                const db = e.srcElement.result;
                if (e.oldVersion === 0 && !db.objectStoreNames.contains(storeName)) {
                    const store = db.createObjectStore(storeName, {keyPath: 'tag'});
                    store.createIndex(storeName + 'Index', 'tag', {unique: false});
                }
            }
        });
    }
    
    

    存储数据

    function saveData(db, data) {
        return new Promise(function (resolve, reject) {
            const STORE_NAME = 'ctc_aleinzhou';
            const tx = db.transaction(STORE_NAME, 'readwrite');
            const store = tx.objectStore(STORE_NAME);
            const request = store.put({tag: 'ctc_data', data});
            request.onsuccess = () => resolve(db);
            request.onerror = reject;
        });
    }
    
    

    查询/读取数据

    function query(db) {
        const STORE_NAME = 'ctc_aleinzhou';
        return new Promise(function (resolve, reject) {
            try {
                const tx = db.transaction(STORE_NAME, 'readonly');
                const store = tx.objectStore(STORE_NAME);
                const dbRequest = store.get('ctc_data');
                dbRequest.onsuccess = e => resolve(e.target.result);
                dbRequest.onerror = reject;
            }
            catch (err) {
                reject(err);
            }
        });
    }
    
    

    剩下的工作就非常简单了。首先打开数据连接,并初始化数据:

    openStore().then(db => saveData(db, null))
    
    

    对于消息读取,可以在连接与初始化后轮询:

    openStore().then(db => saveData(db, null)).then(function (db) {
        setInterval(function () {
            query(db).then(function (res) {
                if (!res || !res.data) {
                    return;
                }
                const data = res.data;
                const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
                console.log('[Storage I] receive message:', text);
            });
        }, 1000);
    });
    
    

    最后,要发送消息时,只需向 IndexedDB 存储数据即可:

    openStore().then(db => saveData(db, null)).then(function (db) {
        // …… 省略上面的轮询代码
        // 触发 saveData 的方法可以放在用户操作的事件监听内
        saveData(db, mydata);
    });
    
    
    1. window.open + window.opener
      当我们使用window.open打开页面时,方法会返回一个被打开页面window的引用。而在未显示指定noopener时,被打开的页面可以通过window.opener获取到打开它的页面的引用 —— 通过这种方式我们就将这些页面建立起了联系(一种树形结构)。
      省略实现

    7.WebSocket
    通过 WebSocket 这类的“服务器推”技术来进行同步

    二、非同源页面之间的通信
    上面我们介绍了七种前端跨页面通信的方法,但它们大都受到同源策略的限制。然而有时候,我们有两个不同域名的产品线,也希望它们下面的所有页面之间能无障碍地通信。那该怎么办呢?

    可以使用一个用户不可见的 iframe 作为“桥”。由于 iframe 与父页面间可以通过指定origin来忽略同源限制,因此可以在每个页面中嵌入一个 iframe (例如:http://sample.com/bridge.html),而这些 iframe 由于使用的是一个 url,因此属于同源页面,其通信方式可以复用上面第一部分提到的各种方式。

    总结
    对于同源页面,常见的方式包括:

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

    而对于非同源页面,则可以通过嵌入同源 iframe 作为“桥”,将非同源页面通信转换为同源页面通信。

    相关文章

      网友评论

          本文标题:前端跨tab页通信

          本文链接:https://www.haomeiwen.com/subject/bcxroltx.html