An Easy-to-Follow Guide to Create Angular Microfrontends

Table contents:

Micro-frontend introduction:

  • Micro-frontend architecture is a design approach in which a front-end app is decomposed into individual, independent “applications” working loosely together.
  • Each microfrontend has an independent build process and an independent deployment. So this indicates a fast development cycle.

A monolithic front-end application is broken up into separate and independent MicroFrontEnds (MFEs)

Micro frontend frameworks/tools:

Currently, no UI framework supports micro-frontends out of the box, so we must use tools or frameworks to implement it.

In the below link you can find details about the many frameworks that support MFEs,

11 Micro Frontends Frameworks You Should Know

Each MFE ,

  • has its own build lifecycle
  • can deploy updates independently
  • have a small bundled size
  • can be lazy loaded
  • should have a minimal communication between other MFE’s
  • can be managed by an autonomous team

MFE Composition:

The integration of micro frontends can be handled in a number of ways,

Build-time composition: In a package.json file, you can specify micro frontend components as NPM dependencies, and write a simple application shell for routing and component composition at build time.

Server-side composition: UI fragments are composed on the server, which means that the client receives a fully assembled page, which speeds up loading.

Run-time composition: Runtime composition involves fetching resources from URLs that have been independently deployed. Micro frontends and their versions do not need to be explicitly hardcoded into project dependencies.

Runtime composition makes it easier to isolate releases and deployments.

Micro Frontends

Steps to build Angular MFE with Webpack5 and module federation:

Beginning with version 5, Webpack supports loading separately compiled program parts through Module Federation. The implementation of microfrontends is now finally official

Angular applications can be built using a custom builder provided by @angular-architects/module-federation.

Microfrontends are easier when you can apply a component-driven development approach to them. Bit makes this easier by providing an integrated dev environment (compiler, tester, linter, documentation, CI, dev server, and packaging/dependency management/bundler all-in-one) for building apps with Angular. It enables you to quickly set up a dev environment that follows best practices implemented by the Angular team and projects generated by the Angular CLI. Learn more here.

teambit / angular: A development environment for Angular components.

With multiple repos,

The demo project consists of a shell and a remote microfrontend stored in different git repositories.

Let’s start by creating the remote microfrontend application -mfe1,

# create mfe1 application
ng new mfe1
cd mfe1

# Add module federation lib
ng add @angular-architects/module-federation@14.3.10 --project mfe1 --port 4201 --type remote

# create new module
ng generate module remoteMfe

