The introduction

In a browser, you can have multiple tabs open at the same time, and each Tab can be roughly interpreted as a “separate” runtime environment. Even global objects are not shared across multiple tabs. Sometimes, however, we want to synchronize page data, information, or state between these “separate” Tab pages.

As in the following example: when I click “Favorites” on the list page, the corresponding details page button will automatically update to “Favorites” status; Similarly, after clicking “Favorites” on the details page, the buttons in the list page are updated.

This is what we call front-end cross-page communication.

What do you know about cross-page communication? If that’s not clear, here are seven ways to communicate across pages.


I. Cross-page communication between homologous pages

In the following ways
Online Demo can be accessed here >>

The browser’s same-origin policy still has limitations in some of the following cross-page communication methods. So let’s start by looking at what techniques can be used to enable cross-page communication if the same origin policy is satisfied.

1. BroadCast Channel

A BroadCast Channel can help us create a communication Channel for broadcasting. When all pages listen for messages on the same channel, messages sent by one page are received by all the other pages. Its API and usage are very simple.

To create a channel branded AlienZHOU:

const bc = new BroadcastChannel('AlienZHOU');Copy the code

Individual pages can listen for messages to be broadcast via onMessage:

bc.onmessage = function (e) { const data = e.data; Const text = '[receive] '+ data. MSG +' -- TAB '+ data.from; console.log('[BroadcastChannel] receive message:', text); };Copy the code

To send a message, simply call the postMessage method on the instance:

bc.postMessage(mydata);Copy the code

For details on how to use Broadcast Channel, see this article
Front-end Broadcast Communication: Broadcast Channel.

2. Service Worker

The Service Worker is a Worker that can run in the background for a long time and can realize two-way communication with the page. Service workers between multiple page sharing can be shared, and the broadcast effect can be realized by taking the Service Worker as the message processing center (central station).

Service workers are also one of the core technologies in PWA. Since this article is not focused on PWA, if you want to learn more about Service workers, you can read my previous articles
(3) Make your WebApp offline.

First, you need to register the Service Worker on the page:

/ * * / navigator page logic. ServiceWorker. Register ('.. /util.sw.js'). Then (function () {console.log('Service Worker registered successfully '); });Copy the code

Among them.. /util.sw.js is the corresponding Service Worker script. The Service Worker itself does not automatically have the function of “broadcast communication”, so we need to add some codes to transform it into a message relay station:

/ *.. / uti.sw. js Service Worker logic */ 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); }); })); });Copy the code

We listen for the Message event in the Service Worker and get the message sent by the page (called client from the perspective of the Service Worker). We then call self.clients.matchall () to retrieve all pages that are currently registered with the Service Worker and send a message to the page by calling the postMessage method of each client (page). This notifies the other pages of messages received from one Tab page.

After processing the Service Worker, we need to listen for the message sent by the Service Worker on the page:

/ * * / navigator page logic. ServiceWorker. AddEventListener (' message ', function (e) {const data = e.d ata. Const text = '[receive] '+ data. MSG +' -- TAB '+ data.from; console.log('[Service Worker] receive message:', text); });Copy the code

Finally, when messages need to be synchronized, the Service Worker’s postMessage method can be called:

/ * * / navigator page logic. ServiceWorker. Controller. PostMessage (mydata);Copy the code

3. LocalStorage

LocalStorage as the front-end of the most commonly used LocalStorage, you should already be very familiar with; However, StorageEvent is a related event that some students may be unfamiliar with.

When LocalStorage changes, the storage event is triggered. With this feature, we can write a message to a LocalStorage when sending it. Then in each page, you can receive notifications by listening for storage events.

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); }});Copy the code

Add the above code to each page to listen to changes in LocalStorage. When a page needs to send a message, just use the familiar setItem method:

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

Notice one detail: we’ve added a.st property to myData that takes the current millisecond timestamp. This is because a storage event is emitted only when the value actually changes. Here’s an example:

window.localStorage.setItem('test', '123');
window.localStorage.setItem('test', '123');Copy the code

Since the second value of ‘123’ is the same as the first, the above code will only fire the storage event on the first setItem. Therefore, we set st to ensure that the storage event will be raised every time the call is made.

