Why a micro-frontend
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”.
Principles of micro-frontend
Micro-frontends have certain principles that allow for scaling an app.
- Domain-driven development : The whole app can be broken down on the basis of different domains or business logic. It can be further broken down into sub-features.
- End-to-end autonomy ** with a domain**: A micro app can be built independently and using different stacks. The team will have complete freedom over this part, and they will hold the responsibility for it.
- Innovation without affecting other parts : Sub apps can function independently.
- Independent deployment : Each team can develop and deploy artifacts at their own speed.
- Isolate failure : The rest of the app will work even if one micro feature goes down.
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.
Advantages of micro-frontend
The following features make micro-frontend architecture more resilient:
- Easier and faster feature development.
- Independent deployments.
- Cross-functional teams.
- Parallel development.
- Loose coupling.
- Clear contracts.
- Easier to add, change, or remove any code.
- Easier to test.
How to implement a micro-frontend
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 micro-frontends in your application.
- Routing micro-frontends.
- Composing micro-frontends.
- Communicating with micro-frontends.
Defining a micro-frontend
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.
Composing micro-frontends
There are multiple stages at which micro-frontend apps can be composed:
- Client-side : All the micro-frontends are composed and bundled at build time.
- Server-side : One container is loaded initially, and micro-frontends are lazy-loaded upon URL change: the content is returned by the server.
- Edge side : The view is assembled at the CDN level. Many CDN providers give us the option of using an XML-based markup language called Edge Side Includes (ESI).
Routing of micro-frontends
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.
Communication between micro-frontends
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.
Event bus: pub-sub, event emitter
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.
Using web storage
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.
Query string
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.
Example
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.
Getting started
- First, create a folder to hold the application.
| mkdir single-spa-app
cd single-spa-app
|
- Next, initialize the npm and install the dependencies.
| npm init -y |
- Then, install single-spa, React, and Vue’s regular dependencies.
| npm install react react-dom single-spa single-spa-react single-spa-vue vue |
- Now, install the following dev dependencies. Babel
| 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.
Configuring the application
Webpack configuration
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.
Babel configuration
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"
]
}
Configuring and initializing a single-spa app
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 |
React app
In src/react_,_ create the following two files.
| touch main.app.js root.component.js |
src/react/main.app.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;
src/react/root.component.js
import React from "react";
const App = () => <h1>Hello from React</h1>;
export default App;
Vue app
In src/vue , create the following two files.
| touch vue.app.js main.vue |
src/vue/vue.app.js
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;
src/vue/main.vue
<template>
<div>
<h1>Hello from Vue</h1>
</div>
</template>
Configuring Package.json
Add the following scripts in the package.json file to run the app.
"start": "webpack serve --mode development",
"build": "webpack"
Running the app
- To run the app, run the start script:
| npm start |
- Now, visit the following URLs to access the apps.
renders only react http://localhost:8080/react
renders only vue http://localhost:8080/vue
|
Resource
For more information, refer to the sample in the Micro-frontend with the single-spa repository on GitHub.
Conclusion
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!