# create new component in mfe1 module
ng generate component remote-mfe/mfe-home --module remote-mfe
// mfe1\webpack.config.js
const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
name: 'mfe1',
exposes: { // List of modules that the application will export as remote to another application
'./RemoteMfeModule': './src/app/remote-mfe/remote-mfe.module.ts',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});
// mfe1\src\app\app-routing.module.ts
import { NgModule } from '@angular/core';
import { Route, RouterModule } from '@angular/router';
export const appRoutes: Route[] = [
{
path: '',
loadChildren: () =>
import('./remote-mfe/remote-mfe.module').then((m) => m.RemoteMfeModule),
},
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

------------------------------------------------------------------
// mfe1\src\app\remote-mfe\remote-mfe.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MfeHomeComponent } from './mfe-home/mfe-home.component';
import { Route, RouterModule } from '@angular/router';

export const remoteRoutes: Route[] = [
{ path: '', component: MfeHomeComponent }, // Add route
];
@NgModule({
declarations: [
MfeHomeComponent
],
imports: [
CommonModule,
RouterModule.forChild(remoteRoutes) // forChild
]
})
export class RemoteMfeModule { }
cd mfe1
npm run start

Let’s create shell appliction,

# create shell application
ng new shell
cd shell
ng add @angular-architects/module-federation@14.3.10 --project shell --port 4200 --type dynamic-host

This will create/update below files,

For the host application, the above plugin will generate mf.manifest.json in the assets folder, which contains the remote mfe information.

In production, we can replace mf.manifest.json file with k8s config map

Make mf.manifest.json more robust,

To make it more robust, update mf.manifest.json to include other details as well. It is possible to add or remove remote MFEs from the host application by modifying the mf.manifest.json file only

Create new model mf.model.ts

// shell\src\app\model\mf.model.ts
import { Manifest, RemoteConfig } from "@angular-architects/module-federation";

export type CustomRemoteConfig = RemoteConfig & {
isActive: boolean;
exposedModule: string;
displayName?: string;
routePath?: string;
ngModuleName?: string;
viaRoute?: boolean;
withInPage?: boolean;
componentName?: string;
};

export type CustomManifest = Manifest<CustomRemoteConfig>;
// shell\src\assets\mf.manifest.json

{
"mfe1": {
"isActive": true,
"remoteEntry": "http://localhost:4201/remoteEntry.js",
"exposedModule": "./RemoteMfeModule",
"displayName": "RemoteMfe",
"routePath": "mfe",
"ngModuleName": "RemoteMfeModule",
"viaRoute": true,
"withInPage": false
}
}

main.ts

The main.ts file should be updated with the following content, so that it only considers the remote mfe marked as isActive true in mf.manifest.json

// shell\src\main.ts

import { setManifest } from '@angular-architects/module-federation';
import { CustomManifest } from './app/model/mf.model';
const mfManifestJson = fetch('/assets/mf.manifest.json');
const parseConfig = async (mfManifest: Promise<Response>) => {
const manifest: CustomManifest = await (await mfManifest).json();
const filterManifest: CustomManifest = {};
for (const key of Object.keys(manifest)) {
const value = manifest[key];
// check more details
if (value.isActive === true) {
filterManifest[key] = value;
}
}
return filterManifest;
};
parseConfig(mfManifestJson)
.then((data) => setManifest(data))
.catch((err) => console.log(err))
.then((_) => import('./bootstrap'))
.catch((err) => console.log(err));

Bootstraping of angular application is moved from main.ts to bootstrap.ts (new file created by plugin)

setManifest method load’s all the required metadata to fetch the Micro Frontends.

Create new mfe utils file and copy the below contents,

// shell/src/app/mfe/mfe-dynamic.routes.ts

import { getManifest, loadRemoteModule } from "@angular-architects/module-federation";
import { Routes } from "@angular/router";
import { routes } from "../app-routing.module";
import { CustomManifest } from "../model/mf.model";

export function buildRoutes(): Routes {
const lazyRoutes = Object.entries(getManifest<CustomManifest>())
.filter(([key, value]) => {
return value.viaRoute === true
})
.map(([key, value]) => {
return {
path: value.routePath,
loadChildren: () => loadRemoteModule({
type: 'manifest',
remoteName: key,
exposedModule: value.exposedModule
}).then(m => m[value.ngModuleName!])
}
});
const notFound = [
{
path: '**',
redirectTo: ''
}]
// { path:'**', ...} needs to be the LAST one.
return [...routes, ...lazyRoutes, ...notFound]
}

Create new mfe service, and copy the below contents,

// shell/src/app/mfe/mfe-service.service.ts

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { buildRoutes } from './mfe-dynamic.routes';

@Injectable({
providedIn: 'root'
})
export class MfeServiceService {
constructor(private router: Router) { }
init () {
return new Promise<void>((resolve, reject) => {
const routes = buildRoutes();
this.router.resetConfig(routes);
resolve();
})
}
}
// shell/src/app/app.module.ts

import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LandingPageComponent } from './landing-page/landing-page.component';
import { HomeComponent } from './home/home.component';
import { MfeServiceService } from './mfe/mfe-service.service';
@NgModule({
declarations: [AppComponent, LandingPageComponent, HomeComponent],
imports: [BrowserModule, AppRoutingModule],
providers: [ // Add APP_INITIALIZER
{
provide: APP_INITIALIZER,
useFactory: (mfeService: MfeServiceService) => () =>
mfeService.init(),
deps: [MfeServiceService],
multi: true,
},
],
bootstrap: [AppComponent],
})
export class AppModule {}

Configure dynamic routes based on the mf.manifest json file,

Create a new component called landing-page component, and get the manifest file and load the routes based on the mf.manifest.json file,

ng generate component landing-page
ng generate component home
// shell/src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { LandingPageComponent } from './landing-page/landing-page.component';

export const routes: Routes = [
{
path: 'home',
component: HomeComponent
},
{
path: '',
redirectTo: 'LandingPageComponent',
pathMatch: 'full'
}
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
// shell\src\app\landing-page\landing-page.component.ts
import { getManifest } from '@angular-architects/module-federation';
import { Component, OnInit } from '@angular/core';
import { CustomManifest, CustomRemoteConfig } from '../model/mf.model';
@Component({
selector: 'app-landing-page',
templateUrl: './landing-page.component.html',
styleUrls: ['./landing-page.component.scss']
})
export class LandingPageComponent implements OnInit {
remotes: CustomRemoteConfig[] = [];
constructor() { }
ngOnInit(): void {
const manifest = getManifest<CustomManifest>();
this.remotes = Object.values(manifest).filter((v) => v.viaRoute === true);
}
}
<!-- shell\src\app\landing-page\landing-page.component.html -->

<!-- Links -->
<div>
<ul class="navbar-nav">
<!-- static links -->
<li class="nav-item">
<a class="nav-link" routerLink="/home" routerLinkActive="active">Home</a>
</li>
<!-- dynamic links from the mf.manifest.json -->
<li *ngFor="let remote of remotes">
<a
class="nav-link"
[routerLink]="remote.routePath"
routerLinkActive="active"
>{{ remote.displayName }}</a
>
</li>
</ul>
</div>
<router-outlet></router-outlet>

Run shell application, output should be something like below,

Now click on the remote mfe hyperlink, it will load the remote mfe module on demand (lazy load)

Remote mfe as fragment inside shell

When using @angular-architects/module-federation, you are only allowed to upload one micro-frontend per page (in Routing). It might be necessary to load multiple remote MFEs on a single HTML page at times.

create new component in mfe1 and update mfe1 webpack.conf.js

# In mfe1, create new component
ng generate component remote-mfe/mfe-fragment
# mfe1\webpack.config.js

const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
name: 'mfe1',
exposes: {
'./RemoteMfeModule': './src/app/remote-mfe/remote-mfe.module.ts',
'./MfeFragmentComponent': './src/app/remote-mfe/mfe-fragment/mfe-fragment.component.ts'
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});

