Micro Frontend app with Reverse Proxy (Part 2)
Introduction
In part 1 of the previous article, I introduced two react projects that are configured with Nginx and a reverse proxy. The separation of apps per route was a problem, each could only be loaded in their route, preventing both apps from being displayed on a single page. Additionally, enabling communication between the two apps was a significant concern. In this article, I will demonstrate how to integrate and use both applications on a single page, addressing the communication challenge. You can check the repo.
Load Apps
To load two apps on one page, we can do two things:
- SSI (Server Side Includes), this is a risky way.
- Iframe
Server Side Includes
Server-side includes (SSI) is a mechanism that helps developers insert dynamic content into HTML files without requiring knowledge of the server—or client-side programming language specification. If you want to load your apps with SSI, add some extra configuration to your Nginx. However, SSI has some costs, so you should be aware of the risks and prevent SSI injection.
SSI is enabled by including SSI directives in Nginx
server {
root /usr/share/nginx/html;
listen 8000;
server_name localhost;
ssi on;
location ~ ^/page1/(.*)$ {
set $PAGE "remote/page1/$1";
try_files $uri $uri/ /index.html;
}
}
then you can use it in your HTML file:
<main>
<!--#if expr="$PAGE" -->
<!--#include virtual="$PAGE" -->
<!--#endif-->
</main>
I am not a fan of this implementation, so I skip it.
Iframe
The <iframe>
HTML tag allows you to load a secondary HTML document within your main document. While it offers a lot of flexibility, it also comes with several drawbacks and may not always be the best strategy.
Using iframe is easy you just need to add HTML tag and add src for it, but the important concern now is how to communicate between iframe and container app. So I jump into communication.
Communication
In the micro-frontend world, communication is vital. However, managing communication between separate apps can be challenging, especially if you are using a multi-repo setup. In this example, we are merging these apps using iframes, and since they are not controlled by Module Federation with Webpack, we cannot handle this at build time.
We also follow an important rule: iframes should not call or send events directly to each other. In large projects, direct communication between iframes can lead to numerous side effects and obscure errors. Instead, we have a shell/container app that handles this responsibility, passing data through a specific layer.
What kinds of data or states do you want to share?
Before discussing communication, it’s critical to consider your states
and data. The method of communication you choose depends on the type of data you have.
In some cases, you might need to employ two strategies simultaneously. For instance, if your data consists only of an ID or token, adding to query
is the simplest approach. On the other hand, if you need to persist data or if the process of updating is unidirectional from parent to child, your strategy will differ.
Additionally, if your data depends on child apps, your approach must accommodate that. Therefore, it’s essential to carefully consider your data and states within your application. Once you’ve done that, you can select one or more appropriate communication methods.
Communication ways
We have some options for communication:
- Custom Event
- Workers
- Share Storage like local/session storage or IndexedDB
- remote-dom (Maybe it can be beneficial)
These options need further exploration, and there might be better approaches I haven’t considered yet.
Custom Event
Custom event is a simple solution, you can simply add an event from a child app like this:
useEffect(() => {
const event = new CustomEvent('mount',
{
detail: { product: "blog", isLoaded: true }
});
window.parent.dispatchEvent(event);
}, [])
and in your container app, you can listen like this:
useEffect(() => {
if (app.loading) return;
const iframeWindow = iframeRef.current.contentWindow;
const handleMountEvent = (event) => {
const { detail } = event
setWhereWeAre(detail.product)
};
window.addEventListener("mount", handleMountEvent);
return () => {
if (iframeWindow) {
iframeWindow.removeEventListener("mount", handleMountEvent);
}
};
}, [app]);
I added some other events, but I didn’t put them here, you can check the code (04-event-custom-communication branch), The result is something like this:
For instance, in our example, header
and footer
comes from the container app, and others are from the store app(also the items in cart
text), but the number of items you add, is synced between them.
Worker
The second way is Worker
. You can use Worker
or sharedWorker
, I use sharedWorker
here.
In this example, I use Workers, specifically SharedWorker
, because it ensures that all iframes share the same worker, allowing data to be shared between them. Additionally, you can create just one instance of a class in this context.
For instance, you can combine the pub/sub pattern with a worker. You create one instance of a publisher, which others can then use.
Another challenge is ensuring that Project A uses this specific worker, given that the projects are separate. We should have just one worker file for all projects. To achieve this, we create the worker file in the container app and modify our Nginx configuration slightly to expose that file:
location /sharedWorker.js {
alias /usr/share/nginx/html/sharedWorker.js;
}
Also pay attention to security side effects, this is an example, maybe in real world you should add some headers or do some security checks.
Now, in other applications like store
project, we can use localhost:8000/sharedWorker.js
to import the same worker.
useEffect(() => {
const worker = new SharedWorker('http://localhost:8000/sharedWorker.js');
setWorker(worker)
}, [])
useEffect(() => {
if(worker) {
worker.port.start();
worker.port.onmessage = (e) => {
const { type, event, data } = e.data;
if (type === 'subscribe' && event === 'cartUpdated') {
console.log('Cart updated in container:', data);
setTotalItemCart(data.count)
}
};
}
return () => {
worker && worker.port.close();
};
}, [worker]);
Also, you can check the code here.
If you are interested, you can use Comlink.
Conclusion
In this blog, we saw a basic sample MFE with Reverse Proxy (aka Route Distribution ), We also have other strategies like Module Federation
, Native Federation
and import maps
and etc. I will cover these topics in the next articles.
Thanks for your attention.