Rest,

We have seen three different ways of communicating across pages, whether it is setting up a Broadcast Channel, using a Service Worker’s message relay, or tricky storage events, all of which are “Broadcast mode” : One page notifies a “central station” of messages, which in turn notifies individual pages.

In the example above, the “central station” could be a BroadCast Channel instance, a Service Worker, ora LocalStorage.

Next we’ll look at two other ways of communicating across pages, which I call the “shared storage + polling pattern.”


4. Shared Worker

A Shared Worker is another member of the Worker family. Ordinary workers operate independently and their data are not connected to each other. Shared workers registered with multiple tabs can realize data sharing.

The problem of Shared Worker in realizing cross-page communication is that it cannot actively notify all pages, so we use polling to pull the latest data. Here’s the idea:

Let the Shared Worker support two types of messages. One is POST, in which the Shared Worker saves the data after receiving it. The other is GET. After receiving the message, the Shared Worker will send the saved data to the page where it is registered via postMessage. That is, the page uses get to actively get (synchronize) the latest news. The concrete implementation is as follows:

First, we will start a Shared Worker on the page in a very simple way:

Const sharedWorker = new sharedWorker ('.. /util.shared.js', 'ctc');Copy the code

Then, the Shared Worker supports messages in the form of GET and POST:

/ *.. /util.shared.js: shared Worker code */ let data = null; self.addEventListener('connect', function (e) { const port = e.ports[0]; port.addEventListener('message', If (event.data.get) {data && port.postMessage(data); if (event.data.get) {data && port.postMessage(data); } else {data = event.data; }}); port.start(); });Copy the code

After that, the page periodically sends the message of get instruction to the Shared Worker, polls the latest message data, and listens for the returned information on the page:

/ / regularly polling, send a get command message setInterval (function () {sharedWorker. Port. PostMessage ({get: true}); }, 1000); / / monitored get message returned data sharedWorker port. AddEventListener (' message ', (e) = > {const data = e.d ata. Const text = '[receive] '+ data. MSG +' -- TAB '+ data.from; console.log('[Shared Worker] receive message:', text); }, false); sharedWorker.port.start();Copy the code

Finally, when communicating across pages, just give the Shared Worker postMessage:

sharedWorker.port.postMessage(mydata);Copy the code

Note if used
addEventListenerTo add message listening for the Shared Worker, call explicitly
MessagePort.startMethod, namely above
sharedWorker.port.start(); If you are using
onmessageBinding listeners are not required.

5. IndexedDB

In addition to sharing data with Shared workers, there are other “global” (cross-page) storage solutions that can be used. Such as IndexedDB or cookie.

Given the familiarity with cookies and the fact that as “one of the earliest storage solutions on the Internet,” cookies have assumed far more responsibility than they were originally designed for, we will use IndexedDB for this purpose.

The idea is simple: similar to Shared Worker schemes, message senders store messages in IndexedDB; Recipients (for example, all pages) poll to get the latest information. Before we do that, we’ll briefly encapsulate a few tool methods for IndexedDB.

  • Open database connection:
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});
            }
        }
    });
}Copy the code
  • Store the data
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;
    });
}Copy the code
  • Query/read data
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); }}); }Copy the code

The rest of the work is very simple. First open the data connection and initialize the data:

openStore().then(db => saveData(db, null))Copy the code

For message reads, polling can be done after connection and initialization:

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);
});Copy the code

Finally, to send a message, simply store data to IndexedDB:

OpenStore ().then(db => saveData(DB, null)).then(function (db) {//... // The method that triggers saveData can be placed in the event listener of the user operation saveData(db, mydata); });Copy the code

Rest,

In addition to “broadcast mode”, we also know the mode of “shared storage + long polling”. You might think long polling is less elegant than listening, but in fact, there are times when “shared storage” is used without long polling.

For example, in A multi-tab scenario, we might move from Tab A to Tab B. After A while, when we switch from Tab B back to Tab A, we want to synchronize the information from the previous actions in Tab B. At this point, in fact, only in Tab A listening visibilitychange event, to do A synchronization of information.

Next, I will introduce another method of communication, which I call “word of mouth” mode.


6. window.open + window.opener

When we open the page with window.open, the method returns a reference to the opened page Window. However, when the specified noopener is not displayed, the opened page can obtain the reference of the opened page through window.opener — in this way, we establish the connection between these pages (a tree structure).

First, we collect the window object from the page that window.open opens:

let childWins = [];
document.getElementById('btn').addEventListener('click', function () {
    const win = window.open('./some/sample');
    childWins.push(win);
});Copy the code

Then, when we need to send a message, as the initiator of the message, a page needs to inform it of both the open page and the open page:

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); }Copy the code

