Building Micro-frontends based on Angular

Benefits of Micro Frontend Architecture

Automation of the CI/CD workflow

Flexibility of teams

Single responsibility

Reusability

Technology agnosticism

Simple learning

Getting started

Header & Footer Module Federation

ng new layout
npm i - save-dev ngx-build-plus
const webpack = require("webpack");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
output: {
publicPath: "http://localhost:4201/",
uniqueName: "layout",
},
optimization: {
runtimeChunk: false,
},
plugins: [
new ModuleFederationPlugin({
name: "layout",
library: { type: "var", name: "layout" },
filename: "remoteEntry.js",
exposes: {
Header: "./src/app/modules/layout/header/header.component.ts",
Footer: "./src/app/modules/layout/footer/footer.component.ts",
},
shared: {
"@angular/core": { singleton: true, requiredVersion: "auto" },
"@angular/common": { singleton: true, requiredVersion: "auto" },
"@angular/router": { singleton: true, requiredVersion: "auto" },
},
}),
],
};
module.exports = require("./webpack.config");
...
"projects": {
"layout": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
},
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "ngx-build-plus:browser",
"options": {
"outputPath": "dist/layout",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": [],
"extraWebpackConfig": "webpack.config.js"
},
"configurations": {
"production": {
"budgets": [{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"extraWebpackConfig": "webpack.prod.config.js",
"fileReplacements": [{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "ngx-build-plus:dev-server",
"configurations": {
"production": {
"browserTarget": "layout:build:production"
},
"development": {
"browserTarget": "layout:build:development",
"extraWebpackConfig": "webpack.config.js",
"port": 4205
}
},
"defaultConfiguration": "development"
},
"test": {
"builder": "ngx-build-plus:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": [],
"extraWebpackConfig": "webpack.config.js"
}
}
}
}
},
"defaultProject": "layout"
}

Register Page Module Federation

ng new registration
npm i - save-dev ngx-build-plus
// webpack.config.js
const webpack = require("webpack");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
output: {
publicPath: "http://localhost:4202/",
uniqueName: "registration",
},
optimization: {
runtimeChunk: false,
},
plugins: [
new ModuleFederationPlugin({
name: "registration",
library: { type: "var", name: "registration" },
filename: "remoteEntry.js",
exposes: {
RegistrationModule:
"./src/app/modules/registration/registration.module.ts",
},
shared: {
"@angular/core": { singleton: true, requiredVersion: "auto" },
"@angular/common": { singleton: true, requiredVersion: "auto" },
"@angular/router": { singleton: true, requiredVersion: "auto" },
},
}),
],
};
module.exports = require("./webpack.config");

Dashboard Module Federation

// webpack.config.js
const webpack = require("webpack");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
output: {
publicPath: "http://localhost:4203/",
uniqueName: "dashboard",
},
optimization: {
runtimeChunk: false,
},
plugins: [
new ModuleFederationPlugin({
name: "dashboard",
library: { type: "var", name: "dashboard" },
filename: "remoteEntry.js",
exposes: {
DashboardModule: "./src/app/modules/dashboard/dashboard.module.ts",
},
shared: {
"@angular/core": { singleton: true, requiredVersion: "auto" },
"@angular/common": { singleton: true, requiredVersion: "auto" },
"@angular/router": { singleton: true, requiredVersion: "auto" },
},
}),
],
};

Shell App Module Federation

