Micro frontends architecture is still a hot topic in the Front-End field. Many projects are adopting or have already adopted this approach. However, it is one of the most controversial topics, and you should carefully consider the pros and cons before implementing it. Today, I will share how I dealt with this challenge and solved it in my application.
The first thing to consider is the business needs of the project and how your architecture will fit in. You can't build whatever you want – your decisions should align with the business direction. Secondly, evaluate the benefits of this architecture for your specific project. Don't rely solely on observations from others or follow trends without a cost-benefit analysis. Determine if the advantages are worth the cost.
Thirdly, address these common concerns before making the shift:
These are the minimal recommendations before you start development.
If you have answers to the above questions, you're ready to proceed with the technical implementation for Angular apps. Historically, Angular apps are built using Webpack. This is not a significant issue until you face the micro frontend topic. Different bundlers can have runtime compatibility issues. For example, integrating an Angular app into a React app built with Vite and esbuild can be problematic.
It makes more sense to follow web standards (ES Modules) rather than locking your projects to a specific bundler like Module Federation and Webpack.
Fortunately, since Angular 16, the Angular team has included esbuild out of the box. I recommend updating beyond version 16. Now, it's enough to update your angular.json file with the new builder and other fields:
{
"build": {
"executor": "@angular-devkit/build-angular:browser-esbuild",
"options": {
"stylePreprocessorOptions": {
"includePaths": [""]
},
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "./tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "1mb",
"maximumError": "2mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "1mb",
"maximumError": "2mb"
}
],
"outputHashing": "none",
"baseHref": "/"
},
"development": {
"optimization": false,
"extractLicenses":false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
}
}
It's a good practice to integrate the micro frontend with minimal effort. For example, you can expose a component that accepts the remote app name and link. This makes it easier for consumers, as they don't need to worry about the underlying processes. Consider the following example:
import { Component, Input, OnInit } from '@angular/core';
const MFE_CACHE_KEY = '__mfe-cache';
@Component({
selector: 'mfe-wrapper',
template: '<div id="mfe-root"></div>'
})
export class MfeWrapperComponent implements OnInit {
@Input() public url: string;
@Input() public appName: string;
public ngOnInit(): void {
const importFn = async (path: string): Promise<string> => {
if (window[MFE_CACHE_KEY]?.[path]) {
return window[MFE_CACHE_KEY][path];
} else {
const response = await fetch(path);
const scriptContent = await response.text();
window[MFE_CACHE_KEY] = { ...window[MFE_CACHE_KEY], [path]: scriptContent };
return scriptContent;
}
};
const element = document.getElementById('mfe-root');
const executeScript = (scriptContent: string): void => {
const scriptElement = document.createElement('script');
scriptElement.type = 'module';
scriptElement.async = true;
scriptElement.textContent = scriptContent;
element.appendChild(scriptElement);
};
element.appendChild(document.createElement(this.appName));
(async (): Promise<void> => {
const mainScript = await importFn(`${this.url}/main.js`);
executeScript(mainScript);
const polyfillsScript = await importFn(`${this.url}/polyfills.js`);
executeScript(polyfillsScript);
})();
}
}
In this example, we download the necessary scripts (main.js
and polyfills.js
) from the provided source. You must deploy your Angular build to the URL that you expose to the consumers. We convert the script into text and insert it into the script tag to avoid caching issues. You can extract the logic of downloading and script creation into separate scripts and reuse it for a React component wrapper for React app integrations – the approach remains the same.
One important note is dealing with styles. You may notice that angular.json
doesn't have a styles
field. This is intentional. If you leave this field, the builder creates a style.css
file, which can override the host application’s styles. By removing this field, the styles are added to the main.js
file. Additionally, you can wrap the app in ShadowDOM
to ensure that your styles don't override the host styles.
If you use UI libraries and components without styles encapsulation, these approaches won't help – the styles will leak into the host application’s head. Carefully choose the components you use.
Advantages:
Disadvantages:
Adopting a micro frontend architecture requires careful planning and consideration of both business and technical factors. The approach described here provides a way to integrate Angular applications into various host environments with minimal effort.
While it offers significant advantages, such as framework-agnostic integration and style encapsulation, there are also challenges to address, including compatibility with older Angular versions and path resolution issues.
By understanding these trade-offs and planning accordingly, you can effectively implement a micro frontend architecture that meets your project’s needs.