面试官:前端跨页面通讯,你晓得哪些要领?

弁言

在浏览器中,我们可以同时翻开多个Tab页,每一个Tab页可以大略理解为一个“自力”的运转环境,即使是全局对象也不会在多个Tab间同享。但是有些时刻,我们愿望能在这些“自力”的Tab页面之间同步页面的数据、信息或状况。

正以下面这个例子:我在列表页点击“珍藏”后,对应的详情页按钮会自动更新为“已珍藏”状况;相似的,在详情页点击“珍藏”后,列表页中按钮也会更新。

《面试官:前端跨页面通讯,你晓得哪些要领?》

这就是我们所说的前端跨页面通讯。

你晓得哪些跨页面通讯的体式格局呢?假如不清楚,下面我就带人人来看看七种跨页面通讯的体式格局。

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

以下各种体式格局的
在线 Demo 可以戳这里 >>

浏览器的同源战略在下述的一些跨页面通讯要领中依旧存在限定。因而,我们先来看看,在满足同源战略的情况下,都有哪些手艺可以用来完成跨页面通讯。

1. BroadCast Channel

BroadCast Channel 可以帮我们竖立一个用于播送的通讯频道。当一切页面都监听统一频道的音讯时,个中某一个页面经由过程它发送的音讯就会被其他一切页面收到。它的API和用法都异常简朴。

下面的体式格局就可以竖立一个标识为AlienZHOU的频道:

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

Broadcast Channel 的详细的运用体式格局可以看这篇
《【3分钟速览】前端播送式通讯:Broadcast Channel》

2. Service Worker

Service Worker 是一个可以历久运转在背景的 Worker,可以完成与页面的双向通讯。多页面同享间的 Service Worker 可以同享,将 Service Worker 作为音讯的处置惩罚中间(中心站)即可完成播送结果。

Service Worker 也是 PWA 中的核心手艺之一,由于本文重点不在 PWA ,因而假如想进一步相识 Service Worker,可以浏览我之前的文章
【PWA进修与实践】(3) 让你的WebApp离线可用

起首,须要在页面注册 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);

3. LocalStorage

LocalStorage 作为前端最经常使用的当地存储,人人应当已异常熟习了;但StorageEvent这个与它相干的事宜有些同砚可能会比较生疏。

当 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。

下面我们会看到别的两种跨页面通讯体式格局,我把它称为“同享存储+轮询情势”。

4. Shared Worker

Shared Worker 是 Worker 家属的另一个成员。一般的 Worker 之间是自力运转、数据互不相通;而多个 Tab 注册的 Shared Worker 则可以完成数据同享。

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绑定监听则不须要。

5. IndexedDB

除了可以运用 Shared Worker 来同享存储数据,还可以运用其他一些“全局性”(支撑跨页面)的存储计划。比方 IndexedDB 或 cookie。

鉴于人人对 cookie 已很熟习,加上作为“互联网最初期的存储计划之一”,cookie 已在现实运用中承受了远多于其设想之初的义务,我们下面会运用 IndexedDB 来完成。

其思绪很简朴:与 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);
});

小憩一下

在“播送情势”外,我们又相识了“同享存储+长轮询”这类情势。或许你会以为长轮询没有监听情势文雅,但现实上,有些时刻运用“同享存储”的情势时,不一定要搭配长轮询。

比方,在多 Tab 场景下,我们可能会脱离 Tab A 到另一个 Tab B 中操纵;过了一会我们从 Tab B 切换回 Tab A 时,愿望将之前在 Tab B 中的操纵的信息同步返来。这时刻,实在只用在 Tab A 中监听visibilitychange如许的事宜,来做一次信息同步即可。

下面,我会再引见一种通讯体式格局,我把它称为“口口相传”情势。

6. 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);
}

注重,我这里先用.closed属性过滤掉已被封闭的 Tab 窗口。如许,作为音讯发送方的使命就完成了。下面看看,作为音讯接收方,它须要做什么。

