Messaging between Browser Windows using ruto.js
At my work I have to build solutions around communication between browser and iframe or a popup window i.e. <iframe> or window.open(). I have to use postMessage JS API, and feel it is not very developer friendly. Consider the below situation:
- The parent window sends a message to the child window
- The child window receives this message
- The child calls a backend server with the message
- The child sends the response back to parent
- The parent wants to receive this backend response, does some calculation
- The parent then wants to send a second message to the child
- And the same thing repeats
While this may seem trivial when you are doing simple HTTP calls from frontend but across windows something like this is much more complicated. We have also to keep in mind that security is our outmost concern.
Let us try to code this thing. Parent is parent.com, child is at child.com.
I am assuming parent has child as an iframe.
1. Send Message from parent to child
iframe.contentWindow.postMessage(message, "child.com");
2. Receive message from parent in child
window.addEventListener("message", (event) => {
const expectedOrigin = "parent.com";
if (event.origin !== expectedOrigin) {
return;
}
const message = event.data;
doProcessingOfMessage(message);
});
3. Child does some processing
async function doProcessingOfMessage(message) {
const resp = await api(message);
return resp;
}
4. Child sends response back
window.addEventListener('message', (event) => {
const expectedOrigin = 'parent.com';
if (event.origin !== expectedOrigin) {
return
}
const message = event.data;
const resp = await doProcessingOfMessage(message);
window.parent.postMessage(resp, 'parent.com');
})
5. Parent receives the message
window.addEventListener("message", (event) => {
const expectedOrigin = "child.com";
if (event.origin !== expectedOrigin) {
return;
}
const message = event.data;
});
6. Parent sends 2nd message
window.addEventListener("message", (event) => {
const expectedOrigin = "child.com";
if (event.origin !== expectedOrigin) {
return;
}
const message = event.data;
const newMessage = getNewMessage(message);
iframe.contentWindow.postMessage(message, "child.com");
});
As we can see the process is completely event based. And writing code to do something like this gets very messy.
What if there was a nicer way to do this? Let us say:
const resp1 = await sendMessageToChild(message1);
const message2 = await someProcessing(resp1);
const resp2 = await sendMessageToChild(message2);
const message1 = await receiveMessageFromParent();
const resp1 = await someProcessing(message1);
await sendMessageToParent(resp1);
In the above scenario, the parent is the orchestrator and child is just responding to actions triggered by parent.
In a different scenario a child can be the orchestrator and the parent would be just responding to actions triggered by the child.
That means both the parent and child can send and receive. If parent is sender then child has to be receiver, and when parent is receiver the child has to be sender.
Handle Retry
We have to make sure that there are retries in communication. And also both parent and child should be ready to receive.
Security
We have to have origin checks in both child and domain. Ignore all messages if not received from where we want to receive.
Solution
I think the best way to do this would be to build it like a client/server kind of a thing. Let us take an example of web client and Node.js Express server.
Client:
fetch("/message")
.then((response) => response.json())
.then((data) => {
document.getElementById("response").textContent = JSON.stringify(
data,
null,
2
);
})
.catch((error) => {
document.getElementById("response").textContent = "Error: " + error;
});
Server:
app.get("/message", (req, res) => {
res.send("Hello, world!");
});
I started to write the solution but soon realized it is not easy. So I have to take a more conservative approach.
Introducing Ruto
Ruto is a lightweight (4KB), fast and easy-to-use JS library that streamlines the communication between parent and child window (iframe/popup).
It uses client-server design pattern to communicate between parent and child window. Any window can become the client or the server depending on who wants to send. It abstracts out the complications of postMessage API and provides a simple API to send and receive messages.
You can visit the GitHub page here.
Ruto Examples
Parent to iframe
Parent will send a message to iframe, Parent expects a response from the iframe within 10 secs.
Parent:
<iframe id="iframe" src="https://example.com/child.html"></iframe>
<script src="https://cdn.jsdelivr.net/gh/rajnandan1/ruto/dist/ruto.min.js"></script>
<script>
const iframe = document.getElementById("iframe");
const message = "Hello from parent";
const options = {
timeout: 10000,
};
ruto.send(
"https://example.com/parent-to-iframe/topic1",
iframe,
message,
options
)
.then((response) => {
console.log(response); })
.catch((error) => {
console.log(error);
});
</script>
Child:
<script src="https://cdn.jsdelivr.net/gh/rajnandan1/ruto/dist/ruto.min.js"></script>
<script>
ruto.receive(
"/parent-to-iframe/topic1",
window.parent,
(response, message) => {
console.log(message); return response.send("Hello from child");
}
);
</script>
Iframe to Parent
Iframe will send a message to parent, and expects a response from parent within 5 secs.
Parent:
<iframe id="iframe" src="https://example.com/child.html"></iframe>
<script src="https://cdn.jsdelivr.net/gh/rajnandan1/ruto/dist/ruto.min.js"></script>
<script>
const iframe = document.getElementById("iframe");
ruto.receive("/iframe-to-parent/topic1", iframe, (response, message) => {
console.log(message); return response.send("Hello from parent");
});
</script>
Child:
<script src="https://cdn.jsdelivr.net/gh/rajnandan1/ruto/dist/ruto.min.js"></script>
<script>
const message = "Hello from iframe";
const options = {
timeout: 5000,
};
ruto.send(
"https://example.com/iframe-to-parent/topic1",
window.parent,
message,
options
)
.then((response) => {
console.log(response); })
.catch((error) => {
console.log(error);
});
</script>
Parent will send a message to popup, and expects a response from popup within 5 secs.
Parent:
<script src="https://cdn.jsdelivr.net/gh/rajnandan1/ruto/dist/ruto.min.js"></script>
<script>
const popup = window.open(
"https://example.com/popup.html",
"popup",
"width=600,height=400"
);
const message = "Hello from parent";
const options = {
timeout: 5000,
};
ruto.send(
"https://example.com/parent-to-popup/topic1",
popup,
message,
options
)
.then((response) => {
console.log(response); })
.catch((error) => {
console.log(error);
});
</script>
Popup:
<script src="https://cdn.jsdelivr.net/gh/rajnandan1/ruto/dist/ruto.min.js"></script>
<script>
ruto.receive(
"/parent-to-popup/topic1",
window.opener,
(response, message) => {
console.log(message); return response.send("Hello from popup");
}
);
</script>
Popup will send a message to parent, and expects a response from parent within 5 secs.
Parent:
<script src="https://cdn.jsdelivr.net/gh/rajnandan1/ruto/dist/ruto.min.js"></script>
<script>
const popup = window.open(
"https://example.com/popup.html",
"popup",
"width=600,height=400"
);
ruto.receive("/popup-to-parent/topic1", popup, (response, message) => {
console.log(message); return response.send("Hello from parent");
});
</script>
Popup:
<script src="https://cdn.jsdelivr.net/gh/rajnandan1/ruto/dist/ruto.min.js"></script>
<script>
const message = "Hello from popup";
const options = {
timeout: 5000,
};
ruto.send(
"https://example.com/popup-to-parent/topic1",
window.opener,
message,
options
)
.then((response) => {
console.log(response); })
.catch((error) => {
console.log(error);
});
</script>