Let’s add new entry in shell mf.manifest.json file

// shell\src\assets\mf.manifest.json
{
"mfe1": {
"isActive": true,
"remoteEntry": "http://localhost:4201/remoteEntry.js",
"exposedModule": "./RemoteMfeModule",
"displayName": "RemoteMfe",
"routePath": "mfe",
"ngModuleName": "RemoteMfeModule",
"viaRoute": true,
"withInPage": false
},
"mfe1-fragment": {
"isActive": true,
"remoteEntry": "http://localhost:4201/remoteEntry.js",
"exposedModule": "./MfeFragmentComponent",
"componentName": "MfeFragmentComponent",
"viaRoute": false,
"withInPage": true
}
}

Create new generic component to load multiple fragments in shell application,

// shell\src\app\model\mf.model.ts
.....
export type PluginOptions = LoadRemoteModuleOptions & {
displayName: string;
componentName: string;
};
// shell\src\app\mfe\load-fragments\load-fragments.component.ts
import { loadRemoteModule } from '@angular-architects/module-federation';
import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
OnInit,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { PluginOptions } from 'src/app/model/mf.model';
@Component({
selector: 'app-load-fragments',
template: ` <ng-container #placeHolder></ng-container> `,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LoadFragmentsComponent implements OnChanges {
@ViewChild('placeHolder', { read: ViewContainerRef, static: true })
viewContainer!: ViewContainerRef;
@Input() options!: PluginOptions;
async ngOnChanges() {
this.viewContainer.clear();
const component = await loadRemoteModule(this.options!).then(
(m) => m[this.options?.componentName]
);
this.viewContainer?.createComponent(component);
}
}
// shell\src\app\home\home.component.ts
import { getManifest } from '@angular-architects/module-federation';
import { Component, OnInit } from '@angular/core';
import { CustomManifest, PluginOptions } from '../model/mf.model';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
})
export class HomeComponent implements OnInit {
options: PluginOptions[] = [];
constructor() {}
ngOnInit(): void {
const manifest = getManifest<CustomManifest>();

// filter remote mfe's which needs to loaded inside page (not via route)
this.options = Object.values(manifest).filter(
(v) => v.withInPage === true
) as PluginOptions[];
}
}
<!-- shell\src\app\home\home.component.html -->

<div *ngFor="let p of options">
<div class="col-md-4">
<app-load-fragments [options]="p"></app-load-fragments>
</div>
</div>

Communication between two different microfrontend’s

There are multiple ways for handling communication between microfrontends, in this example we will use RxJS pub-sub,

Let’s create new angular lib called shared-lib and add dependency to both shell and mfe1

ng new ngx-mfe-lib --create-application=false   # Create new share-lib
cd ngx-mfe-lib
ng generate library share-lib
// ngx-mfe-lib\projects\share-lib\src\lib\share-lib.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable() // Remove providerIn:root
export class ShareLibService {
constructor() {}
private _name = new BehaviorSubject('');
readonly name$ = this._name.asObservable();

addName(name: string) {
this._name.next(name);
}
}
// ngx-mfe-lib\projects\share-lib\src\lib\share-lib.module.ts
import { ModuleWithProviders, NgModule } from '@angular/core';
import { ShareLibComponent } from './share-lib.component';
import { ShareLibService } from './share-lib.service';
@NgModule({
declarations: [ShareLibComponent],
imports: [],
exports: [ShareLibComponent],
})
export class ShareLibModule {
static forRoot(): ModuleWithProviders<ShareLibModule> {
return {
ngModule: ShareLibModule,
providers: [ShareLibService],
};
}
}
# In ngx-mfe-lib
ng build
# In shell application
npm link ..\ngx-mfe-lib\dist\share-lib # as we not published, we can link lib to application

# Update the shell\package.json
"dependencies": {
"@angular-architects/module-federation": "^14.3.10",
....
"share-lib": "0.0.1"
}


