As web applications become increasingly complex, traditional frontend architectures struggle to keep up. To address this challenge, a potential solution is to adopt micro frontends, an architectural approach that involves dividing the client-side functionalities of an application into smaller, more manageable pieces.
At its core, micro frontends take inspiration from microservices - a popular architectural approach for backend development. It breaks down a web application into smaller, independently deployable parts, each with its own codebase and development team. Such division allows for faster development and deployment cycles, improved scalability, and better fault tolerance. With micro frontends, the same principles are applied to the front end. Instead of having a monolithic frontend codebase that handles all aspects of the application, the frontend is broken down into smaller, more manageable parts, each with its own codebase and development team. At a high level, a micro frontend is a self-contained module that can be developed, tested, and deployed independently. Each micro frontend is responsible for a specific feature or functionality of the web application, and it communicates with other micro frontends, typically through APIs. It allows each micro frontend to be developed in isolation, with its own stack and technology choices, and it can be developed by a separate team.
In our previous article,"Revolutionizing Frontend Development: Exploring Micro Frontends for Scalable and Efficient Web Application" we discussed in detail the concept of micro frontends and how they can help overcome the limitations of traditional frontend architectures. Check it out here to learn more about the advantages and disadvantages of this architecture as well as preferred use cases.
With a clear understanding of the concept of micro frontends and their potential benefits, the inquiry that naturally arises next centers around integrating them into a cohesive application. We are going to discuss the topic in detail in this article.
Micro frontends are integrated into a cohesive application commonly through a shell application, which acts as a container for the micro frontends and is also often referred to as a "host" or a "container" app. The shell application provides the overall structure and layout of the application, as well as any shared functionality or components that are used by the micro frontends. A shell app also serves as the entry point for the user and is responsible for managing the routing and communication between the individual micro frontends that make up the application. It provides a consistent look and feel across the application by enforcing shared design systems and user experience patterns.
Integration is usually done in two possible ways:
build-time integration and
runtime integration.
Built-time (compile-time) integration
Built-time (compile-time) integration combines all the micro frontends into a single codebase at build time. This method is faster than runtime integration because the browser only needs to load a single script, leading to improved design consistency. It is the sole option for creating micro frontend architecture in cross-platform solutions like React Native. However, modifying micro frontends requires updating and redeploying the shell app, which actually contradicts one of the critical principles of micro frontends, presupposing that each frontend module can be independently deployable. That is why, in situations other than mobile micro frontends and creating reusable modules, integrating micro frontends at runtime will be preferable. However, using this type of integration at a starting point with the aim of eventually moving toward runtime integration can still be a helpful way to initiate a transformation. One of the most popular frameworks for micro frameworks build-time integration is Bit.
Runtime integration
Runtime integration, on the other hand, involves loading each micro frontend separately at runtime and dynamically integrating them into the application. This approach is more flexible, allowing for better control over which micro frontends are loaded for each page. However, it can also be slower, as each micro frontend needs to be loaded separately.
Runtime integration has two main types of compositions:
Server-side composition and
Client-side composition:
Server-side composition during runtime integration
A server-side composition during runtime integration involves assembling micro frontend fragments into a complete page on the server side before delivering it to the client. This approach helps address performance constraints and when precise control over the user experience is necessary. Typically, a separate service is employed to perform composition, positioned between web browsers and servers. Content Delivery Networks (CDNs) are an example of such services. Server-side composition caches micro frontends at Content CDNs and composes them in a view later served at client runtime. A simple and popular technique for server-side template composition is Server Side Includes (SSI). SSI allows for the dynamic inclusion of content in web pages during server-side rendering and is supported by web servers like Apache, Nginx, and IIS. The following syntax <!--#include--> is used to include content from one file into another.
This is a sample of an HTML template and NGINX configuration file to be included by the server. The header.html file contains the HTML code for the header of the web page, and the footer.html file contains the HTML code for the footer. The header and footer are included on the page using SSI with the <!--#include--> syntax. The virtual attribute is used to specify the path to the file that should be included. When a request comes in for an HTML file, Nginx will enable SSI processing and set the content type to "text/html" using the ssi_types directive. The location / block sets the $header and $footer variables to the paths of the header and footer files. The ssi on directive enables SSI processing for this location block, and ssi_types text/html sets the content type to "text/html". Finally, the try_files directive specifies that if the requested file does not exist, Nginx should return a 404 error.
Client-side composition during runtime integration
A client-side composition during runtime integration presupposes that the micro frontend fragments are loaded on the client side. It provides greater flexibility and interactivity, as the client-side application can decide which fragments to load and when. However, it can result in slower page load times compared to server-side composition and can be more challenging to implement. There are several runtime integration techniques referring to client-side composition, and they are:
via JavaScript
via iframe
via web components
Client-side composition of micro frontends during runtime integration via JavaScript is the most flexible approach and is often used by teams working with micro frontends. This approach uses a <script> tag to incorporate each micro frontend into the page. On page load, a global function is revealed as the entrance to the micro frontend. When the shell application determines which micro frontends should be loaded, it calls the appropriate function to notify the micro frontend where and when to render its content.
This example has two micro frontends: a widget and an analytics component. Each micro frontend is loaded from a different URL and attaches its entry-point function to the window object. The microFrontendsByRoute object maps URL paths to the corresponding entry-point function. The renderFunction variable is set to the entry-point function based on the current URL path. Finally, the entry-point function is called with the ID of the element where it should render itself. In this case, both the widget and analytics components are rendered inside separate div elements with IDs of widget-root and analytics-root.
While most frontend frameworks can be used for micro frontends, some have been designed specifically for them, like Single SPA or WebPack Module Federation. Single SPA is a meta-framework for piecing a UI together using any combination of frontend frameworks like React, Angular, and Vue. WebPack Module Federation is a plugin for Webpack, a popular module bundler for JavaScript, that enables sharing of code and resources between different micro frontends securely and efficiently.
Client-side composition of micro frontends during runtime integration via iframe is a simple approach to composing applications together in the browser. Simply put, it is an HTML element that allows embedding another HTML document within the current document. With iframes, developers can build pages from autonomous sub-pages. It provides better isolation in terms of global variables and styling. Although the mentioned easy isolation can simplify the development process, it is worth mentioning that it may also result in less flexibility compared to other alternatives. It can lead to challenges when attempting to establish integrations between various application sections, making tasks such as routing more complex.
In this example, we have a simple HTML page with a navigation bar and an iframe element with an id of micro-frontend-container. The JavaScript code dynamically loads micro frontends based on the current route using an object that maps the route to the URL of the micro frontend. When the page loads, the JavaScript code sets the src attribute of the iframe to the URL of the micro frontend associated with the current route. It causes the micro frontend to load and render inside the iframe.
When integrating micro frontends at run time via web components, instead of defining a global function for the shell application to call, each micro frontend defines an HTML element for the shell to represent an instance. A web component is a self-contained element that encapsulates its functionality and presentation and can be reused across multiple applications without requiring additional code for integration or communication. By defining micro frontends as web components, developers can benefit from the built-in encapsulation and modularity provided by web components, allowing them to easily compose and reuse different micro frontends in a more flexible and scalable manner. Additionally, web components can be loaded and integrated at runtime using standard HTML and JavaScript APIs, making the integration process more straightforward and streamlined. Web components leverage HTML and the DOM API, making them accessible to other frontend frameworks. Additionally, they provide a standard method of sending and receiving data using props and events, allowing for seamless communication between components.
In this example, there are three micro frontends for analytics, chat, and calendar, each defined by a separate script. The custom element type for each micro frontend is stored in an object webComponentsByRoute, which maps URL routes to the corresponding web component types. When the page loads, the script determines the correct web component type based on the current URL path, creates an instance of that web component, and attaches it to the document. The resulting page will display the appropriate micro frontend based on the URL path. Routing is necessary to ensure that the users are directed to the correct micro frontend when interacting with a specific feature or page. Routing mechanisms such as URL-based routing or event-driven routing can be used to ensure that each micro frontend receives the necessary data. For example, if a user navigates to the "/products" page, the router should load the micro frontend responsible for displaying the products page. Similarly, if a user clicks on a product to view more details, the router should load the micro frontend responsible for displaying the product details. Routing in micro frontends can be implemented using various techniques, such as:
Client-side routing: involves using JavaScript to dynamically load micro frontends based on the requested URL. The main advantage of this approach is that it provides a seamless user experience, as the page does not need to refresh when navigating between micro frontends. However, it may require more initial loading time as all micro frontends need to be loaded upfront.
Server-side routing: involves using the server to determine which micro frontend should be loaded based on the requested URL. The main advantage of this approach is that it reduces the initial loading time, as only the necessary micro frontend is loaded. However, it may result in a slower user experience as the page may need to refresh when navigating between micro frontends.
The micro frontends can communicate with each other to share data and ensure a consistent user experience, using techniques such as events, API calls, or shared state management. Events can be used to trigger actions between micro frontends, such as updating a cart when a product is added to it. For instance, an event bus allows publish/subscribe-style communication between microservices without requiring the components to explicitly be aware of each other. API calls can be used to retrieve data from a microservice or other backend system. Shared state management can be used to keep data consistent across multiple micro frontends, such as maintaining a user's session information. One way to achieve shared state management is by using a centralized data store, such as a database or cache, accessible by all micro frontends.
In conclusion, micro frontends provide a promising solution to the challenges of building and maintaining large-scale web applications. By adopting this architectural approach, teams can work independently on specific features, enabling quicker development and easier maintenance. However, implementing micro frontends is not a one-size-fits-all solution. Each approach has its own trade-offs, and it's important to evaluate which option best fits the needs of your application.
Reading time 8 min 38 sec
Automation testing is a vital component in modern software development, significantly improving testing quality and efficiency. It's applicable to various software types, including web and mobile apps, desktop applications, and APIs.
Read moreThe journey to find a skilled programmer is a quest filled with opportunities and challenges. The intricacies of technology, coupled with the decision-making process between in-house teams, freelancers, and outsourced teams, can be overwhelming.
Read moreEffective branding isn't just about catchy logos and slogans; it's a journey that weaves a compelling story, reflecting your values and mission. It's a commitment to delivering a consistent, meaningful experience, fostering trust, and turning casual buyers into devoted customers.
Read more