Micro Frontends using Module Federation and Angular
Micro-frontends in Angular using Module Federation with Nx Workspace
What is Micro Frontend?
A micro frontend is an architectural design pattern to build robust, highly scalable, distributed applications. Where a monolithic application is broken down into smaller pieces or subdomains and developed and deployed independently.
Do we need it?
The answer is it depends, like when we have a product suite or dashboard so in this kind of application there are lots of things that are domains of their own in such scenarios, we can divide an application into its subdomains and assign a team developing that domain so that can be developed and maintained by the team independently of any other dependencies.
How to implement micro frontend using Angular with module federation
Final Project on GitHub
What we will build
- Dashboard App ( Host )
- Auth App ( Remote )
- Todo App ( Remote )
- Authentication Library ( Shared Library )
- Todo Library ( Shared Library )
Create Nx Workspace
we will be using nx workspace to manage our codebase as it provides an intuitive experience to develop micro frontend applications.
npx create-nx-workspace mfe-demo
Add Angular Plugin
To add Angular-related features to our newly created monorepo we need to install the Angular Plugin.
Note: you are inside the mfe-demo directory i.e root directory of the nx workspace
npm i -D @nrwl/angular
Generate Application
We need to generate two applications. We also need to tell Nx that we want these applications to support Module Federation.
Dashboard Application (Host)
npx nx g @nrwl/angular:app dashboard --mfe --mfeType=host --port=4200 --routing=true --style=scss --inlineTemplate=true --inlineStyle=true
Auth Application (Remote)
npx nx g @nrwl/angular:app auth --mfe --mfeType=remote --host=dashboard --port=4201 --routing=true --style=scss --inlineTemplate=true --inlineStyle=true
Todos Application (Remote)
npx nx g @nrwl/angular:app todos --mfe --mfeType=remote --host=dashboard --port=4202 --routing=true --style=scss --inlineTemplate=true --inlineStyle=true
Note: We provided remote as the --mfeType. This tells the generator to create a Webpack configuration file that is ready to be consumed by a Host application.
Note: We provided 4201 as the --port. This helps when developing locally as it will tell the serve target to serve on a different port reducing the chance of multiple remote apps trying to run on the same port.
Note: We provided --host=dashboard as an option. This tells the generator that this remote app will be consumed by the Dashboard application. The generator will automatically link these two apps together in the webpack.config.js
Note: The RemoteEntryModule generated will be imported in the app.module.ts
file, however, it is not used in the AppModule itself. This is to allow TS to find the Module during compilation, allowing it to be included in the built bundle. This is required for the Module Federation Plugin to expose the Module correctly. You can choose to import the RemoteEntryModule in the AppModule if you wish, however, it is not necessary.
Key Difference between Host and Remote config files.
Dashboard webpack.config.js
file
plugins: [
new ModuleFederationPlugin({
remotes: {
auth: 'http://localhost:4201/authRemoteEntry.js',
todos: 'http://localhost:4202/todosRemoteEntry.js',
},
shared: {
'@angular/core': { singleton: true, strictVersion: true },
'@angular/common': { singleton: true, strictVersion: true },
'@angular/common/http': { singleton: true, strictVersion: true },
'@angular/router': { singleton: true, strictVersion: true },
...sharedMappings.getDescriptors(),
},
library: {
type: 'module',
},
}),
sharedMappings.getPlugin(),
],
Auth webpack.config.js
file
plugins: [
new ModuleFederationPlugin({
name: 'auth',
filename: 'authRemoteEntry.js',
exposes: {
'./RemoteEntryModule': 'apps/auth/src/app/remote-entry/entry.module.ts',
},
shared: {
'@angular/core': { singleton: true, strictVersion: true },
'@angular/common': { singleton: true, strictVersion: true },
'@angular/common/http': { singleton: true, strictVersion: true },
'@angular/router': { singleton: true, strictVersion: true },
...sharedMappings.getDescriptors(),
},
library: {
type: 'module',
},
}),
sharedMappings.getPlugin(),
],
Todos webpack.config.js
file
plugins: [
new ModuleFederationPlugin({
name: 'todos',
filename: 'todosRemoteEntry.js',
exposes: {
'./RemoteEntryModule':
'apps/todos/src/app/remote-entry/entry.module.ts',
},
shared: {
'@angular/core': { singleton: true, strictVersion: true },
'@angular/common': { singleton: true, strictVersion: true },
'@angular/common/http': { singleton: true, strictVersion: true },
'@angular/router': { singleton: true, strictVersion: true },
...sharedMappings.getDescriptors(),
},
library: {
type: 'module',
},
}),
sharedMappings.getPlugin(),
],
Adding Functionality
We'll start by building the Auth app, which will consist of a login form and some very basic and insecure authorization logic.
Authentication Library
Let's create a user authentication library that we will share between the host application and the remote application. This will be used to determine if there is an authenticated user as well as provide logic for authenticating the user.
nx g @nrwl/angular:lib shared/authentication
Also we need an Angular Service that we will use to hold state:
nx g @nrwl/angular:service auth --project=shared-authentication
This will create a file auth.service.ts under the shared/authentication library. Change it's contents to match:
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { v4 } from "uuid";
// Interfaces
export interface AuthUser {
uid: string;
name: string;
email: string;
}
@Injectable({
providedIn: 'root',
})
export class AuthService {
public authUser = new BehaviorSubject<AuthUser | null>(null);
public currentUser(): Observable<AuthUser | null> {
return this.authUser;
}
public async signIn(credential: {
email: string;
password: string;
}): Promise<void> {
if (credential.email === 'admin' && credential.password === 'admin') {
this.authUser.next({
uid: v4(),
email: credential.email,
name: 'Admin',
});
}
}
public async signUp(userData: {
email: string;
password: string;
name: string;
}): Promise<void> {
this.authUser.next({
uid: v4(),
email: userData.email,
name: userData.name,
});
}
public async signOut(): Promise<void> {
this.authUser.next(null);
}
}
Add a new export to the shared/authentication index.ts
file:
export * from './lib/auth.service';
We are done with the Authentication library, Now we will perform a similar procedure for the todos library.
Todo Library
This library will be responsible for storing todos for currently logged-in user and todo CRUD operations.
nx g @nrwl/angular:lib shared/todo
Also, we need an Angular Service that we will use to hold state:
nx g @nrwl/angular:service todo --project=shared-todo
This will create a file todo.service.ts under the shared/todo library. Change its contents to match:
import { Injectable } from '@angular/core';
import { AuthUser } from '@mfe-demo-prototype/shared/authentication';
import { BehaviorSubject, Observable } from 'rxjs';
import { v4 } from 'uuid';
// Interfaces
export interface Todo {
id: string;
body: string;
done: boolean;
}
@Injectable({
providedIn: 'root',
})
export class UserTodoService {
private currentUserId: string | undefined;
public currentUser!: AuthUser;
private todos: Todo[] = [];
public todos$ = new BehaviorSubject<Todo[]>([]);
// Set current User
public setCurrentUser(data: { uid: string; name: string; email: string }) {
this.currentUser = data;
}
// set User ID
public setUserId(userUid: string) {
this.currentUserId = userUid;
}
// get Todos
public getTodos(): Observable<Todo[]> {
return this.todos$;
}
// Create Todo
public async createTodo(body: string): Promise<void> {
const newTodo: Todo = {
id: v4(),
body,
done: false,
};
this.todos.push(newTodo);
this.todos$.next(this.todos);
}
// update Todo
public async updateTodo(todo: Todo): Promise<void> {
this.todos = this.todos.map((t) => {
if (t.id === todo.id) {
const updatedTodo: Todo = {
...t,
done: !todo.done,
};
return updatedTodo;
}
return t;
});
this.todos$.next(this.todos);
}
// delete Todo
public async deleteTodo(todo: Todo): Promise<void> {
this.todos = this.todos.filter((t) => t.id !== todo.id);
this.todos$.next(this.todos);
}
// Clear All Todos When Logged Out
public async clearTodos(): Promise<void> {
this.todos = [];
this.todos$.next(this.todos);
}
}
Add a new export to the shared/todo index.ts
file:
export * from './lib/todo.service';
With that we are done with all the library we need for this project.
Now on Auth App
We will be setting up thelogin route
and signup route
for the user to log in as well as sign up and also the home route
for the application.
Create those components
Login Component
nx g @nrwl/angular:component remote-entry/components/login --project auth --module=entry --inlineTemplate=true --inlineStyle=true
Now open the login.component.ts
file and change its content to match:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { AuthService } from '@mfe-demo-prototype/shared/authentication';
@Component({
selector: 'mfe-demo-prototype-login',
template: `
<div class="flex items-center justify-center h-screen bg-gray-200">
<div
class="flex flex-col items-center justify-start bg-gray-300 rounded-xl shadow-2xl p-4 w-64"
>
<div class="flex items-center justify-center shadow-md rounded-full">
<!-- Image -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-20 w-20 text-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<form
[formGroup]="loginForm"
(submit)="submit()"
class="flex flex-col items-start justify-center space-y-4 w-full"
>
<div class="flex flex-col items-start justify-start w-full">
<label for="email" class="text-xs font-semibold my-1"
>User Name</label
>
<input
type="email"
name="email"
class="rounded focus:outline-none p-1 w-full"
formControlName="email"
/>
</div>
<div class="flex flex-col items-start justify-start w-full">
<label for="passowrd" class="text-xs font-semibold my-1">
Password
</label>
<input
type="password"
name="password"
class="rounded focus:outline-none p-1 w-full"
formControlName="password"
/>
</div>
<button
type="submit"
class="p-1 bg-green-600 rounded-lg w-full font-bold text-gray-50"
>
Login
</button>
</form>
</div>
</div>
`,
styles: [],
})
export class LoginComponent implements OnInit {
public loginForm!: FormGroup;
constructor(private fb: FormBuilder, private authService: AuthService) {}
ngOnInit(): void {
this.loginForm = this.fb.group({
email: '',
password: '',
});
this.loginForm.valueChanges.subscribe(console.log);
}
async submit(): Promise<void> {
if (this.loginForm.valid) {
const email = this.loginForm.get('email')?.value;
const password = this.loginForm.get('password')?.value;
await this.authService.signIn({ email, password });
}
}
}
Signup Component
nx g @nrwl/angular:component remote-entry/components/signup --project auth --module=entry --inlineTemplate=true --inlineStyle=true
Now open the signup.component.ts
file and match the content.
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { AuthService } from '@mfe-demo-prototype/shared/authentication';
@Component({
selector: 'mfe-demo-prototype-signup',
template: `
<div class="flex items-center justify-center h-screen bg-gray-200">
<div
class="flex flex-col items-center justify-start bg-gray-200 rounded-xl shadow-2xl p-4 w-64"
>
<div>
<!-- Image -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-20 w-20 text-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<form
[formGroup]="signUpForm"
(submit)="submit()"
class="flex flex-col items-start justify-center space-y-4 w-full"
>
<div class="flex flex-col items-start justify-start w-full">
<label for="name" class="text-xs font-semibold my-1">Name</label>
<input
type="text"
name="name"
class="rounded focus:outline-none p-1 w-full"
formControlName="name"
/>
</div>
<div class="flex flex-col items-start justify-start w-full">
<label for="email" class="text-xs font-semibold my-1">
Email
</label>
<input
type="email"
name="email"
class="rounded focus:outline-none p-1 w-full"
formControlName="email"
/>
</div>
<div class="flex flex-col items-start justify-start w-full">
<label for="passoword" class="text-xs font-semibold my-1">
Password
</label>
<input
type="password"
name="password"
class="rounded focus:outline-none p-1 w-full"
formControlName="password"
/>
</div>
<button
type="submit"
class="p-1 bg-green-600 rounded-lg w-full font-bold text-gray-50"
>
SignUp
</button>
</form>
</div>
</div>
`,
styles: [],
})
export class SignupComponent implements OnInit {
public signUpForm!: FormGroup;
constructor(private fb: FormBuilder, private authService: AuthService) {}
ngOnInit(): void {
this.signUpForm = this.fb.group({
name: '',
email: '',
password: '',
});
}
async submit() {
if (this.signUpForm.valid) {
const name = this.signUpForm.get('name')?.value;
const email = this.signUpForm.get('email')?.value;
const password = this.signUpForm.get('password')?.value;
await this.authService.signUp({ name, email, password });
}
}
}
Home Component
nx g @nrwl/angular:component remote-entry/components/home --project auth --module=entry --inlineTemplate=true --inlineStyle=true
Now open home.component.ts
file and match the content:
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'mfe-demo-prototype-home',
template: ` <div class="h-screen flex items-center justify-center bg-gray-200">
<div class="p-2 shadow-2xl rounded-lg bg-gray-100">
This Micro App handling only Authentication
</div>
</div> `,
styles: [],
})
export class HomeComponent implements OnInit {
constructor() {}
ngOnInit(): void {}
}
App Component
Now open app.component.ts
file and match the content:
import { Component } from '@angular/core';
@Component({
selector: 'mfe-demo-prototype-root',
template: ` <router-outlet></router-outlet>`,
styles: [],
})
export class AppComponent {}
App Module
Also, we have to make changes to the app.module.ts
file.
/*
* This RemoteEntryModule is imported here to allow TS to find the Module during
* compilation, allowing it to be included in the built bundle. This is required
* for the Module Federation Plugin to expose the Module correctly.
* */
import { RemoteEntryModule } from './remote-entry/entry.module';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { RouterModule, Routes } from '@angular/router';
import { HashLocationStrategy, PathLocationStrategy } from '@angular/common';
const routes: Routes = [
{
path: '',
loadChildren: () =>
import('./remote-entry/entry.module').then((m) => m.RemoteEntryModule),
},
];
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking' }),
],
providers: [
{ provide: PathLocationStrategy, useClass: HashLocationStrategy },
],
bootstrap: [AppComponent],
})
export class AppModule {}
And lastly, edit the webpack.config.js
file to expose the module and to ingest the libraries.
Webpack Config
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const mf = require('@angular-architects/module-federation/webpack');
const path = require('path');
/**
* We use the NX_TSCONFIG_PATH environment variable when using the @nrwl/angular:webpack-browser
* builder as it will generate a temporary tsconfig file which contains any required remappings of
* shared libraries.
* A remapping will occur when a library is buildable, as webpack needs to know the location of the
* built files for the buildable library.
* This NX_TSCONFIG_PATH environment variable is set by the @nrwl/angular:webpack-browser and it contains
* the location of the generated temporary tsconfig file.
*/
const tsConfigPath =
process.env.NX_TSCONFIG_PATH ??
path.join(__dirname, '../../tsconfig.base.json');
const workspaceRootPath = path.join(__dirname, '../../');
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
tsConfigPath,
[
/* mapped paths to share */
'@mfe-demo-prototype/shared/authentication',
'@mfe-demo-prototype/shared/todo',
],
workspaceRootPath
);
module.exports = {
output: {
uniqueName: 'auth',
publicPath: 'auto',
},
optimization: {
runtimeChunk: false,
},
experiments: {
outputModule: true,
},
resolve: {
alias: {
...sharedMappings.getAliases(),
},
},
plugins: [
new ModuleFederationPlugin({
name: 'auth',
filename: 'authRemoteEntry.js',
exposes: {
'./RemoteEntryModule': 'apps/auth/src/app/remote-entry/entry.module.ts',
},
shared: {
'@angular/core': { singleton: true, strictVersion: true },
'@angular/common': { singleton: true, strictVersion: true },
'@angular/common/http': { singleton: true, strictVersion: true },
'@angular/router': { singleton: true, strictVersion: true },
...sharedMappings.getDescriptors(),
},
library: {
type: 'module',
},
}),
sharedMappings.getPlugin(),
],
};
Now we are done with auth application, run
nx run auth:serve
command to run the application on the dev server
Todo Application
Here we will make the todo interface where users can read, create, update, delete to todos.
First, generate the required Components
Todo List Component
nx g @nrwl/angular:component remote/components/todo-list --project todos --module=entry --inlineTemplate=true --inlineStyle=true
Todo List Item Component
nx g @nrwl/angular:component remote/components/todo-list-item --project todos --module=entry --inlineTemplate=true --inlineStyle=true
Todo Form Component
nx g @nrwl/angular:component remote/components/todo-form --project todos --module=entry --inlineTemplate=true --inlineStyle=true
App Module
- Then, go to the
src/app
directory and openapp.module.ts
file, and match the content below
/*
* This RemoteEntryModule is imported here to allow TS to find the Module during
* compilation, allowing it to be included in the built bundle. This is required
* for the Module Federation Plugin to expose the Module correctly.
* */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { RemoteEntryModule } from './remote-entry/entry.module';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { RouterModule, Routes } from '@angular/router';
import { HashLocationStrategy, PathLocationStrategy } from '@angular/common';
const routes: Routes = [
{
path: '',
loadChildren: () =>
import('./remote-entry/entry.module').then((m) => m.RemoteEntryModule),
},
];
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking' }),
],
providers: [
{ provide: PathLocationStrategy, useClass: HashLocationStrategy },
],
bootstrap: [AppComponent],
})
export class AppModule {}
- After that, make changes to
app.component.ts
file with the following
import { Component } from '@angular/core';
@Component({
selector: 'mfe-demo-prototype-root',
template: ` <router-outlet></router-outlet> `,
styles: [],
})
export class AppComponent {}
Remote Entry Module
- Now go to
remote/remote.module.ts
file and match the content:
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TodoFormComponent } from './components/todo-form/todo-form.component';
import { TodoListComponent } from './components/todo-list/todo-list.component';
import { TodoListItemComponent } from './components/todo-list-item/todo-list-item.component';
import { ReactiveFormsModule } from '@angular/forms';
const routes: Routes = [
{
path: ':userUid',
component: TodoListComponent,
},
];
@NgModule({
declarations: [TodoFormComponent, TodoListComponent, TodoListItemComponent],
imports: [
CommonModule,
ReactiveFormsModule,
RouterModule.forChild(routes),
],
providers: [],
})
export class RemoteEntryModule {}
Now its time for components
Todo List Component
go to todo-list.component.ts
file and match the content
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Todo, UserTodoService } from '@mfe-demo-prototype/shared/todo';
import { Observable } from 'rxjs';
@Component({
selector: 'mfe-demo-prototype-todo-list',
template: `
<div class="h-full flex-col items-center justify-center ">
<mfe-demo-prototype-todo-form></mfe-demo-prototype-todo-form>
<h1 class="text-center text-2xl font-bold mt-5 mb-2">Todos</h1>
<div
class="flex flex-col items-center justify-start space-y-5 h-96 flex-grow"
>
<mfe-demo-prototype-todo-list-item
*ngFor="let todo of todos$ | async"
[data]="todo"
></mfe-demo-prototype-todo-list-item>
</div>
</div>
`,
styles: [],
})
export class TodoListComponent implements OnInit {
public todos$!: Observable<Todo[]>;
private userUid!: string;
constructor(
private todoService: UserTodoService,
private activaedRoute: ActivatedRoute
) {
this.activaedRoute.params.subscribe((data) => {
this.userUid = data['userUid'];
this.todoService.setUserId(this.userUid);
});
}
ngOnInit(): void {
this.todos$ = this.todoService.getTodos();
}
}
Todo List Item Component
import { Component, Input } from '@angular/core';
import { Todo, UserTodoService } from '@mfe-demo-prototype/shared/todo';
@Component({
selector: 'mfe-demo-prototype-todo-list-item',
template: `
<div
class="p-2 rounded-lg shadow-md bg-gray-100 w-96 flex items-center justify-between"
>
<div
class="flex items-center justify-start space-x-5"
(click)="updateTodo()"
>
<div>
<!-- icon -->
<svg
*ngIf="!data.done"
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-amber-500"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM7 9H5v2h2V9zm8 0h-2v2h2V9zM9 9h2v2H9V9z"
clip-rule="evenodd"
/>
</svg>
<svg
*ngIf="data.done"
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-green-500"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
/>
</svg>
</div>
<div>
{{ data.body }}
</div>
</div>
<button (click)="deleteTodo()">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-red-500"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
`,
styles: [],
})
export class TodoListItemComponent {
@Input() data!: Todo;
constructor(private todoService: UserTodoService) {}
async updateTodo() {
await this.todoService.updateTodo(this.data);
}
async deleteTodo() {
await this.todoService.deleteTodo(this.data);
}
}
Todo Form Component
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UserTodoService } from '@mfe-demo-prototype/shared/todo';
@Component({
selector: 'mfe-demo-prototype-todo-form',
template: `
<form
class="flex items-center justify-center"
[formGroup]="todoForm"
(submit)="submit()"
>
<div class="flex items-center justify-center shadow-lg rounded-lg">
<input
type="text"
name="body"
id="body"
class="rounded-l-lg h-8 p-1 focus:outline-none"
placeholder="Enter you Todo"
formControlName="body"
/>
<button
type="submit"
class="rounded-r-lg bg-green-600 h-8 w-8 flex items-center justify-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-gray-50"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
</button>
</div>
</form>
`,
styles: [],
})
export class TodoFormComponent implements OnInit {
public todoForm!: FormGroup;
constructor(private fb: FormBuilder, private todoService: UserTodoService) {}
ngOnInit(): void {
this.todoForm = this.fb.group({
body: ['', [Validators.required, Validators.minLength(2)]],
});
this.todoForm.valueChanges.subscribe(console.log);
}
async submit() {
if (this.todoForm.valid) {
const body = this.todoForm.get('body')?.value;
await this.todoService.createTodo(body);
this.todoForm.patchValue({ body: '' });
}
}
}
Webpack Config
With that done now it's time to configure webpack.config.js
file
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const mf = require('@angular-architects/module-federation/webpack');
const path = require('path');
/**
* We use the NX_TSCONFIG_PATH environment variable when using the @nrwl/angular:webpack-browser
* builder as it will generate a temporary tsconfig file which contains any required remappings of
* shared libraries.
* A remapping will occur when a library is buildable, as webpack needs to know the location of the
* built files for the buildable library.
* This NX_TSCONFIG_PATH environment variable is set by the @nrwl/angular:webpack-browser and it contains
* the location of the generated temporary tsconfig file.
*/
const tsConfigPath =
process.env.NX_TSCONFIG_PATH ??
path.join(__dirname, '../../tsconfig.base.json');
const workspaceRootPath = path.join(__dirname, '../../');
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
tsConfigPath,
[
/* mapped paths to share */
'@mfe-demo-prototype/shared/authentication',
'@mfe-demo-prototype/shared/todo',
],
workspaceRootPath
);
module.exports = {
output: {
uniqueName: 'todos',
publicPath: 'auto',
},
optimization: {
runtimeChunk: false,
},
experiments: {
outputModule: true,
},
resolve: {
alias: {
...sharedMappings.getAliases(),
},
},
plugins: [
new ModuleFederationPlugin({
name: 'todos',
filename: 'todosRemoteEntry.js',
exposes: {
'./RemoteEntryModule':
'apps/todos/src/app/remote-entry/entry.module.ts',
},
shared: {
'@angular/core': { singleton: true, strictVersion: true },
'@angular/common': { singleton: true, strictVersion: true },
'@angular/common/http': { singleton: true, strictVersion: true },
'@angular/router': { singleton: true, strictVersion: true },
...sharedMappings.getDescriptors(),
},
library: {
type: 'module',
},
}),
sharedMappings.getPlugin(),
],
};
Now we are all set with Todos Application you can run the dev server and check it yourself. Run
nx run todos:serve
command.
Dashboard Application
This app will act as a host i.e. this will consume the modules exposed by auth and todo application and also use the shared libraries. It will act as a communicator between auth app and the todo app. It will hold the Authentication State for Route Guarding. The user is not allowed to go to the todos page if he is not authenticated. Let's implement such things quickly. We will quickly generate all the components, directives, etc that we will be needing to accomplish micro frontend architecture.
Generate Route Guards
The guards will protect the todo page from unauthorized access.
- Signed In Routes will allow routes if users are signed in.
nx g @nrwl/angular:guard guards/signedIn --project dashboard
- Signed Out will protect users accidentally visiting the login page even after successful login
nx g @nrwl/angular:guard guards/signedOut --project dashboard
Signed In Route Guard
got to guards\signed-in.guard.ts
file and match the content:
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivate,
Router,
RouterStateSnapshot,
UrlTree,
} from '@angular/router';
import { AuthService } from '@mfe-demo-prototype/shared/authentication';
import { map, Observable, } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class SignedInGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router,
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
):
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree>
| boolean
| UrlTree {
return this.authService.currentUser().pipe(
map((user) => !!user),
map((res) => {
if (res) {
return true;
} else {
this.router.navigate(['auth','login']);
return false;
}
})
);
}
}
Signed Out Guard
got to guards\signed-out.guard.ts
file and match the content:
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivate,
Router,
RouterStateSnapshot,
UrlTree,
} from '@angular/router';
import { AuthService } from '@mfe-demo-prototype/shared/authentication';
import { map, Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class SignedOutGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
):
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree>
| boolean
| UrlTree {
const userId = this.authService.authUser.value?.uid;
return this.authService.currentUser().pipe(
map((user) => !!user),
map((res) => {
if (!res) {
return true;
}
this.router.navigate(['dashboard', userId]);
return false;
})
);
}
}
App Component
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import {
AuthService,
AuthUser,
} from '@mfe-demo-prototype/shared/authentication';
import { UserTodoService } from '@mfe-demo-prototype/shared/todo';
import { Observable } from 'rxjs';
@Component({
selector: 'mfe-demo-prototype-root',
template: `
<div class="min-h-screen bg-gray-300">
<div class="flex items-center justify-between p-2">
<h1 class="text-2xl font-bold">
{{ (user | async)?.name }}'s Dashboard
</h1>
<div class="flex items-center justify-end space-x-5">
<div
*ngIf="user | async"
class="flex items-center justify-between space-x-5"
>
<a [routerLink]="['dashboard', userId]">Dashboard</a>
<button (click)="signOut()">Logout</button>
</div>
<div
*ngIf="(user | async) === null"
class="flex items-center justify-between space-x-5"
>
<a routerLink="/auth/login">Login</a>
<a routerLink="/auth/signup">Signup</a>
</div>
</div>
</div>
<router-outlet></router-outlet>
</div>
`,
styles: [],
})
export class AppComponent implements OnInit {
public user!: Observable<AuthUser | null>;
public userId!: string;
constructor(
private authService: AuthService,
private router: Router,
private todoService: UserTodoService
) {}
async ngOnInit(): Promise<void> {
this.user = this.authService.currentUser();
this.user.subscribe((data) => {
if (data === null) {
this.router.navigate(['auth', 'login']);
// this.router.navigate(['shared']);
} else {
this.userId = data.uid;
this.router.navigate(['dashboard', this.userId]);
}
});
}
async signOut() {
await this.authService.signOut();
await this.todoService.clearTodos();
await this.router.navigateByUrl('/auth/login');
}
}
Typescript module declaration
Make a new file decl.d.ts
definition file in the src
directory to declare auth and todo modules.
//decl.d.ts file
declare module 'auth/RemoteEntryModule';
declare module 'todos/RemoteEntryModule';
App Module
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { RouterModule, Routes } from '@angular/router';
import { SignedInGuard } from './guards/signed-in.guard';
import { SignedOutGuard } from './guards/signed-out.guard';
import { AuthService } from '@mfe-demo-prototype/shared/authentication';
import { UserTodoService } from '@mfe-demo-prototype/shared/todo';
import { HashLocationStrategy, PathLocationStrategy } from '@angular/common';
import { LoaderDirective } from './core/loader.directive';
import { DynamicLoaderService } from './core/dynamic-loader.service';
const routes: Routes = [
{
path: 'auth',
pathMatch: 'prefix',
loadChildren: () =>
import('auth/RemoteEntryModule').then((m) => m.RemoteEntryModule),
canActivate: [SignedOutGuard],
},
{
path: 'dashboard',
pathMatch: 'prefix',
loadChildren: () =>
import('todos/RemoteEntryModule').then((m) => m.RemoteEntryModule),
canActivate: [SignedInGuard],
},
];
@NgModule({
declarations: [AppComponent, LoaderDirective],
imports: [
BrowserModule,
RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking' }),
],
providers: [
SignedInGuard,
SignedOutGuard,
AuthService,
UserTodoService,
DynamicLoaderService,
{ provide: PathLocationStrategy, useClass: HashLocationStrategy },
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
bootstrap: [AppComponent],
})
export class AppModule {}
Webpack Config
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const mf = require('@angular-architects/module-federation/webpack');
const path = require('path');
/**
* We use the NX_TSCONFIG_PATH environment variable when using the @nrwl/angular:webpack-browser
* builder as it will generate a temporary tsconfig file which contains any required remappings of
* shared libraries.
* A remapping will occur when a library is buildable, as webpack needs to know the location of the
* built files for the buildable library.
* This NX_TSCONFIG_PATH environment variable is set by the @nrwl/angular:webpack-browser and it contains
* the location of the generated temporary tsconfig file.
*/
const tsConfigPath =
process.env.NX_TSCONFIG_PATH ??
path.join(__dirname, '../../tsconfig.base.json');
const workspaceRootPath = path.join(__dirname, '../../');
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
tsConfigPath,
[
/* mapped paths to share */
'@mfe-demo-prototype/shared/authentication',
'@mfe-demo-prototype/shared/todo',
],
workspaceRootPath
);
module.exports = {
output: {
uniqueName: 'dashboard',
publicPath: 'auto',
},
optimization: {
runtimeChunk: false,
},
experiments: {
outputModule: true,
},
resolve: {
alias: {
...sharedMappings.getAliases(),
},
},
plugins: [
new ModuleFederationPlugin({
remotes: {
auth: 'http://localhost:4201/authRemoteEntry.js',
todos: 'http://localhost:4202/todosRemoteEntry.js',
},
shared: {
'@angular/core': { singleton: true, strictVersion: true },
'@angular/common': { singleton: true, strictVersion: true },
'@angular/common/http': { singleton: true, strictVersion: true },
'@angular/router': { singleton: true, strictVersion: true },
...sharedMappings.getDescriptors(),
},
library: {
type: 'module',
},
}),
sharedMappings.getPlugin(),
],
};
With that done now we are ready to launch all the apps at once and see how it works.
Run the command nx run-many --parallel --target:serve --all
to launch all applications at once or you can also launch individually.
After that open the browser and go to Host application address http://localhost:4200
.