Promises with postMessage API

Promises with postMessage API

Type
Blog
Published
February 26, 2023
Author
Chetan Agrawal
 
The postMessage API is used to enable communication between two different windows or tabs in a web browser. There are several reasons why someone might need to use this API:
  1. Cross-origin communication: The postMessage API allows communication between different origins, i.e., different domains or subdomains, which is otherwise restricted by the Same-Origin Policy. This can be useful for sharing information or data between different applications running on different domains.
  1. Iframe communication: The postMessage API can be used to enable communication between an iframe and its parent window. This is useful when the iframe and parent window are from different domains and need to communicate with each other.
  1. Cross-window communication: If a web application uses multiple windows or tabs, the postMessage API can be used to enable communication between these windows or tabs. This can be useful for sharing data or information between different parts of the same application.
  1. Custom messaging protocols: The postMessage API allows developers to define their own messaging protocols for communication between windows or tabs. This can be useful when using third-party libraries or plugins that require custom messaging protocols.
 
If you are mostly developing a simple single page application, or a server side rendered implementation, you might never need to use a postMessage API. But if you are building a plugin or an extension for any of the existing application or trying to embed a functionality in your own website application, this is one of the heavily used contracts.

How to use postMessage?

Here are some code examples that can help you understand the contract of postMessage API.
 

Between parent and child window

Following code opens a new window and sends the string message via postMessage API.
var targetWindow = window.open('http://example.com'); targetWindow.postMessage('Hello from the parent window!', 'http://example.com');
parent.js
The new window example.com listens to post message call using the following contract. Note that the sent message is present in the event.data.
window.addEventListener('message', function(event) { console.log('Message received: ' + event.data); // Message received: Hello from the parent window! });
child.js
 

Between window and iframe

A very similar code like above can be used for iframe messaging too. Only a slight difference in the structure of accessing the iframe element.
var targetIframe = document.getElementById('my-iframe'); targetIframe.contentWindow.postMessage( 'Hello from the parent window!', 'http://example.com' );
parent.js
Listening to the message is exactly same on the other side.
window.addEventListener('message', function(event) { console.log('Message received: ' + event.data); // Message received: Hello from the parent window! });
iframe.js
The origin of the message is important element here to filter and allow sending messages via postMessage API. More on the security implications in a different article.
 
To extend the above and allow cross communication between both the child and parent, we can add listeners and send messages on both side. Here is the small contract that you can use inside your application.
 
// parent.js const sendMessage = (messagePayload, targetIframe, origin) => { targetIframe.contentWindow.postMessage( mesagePayload, origin // iframe's origin url ); } window.addEventListener('message', function (event) { processMessage(event.data) } const processMessage = (messageEvent) => { // add your business logic }
// iframe.js const sendMessage = (messagePayload, origin) => { window.parent..postMessage( mesagePayload, origin // parent's origin url ); } window.addEventListener('message', function (event) { processMessage(event.data) } const processMessage = (messageEvent) => { // add your business logic }
iframe.js

Need for promise in postMessage

If you look at the above contracts for sending messages via postMessge, there is not way to determine if the message was actually received and processed by the intended recipient of the message. It doesnt have a promise like structures which confirms if the message was received or processed. If you look at MDN documentation as well, there is no return value for the API.
With this writing a crisp communication protocol between the child and parent applications becomes hard as there is not way to know if the message was actually received and processed.
 

MessageChannel to the rescue

In the same MDN documentation, we also have a 3rd argument of TransferableObjects. Transferable Objects help with efficient transfer of large amounts of data between different parts of a web application or between different web workers, without duplicating the data. They can help significantly in improving performance and reducing memory usage for process large objects.
For our use case, we want to use MessageChannel and Message Ports. The following document describes the usage very well and this article is also derived application of the same document.
 
Message Channel helps in creating a communication channel using a shared worker between different windows and/or iframes. Here is a small example of how one can use MessageChannel to communicate messages between applications. Github Link to the demo code from MDN
//parent.js const channel = new MessageChannel(); const iframe = document.querySelector('iframe'); // Wait for the iframe to load iframe.addEventListener("load", onLoad); function onLoad() { // Listen for messages on port1 channel.port1.onmessage = onMessage; // Transfer port2 to the iframe iframe.contentWindow.postMessage("Hello from the main page!", "*", [ channel.port2, ]); } // Handle messages received on port1 function onMessage(e) { // process message }
parent.js
//child.js window.addEventListener("message", onMessage); function onMessage(e) { // process message // Use the transfered port to post a message back to the main frame e.ports[0].postMessage("Message back from the IFrame"); }
child.js
If you look closely, the message event also sends an extra MessagePort information which can be used to send messages back to the sender of the message, again using the postMessage API.
We can augment the above code a bit to create a Promise like structure around the same and use a more crisp contract to sequence our application callback actions.

Promises with postMessage and MessageChannel

Following is a demo code of how we can implement promises around the postMessage api.
 
//parent.js const postMessagePromise = (message: any, iFrameRef: any) => { return new Promise((resolve, reject) => { const iFrameWindow = iFrameRef.contentWindow; const messageChannel = new MessageChannel(); messageChannel.port1.onmessage = (res: any) => { // resolve promise when server sends back // message on the mentioned port if (res.data.success) resolve(res); }; // reject promise if the message response on port times out setTimeout(() => { reject(); }, 30000); try { iFrameWindow.postMessage(message, CLIENT_URL, [messageChannel.port2]); } catch (e) { reject(e); } }); };
parent.js
// client.js window.addEventListener("message", (messageEvent) => { messageEvent.ports[0]?.postMessage({ success: true }); // ... process messageEvent });
client.js
With above parent code example, the function returns a Promise which resolves or rejects based on the response received from the server. Inside the Promise, a new MessageChannel is created, and its port1 property is assigned a callback function that will execute when a response is received from the server. If the response contains a success property that is truthy, then the Promise is resolved with the response object.
If the response does not arrive within 30 seconds, the Promise is rejected.
Finally, the postMessage() method is called on the iFrameWindow object, passing in the message, CLIENT_URL, and the messageChannel.port2 as arguments. If an error occurs during this process, the Promise is rejected with the error object.
In the client code example, we simply message back on the port sent by parent with a success = true object to confirm that we have received the message.
 
This very simple wrapper helps in defining a promise like structure around postMessage api and also helps in implementing more stricter contracts between the apps on different windows or frames.
 

Further Reading

There are more libraries that are built around above functionality and much more to support inter frame communications. Listing down some of them for further explorations.
  1. https://github.com/krakenjs/zoid - one of the most populate ones.
  1. https://github.com/dollarshaveclub/postmate - simpler functionality without any complex features.
  1. https://github.com/davidjbradshaw/iframe-resizer - very specific use case.
 
Drop me a note if you liked the article or have comments to fix any thing. 😊