# In shell\src\app\app.module.ts, add the entry in imports,
@NgModule({
declarations: [ShellComponent],
imports: [
....
ShareLibModule.forRoot(), // New entry
....
]


# In shell\src\app\home\home.component.ts, call the service,
constructor(private readonly shareLib: ShareLibService) {}
sendData() {
this.shareLib.addName('test');
}

# In shell\src\app\home\home.component.html,
<button (click)="sendData()"> Send data to remote mfe</button>
# In mfe1 application
npm link ..\ngx-mfe-lib\dist\mfe-lib

# Update the mfe1\package.json
"dependencies": {
"@angular-architects/module-federation": "^14.3.10",
....
"share-lib": "0.0.1"
}
# In NgModule, no need to add anything
// In mfe1\src\app\remote-mfe\mfe-home\mfe-home.component.ts, call the service,
import { Component, OnInit, Optional } from '@angular/core';
import { Observable } from 'rxjs';
import { ShareLibService } from 'share-lib';
@Component({
selector: 'app-mfe-home',
templateUrl: './mfe-home.component.html',
styleUrls: ['./mfe-home.component.scss']
})
export class MfeHomeComponent implements OnInit {
name$:Observable<string> | undefined;
constructor(@Optional() private readonly shareLib?: ShareLibService) {
if (!shareLib) {
this.shareLib= new ShareLibService();
}
}
ngOnInit(): void {
this.name$ = this.shareLib?.name$; // subscribe to name$
}
}
// mfe1\src\app\remote-mfe\mfe-home\mfe-home.component.html
<p>mfe-home works!</p>
{{ name$ | async }}

IF we add providerIn:root to shared service, shell and remote mfe’s will initiate two instances of the services

Lazy loaded modules have their own root scope

Service is not being singleton for angular2 router lazy loading with loadChildren

Native Federation

Angular core team is working on esbuild in ng build to enable faster build times and simplify. As of now, module federeation is available only in webpack5.
In future, we have migrate all our module federation code to native federation

In short, Native Federation extends wepback Module Federation for the development of Micro Frontends and plugin-based applications in a browser-native way.

Native federation can be used with any framework and build tool

@softarc/native-federation

Using single repo

Nx.dev + ModuleFederation

Create shell and remote mfe’s in single repository,

Create nx workspace and install angular

  1. npx create-nx-workspace nx-mfe — preset=empty
  2. cd nx-mfe && npm install — save-dev @nrwl/angular

Create shell and mfe’s

  1. nx g @nrwl/angular:host shell — remotes=mfe1 — dynamic

Isolate microfrontend’s

  1. Nx provides linting rules. Once in place, they give us errors when we directly reference code belonging to another Micro Frontend and hence another business domain.
# .eslintrc.json
{
"sourceTag": "scope:shell",
"onlyDependOnLibsWithTags": ["scope:shell", "scope:auth-lib"]
}

In Nx.dev, we can implement all the features we discussed above. Here I have not shared anything, but you can try your own.

Mulitple repo’s

  • Pros: Independent build life cycle, no tight dependency on release cycle, small code base, easy to debug, autonomous team
  • Cons: Difficult to make sure all teams are following best practices, coordinating across multiple teams, version mismatch, dependency updates

Nx.dev + MFE

  • Most of the above cons are handled by Nx
  • Nx comes with the commands to rebuild and retest only the apps affected by a PR
  • MFE’s can be isolated from each other by using linting rules.

My vote is for Nx.dev

Deploy remote MFE’s bundle in MINIO Object store

  • Install MINIO
  • Start minio server
  • Create a bucket in minio
  • Build mfe1 application and upload the contents of dist folder to newly created bucket
  • Update the mf.manifest.json file to point minio object store
minio.exe server C:\minio

# Build mfe1 application
cd mfe1
ng build
// shell\src\assets\mf.manifest.json
{
"mfe1": {
"isActive": true,
"remoteEntry": "https://minio-localhost:9000/mfe1-mino-bucket/remoteEntry.js",
"exposedModule": "./RemoteMfeModule",
"displayName": "RemoteMfe",
"routePath": "mfe",
"ngModuleName": "RemoteMfeModule",
"viaRoute": true,
"withInPage": false
},
"mfe1-fragment": {
"isActive": true,
"remoteEntry": "https://minio-localhost:9000/mfe1-mino-bucket/remoteEntry.js",
"exposedModule": "./MfeFragmentComponent",
"componentName": "MfeFragmentComponent",
"viaRoute": false,
"withInPage": true
}
}

Build Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

Learn more:


An Easy-to-Follow Guide to Create Angular Microfrontends was originally published in Bits and Pieces on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Bits and Pieces - Medium and was authored by Suhas

Table contents:

Micro-frontend introduction:

  • Micro-frontend architecture is a design approach in which a front-end app is decomposed into individual, independent “applications” working loosely together.
  • Each microfrontend has an independent build process and an independent deployment. So this indicates a fast development cycle.
A monolithic front-end application is broken up into separate and independent MicroFrontEnds (MFEs)

Micro frontend frameworks/tools:

Currently, no UI framework supports micro-frontends out of the box, so we must use tools or frameworks to implement it.

In the below link you can find details about the many frameworks that support MFEs,

11 Micro Frontends Frameworks You Should Know

Each MFE ,

  • has its own build lifecycle
  • can deploy updates independently
  • have a small bundled size
  • can be lazy loaded
  • should have a minimal communication between other MFE’s
  • can be managed by an autonomous team

MFE Composition:

The integration of micro frontends can be handled in a number of ways,

Build-time composition: In a package.json file, you can specify micro frontend components as NPM dependencies, and write a simple application shell for routing and component composition at build time.

Server-side composition: UI fragments are composed on the server, which means that the client receives a fully assembled page, which speeds up loading.

Run-time composition: Runtime composition involves fetching resources from URLs that have been independently deployed. Micro frontends and their versions do not need to be explicitly hardcoded into project dependencies.

Runtime composition makes it easier to isolate releases and deployments.

Micro Frontends

Steps to build Angular MFE with Webpack5 and module federation:

Beginning with version 5, Webpack supports loading separately compiled program parts through Module Federation. The implementation of microfrontends is now finally official

Angular applications can be built using a custom builder provided by @angular-architects/module-federation.

Microfrontends are easier when you can apply a component-driven development approach to them. Bit makes this easier by providing an integrated dev environment (compiler, tester, linter, documentation, CI, dev server, and packaging/dependency management/bundler all-in-one) for building apps with Angular. It enables you to quickly set up a dev environment that follows best practices implemented by the Angular team and projects generated by the Angular CLI. Learn more here.

teambit / angular: A development environment for Angular components.

With multiple repos,

The demo project consists of a shell and a remote microfrontend stored in different git repositories.

Let’s start by creating the remote microfrontend application -mfe1,

# create mfe1 application
ng new mfe1
cd mfe1

# Add module federation lib
ng add @angular-architects/module-federation@14.3.10 --project mfe1 --port 4201 --type remote

# create new module
ng generate module remoteMfe

# create new component in mfe1 module
ng generate component remote-mfe/mfe-home --module remote-mfe
// mfe1\webpack.config.js
const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
name: 'mfe1',
exposes: { // List of modules that the application will export as remote to another application
'./RemoteMfeModule': './src/app/remote-mfe/remote-mfe.module.ts',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});
// mfe1\src\app\app-routing.module.ts
import { NgModule } from '@angular/core';
import { Route, RouterModule } from '@angular/router';
export const appRoutes: Route[] = [
{
path: '',
loadChildren: () =>
import('./remote-mfe/remote-mfe.module').then((m) => m.RemoteMfeModule),
},
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

------------------------------------------------------------------
// mfe1\src\app\remote-mfe\remote-mfe.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MfeHomeComponent } from './mfe-home/mfe-home.component';
import { Route, RouterModule } from '@angular/router';

export const remoteRoutes: Route[] = [
{ path: '', component: MfeHomeComponent }, // Add route
];
@NgModule({
declarations: [
MfeHomeComponent
],
imports: [
CommonModule,
RouterModule.forChild(remoteRoutes) // forChild
]
})
export class RemoteMfeModule { }
cd mfe1
npm run start

Let’s create shell appliction,

# create shell application
ng new shell
cd shell
ng add @angular-architects/module-federation@14.3.10 --project shell --port 4200 --type dynamic-host

This will create/update below files,

For the host application, the above plugin will generate mf.manifest.json in the assets folder, which contains the remote mfe information.

In production, we can replace mf.manifest.json file with k8s config map

Make mf.manifest.json more robust,

To make it more robust, update mf.manifest.json to include other details as well. It is possible to add or remove remote MFEs from the host application by modifying the mf.manifest.json file only

Create new model mf.model.ts

// shell\src\app\model\mf.model.ts
import { Manifest, RemoteConfig } from "@angular-architects/module-federation";

export type CustomRemoteConfig = RemoteConfig & {
isActive: boolean;
exposedModule: string;
displayName?: string;
routePath?: string;
ngModuleName?: string;
viaRoute?: boolean;
withInPage?: boolean;
componentName?: string;
};

export type CustomManifest = Manifest<CustomRemoteConfig>;
// shell\src\assets\mf.manifest.json

{
"mfe1": {
"isActive": true,
"remoteEntry": "http://localhost:4201/remoteEntry.js",
"exposedModule": "./RemoteMfeModule",
"displayName": "RemoteMfe",
"routePath": "mfe",
"ngModuleName": "RemoteMfeModule",
"viaRoute": true,
"withInPage": false
}
}

main.ts

The main.ts file should be updated with the following content, so that it only considers the remote mfe marked as isActive true in mf.manifest.json

// shell\src\main.ts

import { setManifest } from '@angular-architects/module-federation';
import { CustomManifest } from './app/model/mf.model';
const mfManifestJson = fetch('/assets/mf.manifest.json');
const parseConfig = async (mfManifest: Promise<Response>) => {
const manifest: CustomManifest = await (await mfManifest).json();
const filterManifest: CustomManifest = {};
for (const key of Object.keys(manifest)) {
const value = manifest[key];
// check more details
if (value.isActive === true) {
filterManifest[key] = value;
}
}
return filterManifest;
};
parseConfig(mfManifestJson)
.then((data) => setManifest(data))
.catch((err) => console.log(err))
.then((_) => import('./bootstrap'))
.catch((err) => console.log(err));

Bootstraping of angular application is moved from main.ts to bootstrap.ts (new file created by plugin)

setManifest method load’s all the required metadata to fetch the Micro Frontends.

Create new mfe utils file and copy the below contents,

// shell/src/app/mfe/mfe-dynamic.routes.ts

import { getManifest, loadRemoteModule } from "@angular-architects/module-federation";
import { Routes } from "@angular/router";
import { routes } from "../app-routing.module";
import { CustomManifest } from "../model/mf.model";

export function buildRoutes(): Routes {
const lazyRoutes = Object.entries(getManifest<CustomManifest>())
.filter(([key, value]) => {
return value.viaRoute === true
})
.map(([key, value]) => {
return {
path: value.routePath,
loadChildren: () => loadRemoteModule({
type: 'manifest',
remoteName: key,
exposedModule: value.exposedModule
}).then(m => m[value.ngModuleName!])
}
});
const notFound = [
{
path: '**',
redirectTo: ''
}]
// { path:'**', ...} needs to be the LAST one.
return [...routes, ...lazyRoutes, ...notFound]
}

Create new mfe service, and copy the below contents,

// shell/src/app/mfe/mfe-service.service.ts

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { buildRoutes } from './mfe-dynamic.routes';

@Injectable({
providedIn: 'root'
})
export class MfeServiceService {
constructor(private router: Router) { }
init () {
return new Promise<void>((resolve, reject) => {
const routes = buildRoutes();
this.router.resetConfig(routes);
resolve();
})
}
}
// shell/src/app/app.module.ts

import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LandingPageComponent } from './landing-page/landing-page.component';
import { HomeComponent } from './home/home.component';
import { MfeServiceService } from './mfe/mfe-service.service';
@NgModule({
declarations: [AppComponent, LandingPageComponent, HomeComponent],
imports: [BrowserModule, AppRoutingModule],
providers: [ // Add APP_INITIALIZER
{
provide: APP_INITIALIZER,
useFactory: (mfeService: MfeServiceService) => () =>
mfeService.init(),
deps: [MfeServiceService],
multi: true,
},
],
bootstrap: [AppComponent],
})
export class AppModule {}

Configure dynamic routes based on the mf.manifest json file,

Create a new component called landing-page component, and get the manifest file and load the routes based on the mf.manifest.json file,

ng generate component landing-page
ng generate component home
// shell/src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { LandingPageComponent } from './landing-page/landing-page.component';

export const routes: Routes = [
{
path: 'home',
component: HomeComponent
},
{
path: '',
redirectTo: 'LandingPageComponent',
pathMatch: 'full'
}
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
// shell\src\app\landing-page\landing-page.component.ts
import { getManifest } from '@angular-architects/module-federation';
import { Component, OnInit } from '@angular/core';
import { CustomManifest, CustomRemoteConfig } from '../model/mf.model';
@Component({
selector: 'app-landing-page',
templateUrl: './landing-page.component.html',
styleUrls: ['./landing-page.component.scss']
})
export class LandingPageComponent implements OnInit {
remotes: CustomRemoteConfig[] = [];
constructor() { }
ngOnInit(): void {
const manifest = getManifest<CustomManifest>();
this.remotes = Object.values(manifest).filter((v) => v.viaRoute === true);
}
}
<!-- shell\src\app\landing-page\landing-page.component.html -->

<!-- Links -->
<div>
<ul class="navbar-nav">
<!-- static links -->
<li class="nav-item">
<a class="nav-link" routerLink="/home" routerLinkActive="active">Home</a>
</li>
<!-- dynamic links from the mf.manifest.json -->
<li *ngFor="let remote of remotes">
<a
class="nav-link"
[routerLink]="remote.routePath"
routerLinkActive="active"
>{{ remote.displayName }}</a
>
</li>
</ul>
</div>
<router-outlet></router-outlet>

Run shell application, output should be something like below,

Now click on the remote mfe hyperlink, it will load the remote mfe module on demand (lazy load)

Remote mfe as fragment inside shell

When using @angular-architects/module-federation, you are only allowed to upload one micro-frontend per page (in Routing). It might be necessary to load multiple remote MFEs on a single HTML page at times.

create new component in mfe1 and update mfe1 webpack.conf.js

# In mfe1, create new component
ng generate component remote-mfe/mfe-fragment
# mfe1\webpack.config.js

const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
name: 'mfe1',
exposes: {
'./RemoteMfeModule': './src/app/remote-mfe/remote-mfe.module.ts',
'./MfeFragmentComponent': './src/app/remote-mfe/mfe-fragment/mfe-fragment.component.ts'
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});

Let’s add new entry in shell mf.manifest.json file

// shell\src\assets\mf.manifest.json
{
"mfe1": {
"isActive": true,
"remoteEntry": "http://localhost:4201/remoteEntry.js",
"exposedModule": "./RemoteMfeModule",
"displayName": "RemoteMfe",
"routePath": "mfe",
"ngModuleName": "RemoteMfeModule",
"viaRoute": true,
"withInPage": false
},
"mfe1-fragment": {
"isActive": true,
"remoteEntry": "http://localhost:4201/remoteEntry.js",
"exposedModule": "./MfeFragmentComponent",
"componentName": "MfeFragmentComponent",
"viaRoute": false,
"withInPage": true
}
}

Create new generic component to load multiple fragments in shell application,

// shell\src\app\model\mf.model.ts
.....
export type PluginOptions = LoadRemoteModuleOptions & {
displayName: string;
componentName: string;
};
// shell\src\app\mfe\load-fragments\load-fragments.component.ts
import { loadRemoteModule } from '@angular-architects/module-federation';
import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
OnInit,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { PluginOptions } from 'src/app/model/mf.model';
@Component({
selector: 'app-load-fragments',
template: ` <ng-container #placeHolder></ng-container> `,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LoadFragmentsComponent implements OnChanges {
@ViewChild('placeHolder', { read: ViewContainerRef, static: true })
viewContainer!: ViewContainerRef;
@Input() options!: PluginOptions;
async ngOnChanges() {
this.viewContainer.clear();
const component = await loadRemoteModule(this.options!).then(
(m) => m[this.options?.componentName]
);
this.viewContainer?.createComponent(component);
}
}
// shell\src\app\home\home.component.ts
import { getManifest } from '@angular-architects/module-federation';
import { Component, OnInit } from '@angular/core';
import { CustomManifest, PluginOptions } from '../model/mf.model';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
})
export class HomeComponent implements OnInit {
options: PluginOptions[] = [];
constructor() {}
ngOnInit(): void {
const manifest = getManifest<CustomManifest>();

// filter remote mfe's which needs to loaded inside page (not via route)
this.options = Object.values(manifest).filter(
(v) => v.withInPage === true
) as PluginOptions[];
}
}
<!-- shell\src\app\home\home.component.html -->

<div *ngFor="let p of options">
<div class="col-md-4">
<app-load-fragments [options]="p"></app-load-fragments>
</div>
</div>

Communication between two different microfrontend’s

There are multiple ways for handling communication between microfrontends, in this example we will use RxJS pub-sub,

Let’s create new angular lib called shared-lib and add dependency to both shell and mfe1

ng new ngx-mfe-lib --create-application=false   # Create new share-lib
cd ngx-mfe-lib
ng generate library share-lib
// ngx-mfe-lib\projects\share-lib\src\lib\share-lib.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable() // Remove providerIn:root
export class ShareLibService {
constructor() {}
private _name = new BehaviorSubject('');
readonly name$ = this._name.asObservable();

addName(name: string) {
this._name.next(name);
}
}
// ngx-mfe-lib\projects\share-lib\src\lib\share-lib.module.ts
import { ModuleWithProviders, NgModule } from '@angular/core';
import { ShareLibComponent } from './share-lib.component';
import { ShareLibService } from './share-lib.service';
@NgModule({
declarations: [ShareLibComponent],
imports: [],
exports: [ShareLibComponent],
})
export class ShareLibModule {
static forRoot(): ModuleWithProviders<ShareLibModule> {
return {
ngModule: ShareLibModule,
providers: [ShareLibService],
};
}
}
# In ngx-mfe-lib
ng build
# In shell application
npm link ..\ngx-mfe-lib\dist\share-lib # as we not published, we can link lib to application

# Update the shell\package.json
"dependencies": {
"@angular-architects/module-federation": "^14.3.10",
....
"share-lib": "0.0.1"
}


# In shell\src\app\app.module.ts, add the entry in imports,
@NgModule({
declarations: [ShellComponent],
imports: [
....
ShareLibModule.forRoot(), // New entry
....
]


# In shell\src\app\home\home.component.ts, call the service,
constructor(private readonly shareLib: ShareLibService) {}
sendData() {
this.shareLib.addName('test');
}

# In shell\src\app\home\home.component.html,
<button (click)="sendData()"> Send data to remote mfe</button>
# In mfe1 application
npm link ..\ngx-mfe-lib\dist\mfe-lib

# Update the mfe1\package.json
"dependencies": {
"@angular-architects/module-federation": "^14.3.10",
....
"share-lib": "0.0.1"
}
# In NgModule, no need to add anything
// In mfe1\src\app\remote-mfe\mfe-home\mfe-home.component.ts, call the service,
import { Component, OnInit, Optional } from '@angular/core';
import { Observable } from 'rxjs';
import { ShareLibService } from 'share-lib';
@Component({
selector: 'app-mfe-home',
templateUrl: './mfe-home.component.html',
styleUrls: ['./mfe-home.component.scss']
})
export class MfeHomeComponent implements OnInit {
name$:Observable<string> | undefined;
constructor(@Optional() private readonly shareLib?: ShareLibService) {
if (!shareLib) {
this.shareLib= new ShareLibService();
}
}
ngOnInit(): void {
this.name$ = this.shareLib?.name$; // subscribe to name$
}
}
// mfe1\src\app\remote-mfe\mfe-home\mfe-home.component.html
<p>mfe-home works!</p>
{{ name$ | async }}

IF we add providerIn:root to shared service, shell and remote mfe’s will initiate two instances of the services

Lazy loaded modules have their own root scope

Service is not being singleton for angular2 router lazy loading with loadChildren

Native Federation

Angular core team is working on esbuild in ng build to enable faster build times and simplify. As of now, module federeation is available only in webpack5.
In future, we have migrate all our module federation code to native federation

In short, Native Federation extends wepback Module Federation for the development of Micro Frontends and plugin-based applications in a browser-native way.

Native federation can be used with any framework and build tool

@softarc/native-federation

Using single repo

Nx.dev + ModuleFederation

Create shell and remote mfe’s in single repository,

Create nx workspace and install angular

  1. npx create-nx-workspace nx-mfe — preset=empty
  2. cd nx-mfe && npm install — save-dev @nrwl/angular

Create shell and mfe’s

  1. nx g @nrwl/angular:host shell — remotes=mfe1 — dynamic

Isolate microfrontend’s

  1. Nx provides linting rules. Once in place, they give us errors when we directly reference code belonging to another Micro Frontend and hence another business domain.
# .eslintrc.json
{
"sourceTag": "scope:shell",
"onlyDependOnLibsWithTags": ["scope:shell", "scope:auth-lib"]
}

In Nx.dev, we can implement all the features we discussed above. Here I have not shared anything, but you can try your own.

Mulitple repo’s

  • Pros: Independent build life cycle, no tight dependency on release cycle, small code base, easy to debug, autonomous team
  • Cons: Difficult to make sure all teams are following best practices, coordinating across multiple teams, version mismatch, dependency updates

Nx.dev + MFE

  • Most of the above cons are handled by Nx
  • Nx comes with the commands to rebuild and retest only the apps affected by a PR
  • MFE’s can be isolated from each other by using linting rules.
My vote is for Nx.dev

Deploy remote MFE’s bundle in MINIO Object store

  • Install MINIO
  • Start minio server
  • Create a bucket in minio
  • Build mfe1 application and upload the contents of dist folder to newly created bucket
  • Update the mf.manifest.json file to point minio object store
minio.exe server C:\minio

# Build mfe1 application
cd mfe1
ng build
// shell\src\assets\mf.manifest.json
{
"mfe1": {
"isActive": true,
"remoteEntry": "https://minio-localhost:9000/mfe1-mino-bucket/remoteEntry.js",
"exposedModule": "./RemoteMfeModule",
"displayName": "RemoteMfe",
"routePath": "mfe",
"ngModuleName": "RemoteMfeModule",
"viaRoute": true,
"withInPage": false
},
"mfe1-fragment": {
"isActive": true,
"remoteEntry": "https://minio-localhost:9000/mfe1-mino-bucket/remoteEntry.js",
"exposedModule": "./MfeFragmentComponent",
"componentName": "MfeFragmentComponent",
"viaRoute": false,
"withInPage": true
}
}

Build Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

Learn more:


An Easy-to-Follow Guide to Create Angular Microfrontends was originally published in Bits and Pieces on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Bits and Pieces - Medium and was authored by Suhas


Print Share Comment Cite Upload Translate Updates
APA

Suhas | Sciencx (2023-02-21T11:54:05+00:00) An Easy-to-Follow Guide to Create Angular Microfrontends. Retrieved from https://www.scien.cx/2023/02/21/an-easy-to-follow-guide-to-create-angular-microfrontends/

MLA
" » An Easy-to-Follow Guide to Create Angular Microfrontends." Suhas | Sciencx - Tuesday February 21, 2023, https://www.scien.cx/2023/02/21/an-easy-to-follow-guide-to-create-angular-microfrontends/
HARVARD
Suhas | Sciencx Tuesday February 21, 2023 » An Easy-to-Follow Guide to Create Angular Microfrontends., viewed ,<https://www.scien.cx/2023/02/21/an-easy-to-follow-guide-to-create-angular-microfrontends/>
VANCOUVER
Suhas | Sciencx - » An Easy-to-Follow Guide to Create Angular Microfrontends. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2023/02/21/an-easy-to-follow-guide-to-create-angular-microfrontends/
CHICAGO
" » An Easy-to-Follow Guide to Create Angular Microfrontends." Suhas | Sciencx - Accessed . https://www.scien.cx/2023/02/21/an-easy-to-follow-guide-to-create-angular-microfrontends/
IEEE
" » An Easy-to-Follow Guide to Create Angular Microfrontends." Suhas | Sciencx [Online]. Available: https://www.scien.cx/2023/02/21/an-easy-to-follow-guide-to-create-angular-microfrontends/. [Accessed: ]
rf:citation
» An Easy-to-Follow Guide to Create Angular Microfrontends | Suhas | Sciencx | https://www.scien.cx/2023/02/21/an-easy-to-follow-guide-to-create-angular-microfrontends/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.