introduction
In the browser, we can open multiple Tab pages at the same time. Each Tab page can be roughly understood as an "independent" running environment. Even global objects will not be shared among multiple tabs. Sometimes, however, we want to be able to synchronize page data, information, or status between these "independent" Tab pages.
As in the following example: after I click "favorite" on the list page, the corresponding details page button will be automatically updated to "favorite" status; Similarly, after clicking "favorite" on the details page, the buttons in the list page will also be updated.
This is what we call front-end cross page communication.
What do you know about cross page communication? If not, let me show you seven ways of cross page communication.
1, Cross page communication between homologous pages
In the following ways Online Demo can be poked here > >
Browser Homology strategy There are still limitations in some cross page communication methods described below. Therefore, let's take a look at what technologies can be used to realize cross page communication when the homology strategy is met.
1. BroadCast Channel
BroadCast Channel Can help us create a communication channel for broadcasting. When all pages listen to messages on the same channel, the messages sent by one page will be received by all other pages. Its API and usage are very simple.
You can create a channel identified as AlienZHOU in the following way:
const bc = new BroadcastChannel('AlienZHOU');
Each page can listen to the broadcast message through onmessage:
bc.onmessage = function (e) { const data = e.data; const text = '[receive] ' + data.msg + ' - tab ' + data.from; console.log('[BroadcastChannel] receive message:', text); };
To send a message, you only need to call the postMessage method on the instance:
bc.postMessage(mydata);
See this article for specific usage of Broadcast Channel [3-minute quick view] front end broadcast communication: Broadcast Channel.
2. Service Worker
Service Worker It is a Worker that can run in the background for a long time and can realize two-way communication with the page. Service workers among multi page sharing can be shared, and the broadcasting 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) make your WebApp available offline.
First, you need to register the Service Worker on the page:
/* Page logic */ navigator.serviceWorker.register('../util.sw.js').then(function () { console.log('Service Worker login was successful'); });
Where... / util sw. JS is the corresponding Service Worker script. The Service Worker itself does not automatically have the function of "broadcast communication". We need to add some codes to transform it into a message transfer station:
/* ../util.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); }); }) ); });
We listen to the message event in the Service Worker, get the information sent by the page (called client from the perspective of the Service Worker), and then get all the pages currently registered with the Service Worker through self.clients.matchAll(), Send a message to the page by calling the postMessage method of each client (i.e. page). In this way, the message received from one place (a Tab page) is notified to other pages.
After processing the Service Worker, we need to listen to the message sent by the Service Worker on the page:
/* Page logic */ 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); });
Finally, when you need to synchronize messages, you can call the postMessage method of Service Worker:
/* Page logic */ navigator.serviceWorker.controller.postMessage(mydata);
3. LocalStorage
As the most commonly used local storage in the front end, you should be very familiar with it; but StorageEvent This event related to it may be unfamiliar to some students.
When the LocalStorage changes, the storage event is triggered. Using this feature, we can write messages to a local storage when sending messages; Then, in each page, you can receive the notification by listening to the storage event.
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); } });
Add the above code on each page to listen to the changes of 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));
Note that here is a detail: we have added a timestamp to mydata to get the current millisecond timestamp st attribute. This is because the storage event is triggered only when the value actually changes. for instance:
window.localStorage.setItem('test', '123'); window.localStorage.setItem('test', '123');
Since the value '123' of the second time is the same as that of the first time, the above code will only trigger the storage event at the first setItem. Therefore, we set st to ensure that the storage event will be triggered every time we call.
Take a nap
We have seen three ways to realize cross page communication above. Whether it is to establish the Broadcast Channel of the Broadcast Channel, use the message transfer station of the Service Worker, or some tricky storage events, they are "broadcast mode": a page notifies the message to a "central station", and then the "central station" notifies each page.
In the above example, the "central station" can be a BroadCast Channel instance, a Service Worker or LocalStorage.
Next, we will see two other cross page communication modes, which I call "shared storage + polling mode".
4. Shared Worker
Shared Worker Is another member of the Worker family. Ordinary workers operate independently and data are not interlinked with each other; Shared workers registered with multiple tabs can realize data sharing.
The problem with Shared Worker in cross page communication is that it cannot actively notify all pages. Therefore, we will use polling to pull the latest data. The idea is as follows:
Let Shared Worker support two kinds of messages. One is post, and the Shared Worker will save the data after receiving it; The other is get. After receiving the message, the Shared Worker will send the saved data to the page registering it through postMessage. That is, let the page actively obtain (synchronize) the latest messages through get. The specific implementation is as follows:
First, we will start a Shared Worker in the page. The starting method is very simple:
// The second parameter of the constructor is the Shared Worker name, which can also be left blank const sharedWorker = new SharedWorker('../util.shared.js', 'ctc');
Then, messages in the form of get and post are supported in the Shared Worker:
/* ../util.shared.js: Shared Worker code */ let data = null; self.addEventListener('connect', function (e) { const port = e.ports[0]; port.addEventListener('message', function (event) { // The get instruction returns the stored message data if (event.data.get) { data && port.postMessage(data); } // Non get instructions store the message data else { data = event.data; } }); port.start(); });
Then, the page regularly sends the message of get instruction to the Shared Worker, polls the latest message data, and listens for the returned information on the page:
// Regular polling, sending the message of get instruction setInterval(function () { sharedWorker.port.postMessage({get: true}); }, 1000); // Listen for the return data of the get message 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();
Finally, when you want to communicate across pages, just send a PostMessage to the shared worker:
sharedWorker.port.postMessage(mydata);
Note that if you use addEventListener to add message listening of Shared Worker, you need to explicitly call messageport The start method, sharedworker port. start(); If onmessage binding is used to listen, it is not required.
5. IndexedDB
In addition to using Shared Worker to share storage data, you can also use other "global" (cross page) storage schemes. For example IndexedDB Or cookie s.
Since everyone is already familiar with cookies, and as "one of the earliest storage schemes on the Internet", cookies have borne far more responsibilities in practical application than they were designed at the beginning, we will use IndexedDB to implement them below.
The idea is simple: similar to the Shared Worker scheme, the message sender saves the message in IndexedDB; The receiver (for example, all pages) obtains the latest information through polling. Before that, we simply encapsulate several IndexedDB tools and methods.
- 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}); } } }); }
- Store 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; }); }
- 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); } }); }
The rest of the work is very simple. First open the data connection and initialize the data:
openStore().then(db => saveData(db, null))
For message reading, you can poll 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); });
Finally, to send a message, simply store data in IndexedDB:
openStore().then(db => saveData(db, null)).then(function (db) { // ... omit the polling code above // The method that triggers saveData can be placed in the event listener of user operation saveData(db, mydata); });
Take a nap
In addition to the "broadcast mode", we also learned about the "shared storage + long polling" mode. You may think that long polling is not as elegant as listening mode, but in fact, when using the form of "shared storage", it is not necessary to match long polling.
For example, in a multi Tab scenario, we may leave Tab A and operate in another Tab B; After a while, when we switch back from Tab B to Tab a, we want to synchronize the information of the previous operations in Tab B. At this time, you can only listen to events such as visibility change in Tab a for information synchronization.
Next, I will introduce another communication mode, which I call "word of mouth" mode.
6. window.open + window.opener
When we use window When opening a page, the open method returns a reference to the window of the open page. When the specified nopopener is not displayed, the opened page can be displayed through window Opener gets references to the pages that open it -- in this way, we connect these pages (a tree structure).
First, we put window Collect the window objects of the open page:
let childWins = []; document.getElementById('btn').addEventListener('click', function () { const win = window.open('./some/sample'); childWins.push(win); });
Then, when we need to send a message, as the initiator of the message, a page needs to notify it of the open page and the open page at the same time:
// Filter out closed windows 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); }
Attention, I'll use it here first The closed property filters out Tab windows that have been closed. In this way, the task of being the message sender is completed. Let's see what it needs to do as a message receiver.
At this time, a page that receives a message cannot be so selfish. In addition to displaying the received message, it also needs to deliver the message to the "people it knows" (open and open the page):
It should be noted that by judging the source of the message, I avoid returning the message to the sender and preventing the message from being transmitted in an endless loop between the two. (there will be some other small problems in the 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); // Avoid message return if (window.opener && !window.opener.closed && data.fromOpenner) { window.opener.postMessage(data); } // Filter out closed windows childWins = childWins.filter(w => !w.closed); // Avoid message return if (childWins && !data.fromOpenner) { childWins.forEach(w => w.postMessage(data)); } });
In this way, each node (page) shoulders the responsibility of transmitting messages, that is, what I call "word of mouth", and messages flow in this tree structure.
Take a nap
Obviously, there is a problem with the "word of mouth" mode: if the page does not pass through the window in another page Open (for example, enter directly in the address bar or link from other websites), the connection is broken.
In addition to the above six common methods, there is another (seventh) method to synchronize through "server push" technology such as WebSocket. This is like moving our "central station" from the front end to the back end.
About WebSocket and other "server push" technologies, students who do not understand can read this article Principles and examples of various "server push" technologies (Polling/COMET/SSE/WebSocket)
In addition, I also wrote one for each of the above ways Demo for online presentation > >

2, Communication between non homologous pages
Above, we have introduced seven front-end cross page communication methods, but most of them are limited by the homology policy. However, sometimes we have two product lines with different domain names, and we also hope that all the pages below them can communicate without obstacles. What should I do?
To achieve this function, you can use an iframe invisible to the user as the "bridge". Since the same origin restriction can be ignored between iframe and parent page by specifying origin, an iframe can be embedded in each page (for example: http://sample.com/bridge.html )Because these iframes use a url, they belong to homologous pages, and their communication methods can reuse the various methods mentioned in the first part above.
The communication between the page and iframe is very simple. First, you need to listen to the messages sent by iframe in the page and do the corresponding business processing:
/* Business page code */ window.addEventListener('message', function (e) { // ...... do something });
Then, when the page wants to communicate with other homologous or non homologous pages, it will first send a message to iframe:
/*Business page code / window frames[0]. window. postMessage(mydata, '’); Copy code
For simplicity, the second parameter of postMessage is set to '*' here, and you can also set it to the URL of iframe. After iframe receives a message, it will use some cross page message communication technology to synchronize messages among all iframes, such as the following Broadcast Channel:
/* iframe Internal code */ const bc = new BroadcastChannel('AlienZHOU'); // After receiving the message from the page, broadcast between iframe s window.addEventListener('message', function (e) { bc.postMessage(e.data); });
After other iframe s receive the notification, they will synchronize the message to the page they belong to:
/* iframe Internal code */ // For the received (iframe) broadcast message, notify the service page to which it belongs bc.onmessage = function (e) { window.parent.postMessage(e.data, '*'); };
The following figure shows the communication mode between non homologous pages using iframe as a "bridge".
The "homologous cross domain communication scheme" can use some technology mentioned in the first part of the article.
summary
Today I shared with you various ways of cross page communication.
Common methods for homologous pages include:
- Broadcast mode: broadcast channel / service worker / localstorage + storageevent
- Shared storage mode: Shared Worker / IndexedDB / cookie
- Word of mouth mode: window open + window. opener
- Based on server: Websocket / Comet / SSE, etc
For non homologous pages, the non homologous page communication can be transformed into homologous page communication by embedding homologous iframe as a "bridge".