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
The following types of online demos can be found 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 codeCopy 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 + '- the TAB' + data.from; console.log('[BroadcastChannel] receive message:', text); }; Copy the codeCopy the code
To send a message, simply call the postMessage method on the instance:
bc.postMessage(mydata); Copy the codeCopy the code
For details on how to use Broadcast Channel, see this article “3 Minute Overview of 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 Worker is also one of the core technologies in PWA. Since this article does not focus on PWA, if you want to know more about Service Worker, you can read my previous article “PWA Learning and Practice” (3) To make your WebApp available 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 codeCopy 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 codeCopy 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.data; const text = '[receive] ' + data.msg + '- the TAB' + data.from; console.log('[Service Worker] receive message:', text); }); Copy the codeCopy 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 codeCopy 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 + '- the TAB' + data.from; console.log('[Storage I] receive message:', text); }}); Copy the codeCopy 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 codeCopy 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 codeCopy 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 (const sharedWorker = new sharedWorker);'.. /util.shared.js'.'ctc'); Copy the codeCopy the code
Then, the Shared Worker supports messages in the form of GET and POST:
/ *.. /util.shared.js: shared Worker code */letdata = null; self.addEventListener('connect'.function (e) { const port = e.ports[0]; port.addEventListener('message'.functionThe (event) {// GET directive returns stored message dataif(event.data.get) { data && port.postMessage(data); } // A non-GET directive stores the message dataelse{ data = event.data; }}); port.start(); }); Copy the codeCopy 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:
// Timed polling, send message of get instructionsetInterval(function () { sharedWorker.port.postMessage({get: true}); }, 1000); / / monitored get message returned data sharedWorker port. AddEventListener ('message', (e) => { const data = e.data; const text = '[receive] ' + data.msg + '- the TAB' + data.from; console.log('[Shared Worker] receive message:', text); },false); sharedWorker.port.start(); Copy the codeCopy the code
Finally, when communicating across pages, just give the Shared Worker postMessage:
sharedWorker.port.postMessage(mydata); Copy the codeCopy the code
Note that if you use addEventListener to add the Shared Worker’s message listener, you need to explicitly call the messagePort.start method, i.e. Sharedworker.port.start () above; Not if you use the onMessage binding to listen.
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}); }}}); } Duplicate codeCopy 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; }); } Duplicate codeCopy 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); }}); } Duplicate codeCopy 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 codeCopy 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 + '- the TAB' + data.from; console.log('[Storage I] receive message:', text); }); }, 1000); }); Copy the codeCopy 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 codeCopy 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:
letchildWins = []; document.getElementById('btn').addEventListener('click'.function () { const win = window.open('./some/sample'); childWins.push(win); }); Copy the codeCopy 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); } Duplicate codeCopy 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 + '- the TAB' + data.from; console.log('[Cross-document Messaging] receive message:', text); // Avoid sending messages backif(window.opener && ! window.opener.closed && data.fromOpenner) { window.opener.postMessage(data); } childWins = childwins. filter(w =>! w.closed); // Avoid sending messages backif(childWins && ! data.fromOpenner) { childWins.forEach(w => w.postMessage(data)); }}); Copy the codeCopy 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) {/ /...dosomething}); Copy the codeCopy the code
Then, when a page wants to communicate with other homologous or non-homologous pages, it first sends a message to the IFrame:
Frames [0].window. PostMessage (mydata,The '*'); Copy the codeCopy 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'); // After receiving a message from the page, broadcast between iframes window.adDeventListener ('message'.function(e) { bc.postMessage(e.data); }); Copy the codeCopy the code
When other IFrames receive the notification, they synchronize the message to the page they belong to:
/* Iframe code *// For received (iframe) broadcast messages, notify the business page to which they belong bc.onMessage =function (e) { window.parent.postMessage(e.data, The '*'); }; Copy the codeCopy 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”.