I’m Evgenii Gordeev, a Full-Stack Software Developer at Sphere Partners. In this article, I’m going to take you through a short journey through the history of micro frontends, followed by a use case of building a modern composite frontend based on the webpack module federation.
In the video above, you can find me going further back, from static to dynamic to asynchronous pages, so if that quick recap is of interest to you, play the video! Here we’ll focus on micro frontends. Let’s start with defining micro frontends.
What are micro frontends?
The “micro” in this case here is short microservices, and frontend is the opposite of backend. Since the introduction of html or even earlier, starting with templates and languages for server side html rendering, we started splitting our applications into frontend and backend. We split them into isolated interoperable applications, each having single responsibility, simpler and more manageable codebase, independent deployment process and dedicated support team behind it. The community called them microservices.
The current trend is to build a feature-rich and powerful browser application, aka single page app (SPA), which sits on top of a micro service architecture. Over time the frontend layer, often developed by a separate team, grows and gets more difficult to maintain. That’s what we call a “Frontend Monolith”. (source: micro-frontends.org). The term micro frontends first came up at the end of 2016, essentially extending the concept of microservices to the frontend world. The idea behind micro frontends is to think about a website or a web application as a composition of features, which are owned by independent teams. Each team has a distinct area of business or mission it cares about and specializes in. A team is cross-functional and develops its features end-to-end, from database to user interface.
Four key principles of micro frontends
- Incremental upgrades
- Simple, decoupled codebases
- Independent deployment
- Autonomous teams
It should support incremental upgrades when code is delivered to the user as soon as it’s ready without long development cycles. It requires simple and decoupled codebases, independent deployment and fully autonomous teams.
Micro-frontend Architectural Approaches
Here I’ll briefly outline several possibilities but the range is much wider!
Server-side composition
The first option is server-side composition and it’s been known for a long time. The idea is to include different pieces of HTML into a layout template depending on certain criteria. Here we define the $PAGE variable by matching against the URL that is being requested. The task can be developed and deployed independently and can even reside on separate domains and browsers and web servers are able to embed these pages as well. This example shows how micro frontends are not necessarily a new technique and do not have to be complicated.
Nginx config
server { ssi on; error_page 404 /index.html; location /home { set $PAGE 'home'; } location /about { set $PAGE 'about'; } location /help { set $PAGE 'help' } }
+
index.html
<html lang="en" dir="ltr"> <head> <meta charset="utf-8"> <title>An Application</title> </head> <body> <div class="navigation"> <a href="/home">Home</a> <a href="/about">About</a> <a href="/help">Help</a> </div> <div class="content"> <!--# include file="$PAGE.html" --> </div> </body> </html>
=
iFrames
Another well known approach is iFrames. By placing the page links into a navigation area and making javascript change the main area iFrame URL on click, we can achieve most of the micro frontends goals. These pages are very well isolated, can reside on different web servers and can be developed and deployed independently. The isolation of subpages make them less flexible and integrations are also a pain. Finally, iFrames present some more challenges when it comes to making the page fully responsive.
Pure Javascript
<html> <head> <title>An Application</title> </head> <body> <script src="https://localhost/home.js"></script> <script src="https://localhost/about.js"></script> <script src="https://localhost/help.js"></script> <div id="micro-frontend-root"></div> <script type="text/javascript"> const microFrontendsByRoute = { '/home': window.renderHomePage, '/about': window.renderAboutPage, '/help': window.renderHelpPage, }; const renderFunction = microFrontendsByRoute[window.location.pathname]; renderFunction('micro-frontend-root'); </script> </body> </html>
This approach is probably the most flexible. Each micro frontend is included into the page using a script tag. Upon load, it exposes a global function as its entry point. These scripts don’t render anything immediately. Instead, they attach entry-point functions to ‘window’. These global functions are attached to the window by the above scripts. Having determined the entry point function, we now call it, giving it the ID of the element where it should render itself.
Unlike iFrames, we have full flexibility to build integrations between our micro frontends however we like. We could extend the code in many ways. For example, if we download a Javascript bundle as needed or to pass data in and out when rendering a micro frontend. Good, right?
Web Components
The evolution of the previous example is web components. Instead of defining global functions and embedded Javascript bundles, we can define custom HTML elements. The end result is quite similar. Having determined the right web component custom element type, we now create an instance of it and attach it to the document. So if you like the web component specification, you can use that or if you need some extra flexibility, you can stick with the pure javascript approach.
<html> <head> <title>An Application</title> </head> <body> <script src="https://localhost/home.js"></script> <script src="https://localhost/about.js"></script> <script src="https://localhost/help.js"></script> <div id="micro-frontend-root"></div> <script type="text/javascript"> const webComponentsByRoute = { '/home': 'home-component', '/about': 'about-component', '/help': 'help-component', }; const webComponentType = webComponentsByRoute[window.location.pathname]; const root = document.getElementById('micro-frontend-root'); const webComponent = document.createElement(webComponentType); root.appendChild(webComponent); </script> </body> </html>
Webpack 5 and Module Federation
Webpack is an open source Javascript bundler. It’s made primarily for Javascript but can transform frontends assets such as HTML, CSS and images as well. Webpack takes models with dependencies with dependencies and generates static assets.
Webpack 5 was released in December of 2020. It introduced a revolutionizing plugin named Module Federation. The architecture was originally developed by Zack Jackson who then proposed to build a webpack plugin for it. The Webpack team agreed and they brought it to Webpack 5 as part of the standard installation. In short, module federation allows a Javascript application to dynamically import code from another application at the right time. The model will build a Javascript entry file which can be downloaded by other applications by setting up the Webpack configuration to do so.
Unfortunately I cannot share the production code with you. However, you can find a lot of examples here on GitHub.
Let’s take a look at one way. It reflects both the real complexity and simplicity of this approach very well. On the left of the image below, you can see a configuration of our host application which is running on port 3001 locally, it includes configuration of the module federation plugin and it defines a single remote alias as app2. You can see “app2” resides on port 3002 locally and exposes an entry point which in turn includes information about exported components. In this case, it’s a simple button component. This exposed component can be consumed by the first application and embedded into it.
Now let’s take a look at the application. The button component on the right of the image below is pretty simple and it is used by application 2 locally. You can see it in the middle. We import the button as “LocalButton” and render it. On the left, you can see how this remote component is being consumed by the host application. “React.lazy” is used to dynamically import the remote component. The “React.Suspense” protects our page from crushing while the button is being loaded and provides some fallback UI. At the bottom, you can find two independent applications running on a different port and potentially web servers and domains, using the same button component exported from application 2.
This was the simplest possible example but applications built with module federation can be much more complex. Let me share a high level overview of what my team and I have been working on lately.
Here we have a diagram of some wrapper application which embeds several micro frontends. The wrapper includes a react router which mounts certain owned components on specific endpoints like Home or About pages and it also mounts dynamically imported remote components onto dedicated URL subsets. The imported models are essentially react routers by themselves as well and they brought fully functional independent applications with all the assets, subpages and styles bundled. Each application knows its URL namespace and this allows us to avoid URL routes collision. We do not need to load and run all of the microservices at once if we work on a single microservice. As you see in this example, we can run each of the micro frontends from a separate domain.
But what if we need to put all of the compiled static assets into CDN for example? We can do that as well. It’s just a matter of reconfiguring our module federation import. So, each build would reside in a subdirectory and can be independently developed, built and deployed on demand.
Common Pitfalls and Challenges
So to summarize, module federation allowed us to build a complex Intranet dashboard consisting of nearly a dozen microservices and the number keeps increasing. Each of them is linked to a dedicated backend API microservice developed and deployed independently by separate teams and all of this runs smoothly as an ordinary SPA from the user POV.
Some of the challenges we faced include:
Styling and CSS scopes isolation – CSS modules to the rescue
We need to avoid clashing the CSS scopes and class names. CSS modules come to the rescue here and they effectively assign random class names to CSS classes for each individual component.
Shared Redux store – requires code sharing, so do not use it
We tried to implement something with Shared Redux store but our research led us to understand that it would require some code sharing so we would recommend to avoid using it. Let each micro frontend to have its own Redux store.
Cross-app communication – custom events can help
We did not use cross-app communication in our project. The common advice is to rely on custom Javascript browser events here.
Remote entries are down – Error Boundaries
Another problem that might arise is in case some of the micro frontends remote entries are not available, for e.g. the remote server is down. Error Boundaries effectively protect us in such cases so this allows us to implement some fallback components to be shown when the remote component is not available.
Session management and authentication – Secure HTTP-only cookies
This is a responsibility of the host application and in our case, we rely on secure HTTP-only cookies which are requested by the host application. All the mounted microservices and their components use these cookies to communicate with the backend API microservices.
Naming – do not use identical app names in package.json
We spent about a couple days troubleshooting an application that wasn’t working at the beginning of our development. It turned out that the module federation plugin is sensitive to distant names of micro frontends in package.json.
Caching – add some random string to the imported remote entry URL
Since we import remote components via static URL with remote entry .js, that might cause spam because on deployment of a new version of a micro frontend, the old cache version might be loaded by a web browser. So, make sure to add some random string to the imported URL.
Resources
Finally, I’d like to share some resources with you to explore further if you’re interested. I’ve listed them all here.
- https://micro-frontends.org/
- https://www.thoughtworks.com/radar/techniques/micro-frontends
- https://martinfowler.com/articles/micro-frontends.html
- https://webpack.js.org/concepts/module-federation/
- https://blog.bitsrc.io/revolutionizing-micro-frontends-with-webpack-5-module-federation-and-bit-99ff81ceb0
Working on a project that uses the module federation approach? As proven, our expert developer teams understand how to use modern approaches to bring your vision to life. Do not hesitate to reach out to us for help with your development projects. Alternatively, see more about our software development services here.