Historically, front ends did not have many options for scaling an application. When the trend of having a fat server (all the processing happens on the server) and a thin client (minimal computation) started to shift, driven by the need to provide a better user experience, there was room for architecture that allowed features to be developed and shipped independently and faster.
When organizations started to adopt microservices on the server-side, the major force behind doing so was the idea to break monolithic codebases into smaller chunks, allowing them to distribute work among different teams without impacting their delivery throughput.
While domain-driven development has revolutionized the whole back-end dev ecosystem, breaking services into specific businesses or domains, a similar architecture can be applied to the front end.
Micro-frontend is a new architecture inspired by microservices. The concept is similar to microservices, where we can break the whole monolithic codebase into smaller codebases that can be developed autonomously by distributed teams.
According to Micro-Frontend.org:
“The idea behind micro frontends is to think about a website or web app 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”.
Micro-frontends have certain principles that allow for scaling an app.
Although it is not suitable for all projects, a micro-frontend can provide a new approach to building and scaling front-end apps. It also solves the key obstacles that companies have faced at both technical as well as organizational levels.
The following features make micro-frontend architecture more resilient:
If not implemented with a plan, even great architecture can lead to the worst codebase possible, and the same goes with a micro-frontend.
There are different ways of architecting a micro-frontend application, but majorly we can break them into four key areas:
Defining your micro-frontend is important, as it will have an impact on all your other decisions. Divide the app into different micro-frontends. Make this division either from logic or business viewpoint.
In the logical approach, split the same view into multiple front-ends based on the logical differences. For example, the recommendations view of Netflix can be a micro-frontend that is developed by a different team. Dividing the existing view into multiple parts is known as a horizontal split.
On the domain or business basis, split each view based on the domain logic—for example, Facebook’s marketplace and videos section. This type of division is known as a vertical split.
Micro-frontend architecture is composed of multiple views with horizontal and vertical splits depending upon the use case.
There are multiple stages at which micro-frontend apps can be composed:
Routing majorly depends upon the type of composition. In server-side composition, the routes are done through the origin, as the whole app logic is on the server. In edge-side composition, CDN is the prominent player, as it serves the micro-frontends by assembling them together via transclusion at the edge level, based on the requested page URL.
In client-side composition, the micro-frontends are loaded per demand and the current state of the app. For example, if the user is about to authenticate, the authentication micro-frontend will be loaded or the landing page will be loaded.
Apart from the above routing techniques, we can also use smart routing to configure the app according to our needs. For example, if we use an application shell that loads a micro-frontend as a single-page app. Then, the app shell is the central command for all the routing logic. The app shell will govern all the routing logic, and then it decides which micro-frontend to load based on its configuration. This is one of the best approaches when we have complex routing, as there is only a single point of failure or configuration.
Like routing, the communication between micro-frontends also depends upon the type of composition. We always want to notify other micro-frontends about the user interaction when we work with multiple micro-frontends on the same or different pages.
The communication between different micro-frontends may not be so trivial, especially when there are different teams building them. To sustain the principle of independent deployment, we need to make sure each micro-frontend is unaware of the others, even if they are horizontally split and are part of the same page.
In this case, we are left with a few options for better communication.
We can inject an event-bus mechanism into front-end development that will allow decoupled components to communicate with each other by emitting events or a bus and different micro-frontends in the same view. If the components are interested, they can listen and react to these events.
We can create this by adding a container to instantiate the event bus and injecting it inside all of the page’s micro-frontends.
Alternatively, we can also use custom events. These are custom-defined events with a custom payload. The payload includes the string that identifies the event and an optional object custom for the event. These custom events are dispatched via a common object-like window so that it is available to all the micro-frontends.
As long as all the micro-frontends are in the subdomain, we can store and access data through web storage like cookies, sessions, and local storage.
A query string is the least efficient way, but still, we can use it in scenarios where we can pass data through query strings and access them in micro-frontends.
Let us see how we can create a micro-frontend with a simple example. The following are the five most popular frameworks we can use to create a micro-frontend:
Apart from these, you can also use the Module Federation of the webpack to bootstrap and configure your custom micro-frontend app. To keep it simple, we are going to see a simple example using single-spa where we will use two different tech stacks (React and Vue) to create a micro-frontend.
mkdir single-spa-app cd single-spa-app
npm init -y
npm install react react-dom single-spa single-spa-react single-spa-vue vue
npm install @babel/core @babel/plugin-proposal-object-rest-spread @babel/plugin-syntax-dynamic-import @babel/preset-env @babel/preset-react babel-loader –save-dev
Webpack
npm install webpack webpack-cli webpack-dev-server clean-webpack-plugin css-loader html-loader style-loader vue-loader vue-template-compiler html-webpack-plugin –save-dev
Let’s move forward and create our project structure.
In the root of the app, create a new file named webpack.config.js with the following code.
const path = require("path"); const webpack = require("webpack"); const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const { VueLoaderPlugin } = require("vue-loader"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { mode: "development", entry: { "single-spa.config": "./single-spa.config.js", }, output: { publicPath: "/dist/", filename: "[name].js", path: path.resolve(__dirname, "dist"), }, module: { rules: [ { test: /\.css$/, use: ["style-loader", "css-loader"], }, { test: /\.js$/, exclude: [path.resolve(__dirname, "node_modules")], loader: "babel-loader", }, { test: /\.vue$/, loader: "vue-loader", }, ], }, node: { __filename: false, }, resolve: { extensions: [".tsx", ".ts", ".js", ".vue"], alias: { vue: "vue/dist/vue.esm-bundler.js", }, modules: [path.resolve(__dirname, "node_modules")], }, plugins: [ new CleanWebpackPlugin(), new VueLoaderPlugin(), new HtmlWebpackPlugin({ template: "/index.html" }), ], devtool: "source-map", externals: [], devServer: { historyApiFallback: true, static: "./", }, };
This will compile the React and Vue code and serve the generated builds.
In the root of the app, create a new file named .babelrc with the following code.
{ "presets": [ ["@babel/preset-env", { "targets": { "browsers": ["last 2 versions"] } }], ["@babel/preset-react"] ], "plugins": [ "@babel/plugin-syntax-dynamic-import", "@babel/plugin-proposal-object-rest-spread" ] }
Register different apps to the single-spa to configure how to bootstrap, mount, and unmount the micro-app.
Create a file named single-spa.config.js in the root of the app with the following code.
import { registerApplication, start } from "single-spa"; registerApplication( "vue", () => import("./src/vue/vue.app.js"), () => (location.pathname === "/react" ? false : true) ); registerApplication( "react", () => import("./src/react/main.app.js"), () => (location.pathname === "/vue" ? false : true) ); start();
Last but not the least, create an index.html file, which is the entry point of the application.
<html> <head> <title>Micro Frontend</title> </head> <body> <div id="react"></div> <div id="vue"></div> <script src="/dist/single-spa.config.js"></script> </body> </html>
The framework code resides inside the src folder. Create two directories with the name vue and react inside the src directory.
mkdir src src/vue src/react
In src/react, create the following two files.
touch main.app.js root.component.js
import React from "react"; import ReactDOM from "react-dom"; import singleSpaReact from "single-spa-react"; import Home from "./root.component.js"; function domElementGetter() { return document.getElementById("react"); } const reactLifecycles = singleSpaReact({ React, ReactDOM, rootComponent: Home, domElementGetter, }); export const bootstrap = reactLifecycles.bootstrap; export const mount = reactLifecycles.mount; export const unmount = reactLifecycles.unmount;
import React from "react"; const App = () => <h1>Hello from React</h1>; export default App;
In src/vue, create the following two files.
touch vue.app.js main.vue
import { h, createApp } from "vue"; import singleSpaVue from "single-spa-vue"; import App from "./main.vue"; const vueLifecycles = singleSpaVue({ createApp, appOptions: { el: "#vue", render() { return h(App); }, }, }); export const bootstrap = vueLifecycles.bootstrap; export const mount = vueLifecycles.mount; export const unmount = vueLifecycles.unmount;
<template> <div> <h1>Hello from Vue</h1> </div> </template>
Add the following scripts in the package.json file to run the app.
"start": "webpack serve --mode development", "build": "webpack"
`
npm start
# renders both apps http://localhost:8080/ # renders only react http://localhost:8080/react # renders only vue http://localhost:8080/vue
For more information, refer to the sample in the Micro-frontend with the single-spa repository on GitHub.
I hope now you have a clear idea of what a micro-frontend is, why you should use it, and the procedure to implement it. Try to create your own SPA and share your feedback in the comments section of this blog.
The Syncfusion React JS suite and the Vue UI components library offer over 65 high-performance, lightweight, modular, and responsive UI components in a single package. They are the only suites you’ll ever need to construct a complete application.
For questions, you can contact us through our support forum, support portal, or feedback portal. We are always happy to assist you!