此时,一个收到音讯的页面就不能那末自私了,除了展现收到的音讯,它还须要将音讯再通报给它所“晓得的人”(翻开与被它翻开的页面):

须要注重的是,我这里经由过程推断音讯来源,防备将音讯回传给发送方,防备音讯在二者间死循环的通报。(该计划会有些其他小题目,现实中可以进一步优化)

window.addEventListener('message', function (e) {
    const data = e.data;
    const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
    console.log('[Cross-document Messaging] receive message:', text);
    // 防备音讯回传
    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));
    }
});

如许,每一个节点(页面)都负担起了通报音讯的义务,也就是我说的“口口相传”,而音讯就在这个树状构造中流转了起来。

小憩一下

明显,“口口相传”的情势存在一个题目:假如页面不是经由过程在另一个页面内的window.open翻开的(比方直接在地址栏输入,或从其他网站链接过来),这个联络就被打破了。

除了上面这六个罕见要领,实在另有一种(第七种)做法是经由过程 WebSocket 这类的“服务器推”手艺来举行同步。这比如将我们的“中心站”从前端移到了后端。

关于 WebSocket 与其他“服务器推”手艺,不相识的同砚可以浏览这篇《各种“服务器推”手艺道理与实例(Polling/COMET/SSE/WebSocket)》

另外,我还针对以上各种体式格局写了一个 在线演示的 Demo >>

《面试官:前端跨页面通讯,你晓得哪些要领?》

二、非同源页面之间的通讯

上面我们引见了七种前端跨页面通讯的要领,但它们多数遭到同源战略的限定。但是有时刻,我们有两个差别域名的产品线,也愿望它们下面的一切页面之间能无障碍地通讯。那该怎么办呢?

要完成该功用,可以运用一个用户不可见的 iframe 作为“桥”。由于 iframe 与父页面间可以经由过程指定origin来疏忽同源限定,因而可以在每一个页面中嵌入一个 iframe (比方:http://sample.com/bridge.html),而这些 iframe 由于运用的是一个 url,因而属于同源页面,其通讯体式格局可以复用上面第一部份提到的各种体式格局。

页面与 iframe 通讯异常简朴,起首须要在页面中监听 iframe 发来的音讯,做响应的营业处置惩罚:

/* 营业页面代码 */
window.addEventListener('message', function (e) {
    // …… do something
});

然后,当页面要与其他的同源或非同源页面通讯时,会先给 iframe 发送音讯:

/* 营业页面代码 */
window.frames[0].window.postMessage(mydata, '*');

个中为了轻便此处将postMessage的第二个参数设为了'*',你也可以设为 iframe 的 URL。iframe 收到音讯后,会运用某种跨页面音讯通讯手艺在一切 iframe 间同步音讯,例以下面运用的 Broadcast Channel:

/* iframe 内代码 */
const bc = new BroadcastChannel('AlienZHOU');
// 收到来自页面的音讯后,在 iframe 间举行播送
window.addEventListener('message', function (e) {
    bc.postMessage(e.data);
});    

其他 iframe 收到关照后,则会将该音讯同步给所属的页面:

/* iframe 内代码 */
// 关于收到的(iframe)播送音讯,关照给所属的营业页面
bc.onmessage = function (e) {
    window.parent.postMessage(e.data, '*');
};

下图就是运用 iframe 作为“桥”的非同源页面间通讯情势图。

《面试官:前端跨页面通讯,你晓得哪些要领?》

个中“同源跨域通讯计划”可以运用文章第一部份提到的某种手艺。

总结

今天和人人分享了一下跨页面通讯的各种体式格局。

关于同源页面,罕见的体式格局包含:

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

而关于非同源页面,则可以经由过程嵌入同源 iframe 作为“桥”,将非同源页面通讯转换为同源页面通讯。

本文在分享的同时,也是为了抛转引玉。假如你有什么其他主意,迎接一同议论,提出你的看法和主意~

对文章感兴趣的同砚迎接关注
我的博客 >> https://github.com/alienzhou/blog

    原文作者:alienzhou
    原文地址: https://segmentfault.com/a/1190000018731597
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