乐趣区

关于前端:Js前端跨页面通信

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

以下各种形式的 在线 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 >>

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 作为“桥”的非同源页面间通信模式图。

image

其中“同源跨域通信计划”能够应用文章第一局部提到的某种技术。


总结

明天和大家分享了一下跨页面通信的各种形式。

对于同源页面,常见的形式包含:

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

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

本文在分享的同时,也是为了抛转引玉。如果你有什么其余想法,欢送一起探讨,提出你的见解和想法~

作者:AlienZHOU
链接:https://www.jianshu.com/p/bda…
起源:简书
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。

退出移动版