How do you implement micro-frontend architecture in Angular?
Micro-frontend architecture breaks down a monolithic frontend into smaller, independently deployable units, each potentially developed by different teams. In Angular, this is primarily achieved using Webpack 5's Module Federation, which enables multiple standalone Angular applications to be composed into a single, cohesive user experience by dynamically loading code at runtime.
1. Understanding Module Federation
Webpack 5 Module Federation is the recommended and most robust way to implement micro-frontends in Angular. It allows an application (the 'host') to dynamically load code from other applications (the 'remotes' or micro-frontends) at runtime. This enables sharing components, modules, and services across independently built and deployed applications.
2. Setting Up the Environment
You'll typically start by creating a host application and one or more remote applications. The @angular-architects/module-federation package simplifies the setup process.
# Create a new Angular workspace (optional, can be existing)
ng new my-mf-workspace --create-application false
cd my-mf-workspace
# Create the Host application
ng generate application host --routing --style=scss
ng add @angular-architects/module-federation --project host --port 4200 --type host
# Create a Remote (Micro-Frontend) application
ng generate application mfe-app1 --routing --style=scss
ng add @angular-architects/module-federation --project mfe-app1 --port 4201 --type remote
3. Configuring the Remote Micro-Frontend
Each remote application needs to expose its components or modules that the host or other remotes might consume. This is configured in its module-federation.config.js file (or webpack.config.js if manually configured).
// projects/mfe-app1/module-federation.config.js
const { shareAll } = require('@angular-architects/module-federation/webpack');
module.exports = {
name: 'mfeApp1',
exposes: {
'./Module': './src/app/mfe1-entry/mfe1-entry.module.ts', // Expose an Angular Module
'./Component': './src/app/my-exposed-component/my-exposed-component.component.ts' // Expose a Component
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
};
4. Consuming the Remote in the Host Application
The host application needs to declare which remotes it will consume. This is configured in its module-federation.config.js.
// projects/host/module-federation.config.js
const { shareAll } = require('@angular-architects/module-federation/webpack');
module.exports = {
name: 'host',
remotes: {
'mfeApp1': 'http://localhost:4201/remoteEntry.js', // Name of remote and its entry point URL
// 'mfeApp2': 'http://localhost:4202/remoteEntry.js',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
};
Loading Remote Modules via Routing
The most common way to load a remote module is lazily through Angular's router.
// projects/host/src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
// ... other routes
{
path: 'mfe1-path',
loadChildren: () =>
import('mfeApp1/Module').then((m) => m.Mfe1EntryModule),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
Loading Remote Components Dynamically
You can also load remote components dynamically into a host component using techniques involving ViewContainerRef and dynamic imports.
5. Managing Shared Dependencies
A critical aspect is ensuring that host and remote applications share common dependencies (e.g., Angular itself, RxJS, Angular Material) to avoid duplicated code in bundles and ensure singleton instances. The @angular-architects/module-federation plugin uses shareAll to automate this based on your package.json.
// Example of shared configuration in module-federation.config.js
shared: {
'@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'@angular/common': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'@angular/router': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
// ... other Angular packages and commonly used libraries
'rxjs': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
}
6. Key Considerations for Micro-Frontends in Angular
- Communication: Establish clear communication channels between micro-frontends (e.g., shared services, custom event buses, RxJS subjects, or global state management like NgRx).
- State Management: Decide on a strategy for global state. Each MFE can manage its own local state, but cross-MFE state often requires a shared context or event-driven approach.
- Styling & Theming: Maintain a consistent look and feel using shared design systems, CSS variables, or a common UI library (e.g., Angular Material) loaded via the host or shared dependencies.
- Deployment: Each micro-frontend is deployed independently, typically to a static file server. The host application needs to know the correct URLs for the
remoteEntry.jsfiles, which can be handled via environment variables or a dynamic manifest. - Performance: Monitor bundle sizes and initial load times. Lazy loading remotes is crucial for performance. Optimize shared dependencies to minimize downloaded code.
- Error Handling: Implement robust error boundaries and fallback UIs for when a micro-frontend fails to load or encounters runtime issues.
- Routing Strategies: Decide whether the host handles all routing or if remotes manage their internal routes. Module Federation supports both deep linking into remotes.
- Version Control: Manage versions of exposed modules and shared dependencies carefully to avoid breaking changes between independently deployed micro-frontends.