ng new shell
npm i --save-dev ngx-build-plus
const webpack = require("webpack");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
output: {
publicPath: "http://localhost:4200/",
uniqueName: "shell",
},
optimization: {
runtimeChunk: false,
},
plugins: [
new ModuleFederationPlugin({
shared: {
"@angular/core": { eager: true, singleton: true },
"@angular/common": { eager: true, singleton: true },
"@angular/router": { eager: true, singleton: true },
},
}),
],
};
export const environment = {
production: false,
microfrontends: {
dashboard: {
remoteEntry: "http://localhost:4203/remoteEntry.js",
remoteName: "dashboard",
exposedModule: ["DashboardModule"],
},
layout: {
remoteEntry: "http://localhost:4201/remoteEntry.js",
remoteName: "layout",
exposedModule: ["Header", "Footer"],
},
},
};
// src/app/utils/federation-utils.ts
type Scope = unknown;
type Factory = () => any;
interface Container {
init(shareScope: Scope): void;
get(module: string): Factory;
}
declare const __webpack_init_sharing__: (shareScope: string) => Promise<void>;
declare const __webpack_share_scopes__: { default: Scope };
const moduleMap: Record<string, boolean> = {};
function loadRemoteEntry(remoteEntry: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (moduleMap[remoteEntry]) {
return resolve();
}
const script = document.createElement("script");
script.src = remoteEntry;
script.onerror = reject;
script.onload = () => {
moduleMap[remoteEntry] = true;
resolve(); // window is the global namespace
};
document.body.append(script);
});
}
async function lookupExposedModule<T>(
remoteName: string,
exposedModule: string
): Promise<T> {
// Initializes the share scope. This fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__("default");
const container = window[remoteName] as Container;
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const factory = await container.get(exposedModule);
const Module = factory();
return Module as T;
}
export interface LoadRemoteModuleOptions {
remoteEntry: string;
remoteName: string;
exposedModule: string;
}
export async function loadRemoteModule<T = any>(
options: LoadRemoteModuleOptions
): Promise<T> {
await loadRemoteEntry(options.remoteEntry);
return lookupExposedModule<T>(options.remoteName, options.exposedModule);
}
// src/app/utils/route-utils.ts
import { Routes } from "@angular/router";
import { loadRemoteModule } from "./federation-utils";
import { APP_ROUTES } from "../app.routes";
import { Microfrontend } from "../core/services/microfrontends/microfrontend.types";
export function buildRoutes(options: Microfrontend[]): Routes {
const lazyRoutes: Routes = options.map((o) => ({
path: o.routePath,
loadChildren: () => loadRemoteModule(o).then((m) => m[o.ngModuleName]),
canActivate: o.canActivate,
pathMatch: "full",
}));
return [...APP_ROUTES, ...lazyRoutes];
}
// src/app/core/services/microfrontends/microfrontend.service.ts
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { MICROFRONTEND_ROUTES } from "src/app/app.routes";
import { buildRoutes } from "src/app/utils/route-utils";
@Injectable({ providedIn: "root" })
export class MicrofrontendService {
constructor(private router: Router) {}
/*
* Initialize is called on app startup to load the initial list of
* remote microfrontends and configure them within the router
*/
initialise(): Promise<void> {
return new Promise<void>((resolve) => {
this.router.resetConfig(buildRoutes(MICROFRONTEND_ROUTES));
return resolve();
});
}
}
// src/app/core/services/microfrontends/microfrontend.types.ts
import { LoadRemoteModuleOptions } from "src/app/utils/federation-utils";
export type Microfrontend = LoadRemoteModuleOptions & {
displayName: string;
routePath: string;
ngModuleName: string;
canActivate?: any[];
};
// src/app/app.routes.ts
import { Routes } from "@angular/router";
import { LoggedOnlyGuard } from "./core/guards/logged-only.guard";
import { UnloggedOnlyGuard } from "./core/guards/unlogged-only.guard";
import { Microfrontend } from "./core/services/microfrontends/microfrontend.types";
import { environment } from "src/environments/environment";
export const APP_ROUTES: Routes = [];
export const MICROFRONTEND_ROUTES: Microfrontend[] = [
{
...environment.microfrontends.dashboard,
exposedModule: environment.microfrontends.dashboard.exposedModule[0],
// For Routing, enabling us to ngFor over the microfrontends and dynamically create links for the routes
displayName: "Dashboard",
routePath: "",
ngModuleName: "DashboardModule",
canActivate: [LoggedOnlyGuard],
},
{
...environment.microfrontends.registration,
exposedModule: environment.microfrontends.registration.exposedModule[0],
displayName: "Register",
routePath: "signup",
ngModuleName: "RegistrationModule",
canActivate: [UnloggedOnlyGuard],
},
];
// src/app/app.module.ts
import { APP_INITIALIZER, NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { RouterModule } from "@angular/router";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { APP_ROUTES } from "./app.routes";
import { LoaderComponent } from "./core/components/loader/loader.component";
import { NavbarComponent } from "./core/components/navbar/navbar.component";
import { MicrofrontendService } from "./core/services/microfrontends/microfrontend.service";
export function initializeApp(
mfService: MicrofrontendService
): () => Promise<void> {
return () => mfService.initialise();
}
@NgModule({
declarations: [AppComponent, NavbarComponent, LoaderComponent],
imports: [
BrowserModule,
AppRoutingModule,
RouterModule.forRoot(APP_ROUTES, { relativeLinkResolution: "legacy" }),
],
providers: [
MicrofrontendService,
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
multi: true,
deps: [MicrofrontendService],
},
],
bootstrap: [AppComponent],
})
export class AppModule {}
// src/app/app.component.html
<main>
<header #header></header>
<div class="content">
<app-navbar [isLogged]="auth.isLogged"></app-navbar>
<div class="page-content">
<router-outlet *ngIf="!loadingRouteConfig else loading"></router-outlet>
<ng-template #loading>
<app-loader></app-loader>
</ng-template>
</div>
</div>
<footer #footer></footer>
</main>
import {
ViewContainerRef,
Component,
ComponentFactoryResolver,
OnInit,
AfterViewInit,
Injector,
ViewChild,
} from "@angular/core";
import {
RouteConfigLoadEnd,
RouteConfigLoadStart,
Router,
} from "@angular/router";
import { loadRemoteModule } from "./utils/federation-utils";
import { environment } from "src/environments/environment";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
})
export class AppComponent implements AfterViewInit, OnInit {
@ViewChild("header", { read: ViewContainerRef, static: true })
headerContainer!: ViewContainerRef;
@ViewChild("footer", { read: ViewContainerRef, static: true })
footerContainer!: ViewContainerRef;
loadingRouteConfig = false;
constructor(
private injector: Injector,
private resolver: ComponentFactoryResolver,
private router: Router
) {}
ngOnInit() {
this.router.events.subscribe((event) => {
if (event instanceof RouteConfigLoadStart) {
this.loadingRouteConfig = true;
} else if (event instanceof RouteConfigLoadEnd) {
this.loadingRouteConfig = false;
}
});
}
ngAfterViewInit(): void {
// load header
loadRemoteModule({
...environment.microfrontends.layout,
exposedModule: environment.microfrontends.layout.exposedModule[0],
}).then((module) => {
const factory = this.resolver.resolveComponentFactory(
module.HeaderComponent
);
this.headerContainer?.createComponent(factory, undefined, this.injector);
});
// load footer
loadRemoteModule({
...environment.microfrontends.layout,
exposedModule: environment.microfrontends.layout.exposedModule[1],
}).then((module) => {
const factory = this.resolver.resolveComponentFactory(
module.FooterComponent
);
this.footerContainer?.createComponent(factory, undefined, this.injector);
});
}
}

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store