Note that I first use the.closed attribute to filter out Tab Windows that are already closed. This completes the task of being the sender of the message. Let’s look at what it needs to do as a message receiver.

At this point, a page that receives a message can’t be so selfish. In addition to displaying the received message, it also needs to relay the message to “people it knows” (open and opened pages) :

It is important to note that I avoid sending messages back to the sender by determining the source of the message, preventing the message from passing in an infinite loop between the two. (There are other minor problems with this scheme, which can be further optimized in practice)

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)); }});Copy the code

In this way, each node (page) is responsible for delivering the message, which I call “word of mouth,” and the message flows through the tree.

Rest,

Obviously, there is a problem with the “word-of-mouth” model: if the page is not opened by window.open in another page (for example, by typing directly into the address bar, or by linking from another site), the link is broken.

In addition to these six common methods, there is another (seventh) way to synchronize through “server push” techniques such as WebSocket. This is like moving our “central station” from the front end to the back end.

About WebSocket and other “server push technology, don’t know the students can read the article” the “server push” technology principle and instance (Polling/COMET/SSE/WebSocket)”

In addition, I also wrote an online Demo for each of the above methods >>

2. Communication between non-homologous pages

We have described seven approaches to front-end cross-page communication, but most of them are limited by the same origin policy. However, sometimes we have a product line with two different domains and want all the pages under them to communicate seamlessly. So what to do?

To do this, use an IFrame that is not visible to the user as the “bridge.” Since the origin restriction can be ignored between an iframe and the parent page by specifying origin, it is possible to embed an IFrame in each page (for example: http://sample.com/bridge.html), but the iframe by using a url, therefore belong to the same page, its communication mode can reuse the first part of the above mentioned ways.

The communication between the page and iframe is very simple. First, you need to listen to the message sent by iframe in the page and do the corresponding service processing:

// window. AddEventListener ('message', function (e) {//... do something });Copy the code

Then, when a page wants to communicate with other homologous or non-homologous pages, it first sends a message to the IFrame:

/* Window.frames [0].window.postMessage(mydata, '*');Copy the code

The second argument to postMessage is set to ‘*’ for simplicity. You can also set it to the URL of iframe. When an IFrame receives a message, it synchronizes the message across all ifRames using some cross-page message communication technique, such as the following Broadcast Channel:

/* iframe */ const BC = new BroadcastChannel('AlienZHOU'); Window. addEventListener('message', function (e) {bc.postmessage (e.data); });Copy the code

When other IFrames receive the notification, they synchronize the message to the page they belong to:

* / / / / * iframe code to receive (iframe) radio news, notice to BC's business page. The onmessage = function (e) {window. The parent. PostMessage (e.d ata, '*'); };Copy the code

The following diagram shows a communication pattern between non-homologous pages using iframe as a bridge.

The homologous cross-domain communication scheme can use one of the techniques mentioned in the first part of the article.


conclusion

Today I have shared with you various ways of communicating across pages.

Common approaches to same-origin pages include:

  • Broadcast mode: Broadcast Channe/Service Worker/LocalStorage + StorageEvent
  • Shared storage mode: Shared Worker/IndexedDB/cookie
  • Word-of-mouth: window.open + window.opener
  • Server based: Websocket/Comet/SSE, etc

For non-homologous pages, non-homologous page traffic can be converted to homologous page traffic by embedding homologous IFrame as a “bridge”.

At the same time, this article is to share, but also to throw jade. If you have any other ideas, please feel free to discuss them and put forward your opinions and ideas

Those who are interested in the article are welcome to pay attention
My blog > > https://github.com/alienzhou/blog