An iframe, or inline frame, is a HTML element that embeds another HTML page in an existing page. Remember seeing blog posts on the internet with the Facebook 'like' button? Those use iframes. But iframes can be used for all sorts of purposes: show advertisements, play videos, present a tweet, or display a page to sign a contract. At Anvil, we build our product with iframes in mind so customers can embed our product into their application.
A parent page and an iframe hold different browsing contexts. Parents and iframes within the same origin can freely access each other's information because it's all shared. But more often than not, the parent and the iframe will have different origins. In this situation, the same-origin policy
becomes an issue. The policy is a security mechanism that blocks code from different origins from interacting with each other. An iframe won't have access to a function from the parent window, for example.
Luckily, Window.postMessage()
provides a way to securely communicate between cross-origin environments. The API works by serializing the data using the structured clone algorithm and sending it over to the destination. The data will automatically be deserialized in the receiving end.
The problem
There are many cases where information crucially needs to be passed between iframes and their parent window. Many Anvil customers embed Etch e-sign, a PDF electronic signature tool, into their applications. Messages need to be passed from the e-sign page to the customer app informing it that the signature process is complete. In this case, the messages between the iframe and parent app act as webhooks.
Another scenario where communication with iframes is necessary is when building a sandbox to execute potentially malicious code. Code within iframes live in an isolated browsing context, so the code won't have access to cookies or sensitive information.
In my case, I need to write a function that tells an iframe to execute some code which then returns a response based on the result of the executed code.
As an example, let's imagine a function that accepts an array of numbers as a parameter. Let's call the function sumArrayInIframe()
. The function will send the array to an iframe using Window.postMessaage()
. The iframe will receive the message, sum up the numbers, then transmit the sum back to the parent window to be returned by sumArrayInIframe()
.
Let's use postMessage()
and add an event listener on both the parent and iframe end.
Parent window code:
// our function which takes an array of numbers and returns the sum
async function sumArrayInIframe(arr) {
return new Promise((resolve, reject) => {
// listen for message from the iframe
window.addEventListener(
'message',
(event) => {
if (event.origin !== 'http://exampleIframeWindow.com') return
if (event.data instanceof Error) reject(event.data)
else resolve(event.data)
},
false
)
// send message to iframe
document
.getElementById('codeExecutionIframe')
.contentWindow.postMessage(arr, 'http://exampleIframeWindow.com')
})
}
// call the function
const sum = await sumArrayInIframe([5, 2, 3]) // sum is 10
Iframe code:
function sumArray(arr) {
return arr.reduce((partialSum, num) => partialSum + num, 0)
}
// listen for event from parent window and immediately send back a response
window.addEventListener(
'message',
(event) => {
if (event.origin !== 'http://exampleParentWindow.com') return
const sum = sumArray(event.data)
window.parent.postMessage(sum, 'http://exampleParentWindow.com')
},
false
)
This code allows freely transmitting information between the parent and the iframe using window.postMessage()
. The crux is the window event handler and the message sending function. Messages can be sent to different destinations using postMessage()
, but all messages pass through the same window.addEventListener()
handler in global scope.
This could become an issue if two different calls to sumArrayInIframe()
are called simultaneously. What if the two responses from the iframe get mixed up and each is received by the other sumArrayInIframe()
function call?
For example, sumArrayInIframe([1,-1])
could return 10 while sumArrayInIframe([5,2,3])
could return 0.
The solution
We need to address the issue of associating each response to its respective function call. Instead of having an event listener that is global in scope, it needs to be locally scoped to each function call.
Here we introduce MessageChannel
, a channel messaging API interface, that allows scripts from different browsing contexts attached to the same document to communicate directly with each other using two-way channels with ports on each end. Messages can be sent between iframes, a parent and an iframe, and web workers.MessageChannel
does not replace Window.postMessage()
, but rather complements it.
A MessageChannel
is created using the MessageChannel()
constructor and consists of two MessagePorts
. The first port is attached to the origin context and kept in the parent window, while the second port is attached to the destination context. Thus port2
is transferred to and kept in the iframe.
Instead of adding a global event listener such as window.addEventListener()
, we'll be listening through channel.port1
and channel.port2
on the parent and iframe respectively.
Let's take a look at my code using MessageChannel
.
Parent window:
async function sumArrayInIframe(arr) {
return new Promise((resolve, reject) => {
// construct a message channel for each function call
const channel = new MessageChannel()
// listen for message on port1 which is attached to the origin
channel.port1.onmessage = (event) => {
if (event.data instanceof Error) reject(event.data)
else resolve(event.data)
channel.port1.close()
}
// send message AND port2 to iframe
document
.getElementById('codeExecutionIframe')
.contentWindow.postMessage(arr, 'http://exampleIframeWindow.com', [
channel.port2,
])
})
}
Iframe window:
window.addEventListener('message', (event) => {
if (event.origin !== 'http://exampleParentWindow.com') return
const sum = sumArray(event.data)
// send the result back using port2 which is attached to the destination
event.ports[0].postMessage(sum)
})
Imagine a railway system. The messages are the trains while the ports are the train stations. Port1
is station A, the train station next to where you live. Port2
is station B, the train station located next to where you work. In this example, we're simply using MessageChannel
to receive messages back from the destination. port2
is used to send the returning message, while port1
is used to receive the returning message. By designing our solution this way, you can ensure each returning message arrives at the right destination.
Summary
Many have never encountered the MessageChannel
API before as it's kind of niche, but I've been using it and it is a spectacular API that solves many problems for us as developers. While simply using postMessage()
can get the job done in many cases, there are specific scenarios where MessageChannel
makes your life a whole lot easier. The strength in MessageChannel
lies in the fact that you can compartmentalize channels of communication. I've given just one example of how this API can be leveraged, but I'm sure you can find other use cases as well.
We've applied this API and other best practices to our code at Anvil and believe sharing our experiences helps everyone in creating awesome products. If you're developing something neat with PDFs or paperwork automation, let us know at developers@useanvil.com. We'd love to hear from you.