Introduction
I've already covered implementing microfrontends for React using Vite and Module Federation. In this article, we'll continue that discussion by implementing a microfrontend using Angular.
Since Angular is a complete framework, the integration of microfrontends comes with automatic tooling support. We won't need to configure as much as we did with React.
Detail
First, you need to install the Angular CLI:
npm install -g @angular/cli
Note that in this article, I'm using Angular version 20. If you're using a different version, please ensure that Module Federation is compatible with that Angular version.
As per the theory I discussed in previous articles about Microfrontends, we need a shell app and a remote app. In this article, I'll also add a remote component to demonstrate how to integrate both a remote app and a remote component into the shell app.
Creating the Remote App
Use the Angular CLI to execute the following command:
After configuring the necessary information and initializing the project, continue by adding Module Federation:
ng add @angular-architects/module-federation --project=remote-app
The resulting information will be as follows. Note that you need to input a port (I chose port 4201 here) and select the classic Module Federation webpack stack:
✔ Determining Package Manager
› Using package manager: npm
✔ Searching for compatible package version
› Found compatible package version: 20.0.0.
✔ Loading package information from registry
✔ Confirming installation
✔ Installing package
✔ Port to use (press enter for default port 4200) 4201
✔ Which stack do you want to use? Module Federation with webpack (classic)
CREATE webpack.prod.config.js (46 bytes)
CREATE webpack.config.js (363 bytes)
CREATE src/bootstrap.ts (228 bytes)
UPDATE tsconfig.json (753 bytes)
UPDATE tsconfig.app.json (190 bytes)
UPDATE angular.json (2485 bytes)
UPDATE package.json (1378 bytes)
UPDATE src/main.ts (58 bytes)
✔ Packages installed successfully.
Next, create the following files sequentially in the app folder. The file contents are very simple; you can modify them to whatever information you need.
app/test1/test1.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-test1',
templateUrl: './test1.html',
})
export class Test1Component { }
app/test1/test1.html
<div style="border: 1px dashed #000; padding: 10px">test1 component</div>
app/test2/test2.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-test2',
imports: [RouterOutlet],
templateUrl: './test2.html',
})
export class Test2Component { }
app/test2/test2.html
<div style="border: 1px dashed #000; padding: 10px; margin-bottom: 10px">test2 component</div>
<router-outlet></router-outlet>
app/test2/routes.ts
import { Routes } from '@angular/router';
export const REMOTE_ROUTES: Routes = [
{
path: '',
loadComponent: () => import('./test2').then((t) => t.Test2Component),
children: [
{
path: 'test3',
loadComponent: () => import('../test3/test3').then((t) => t.Test3Component),
},
],
},
];
app/test2/test2.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { REMOTE_ROUTES } from './routes';
@NgModule({
imports: [CommonModule, RouterModule.forChild(REMOTE_ROUTES)],
})
export class Test2Module { }
app/test3/test3.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-test3',
templateUrl: './test3.html',
})
export class Test3Component { }
app/test3/test3.html
<div style="border: 1px dashed #000; padding: 10px">test3 component</div>
Next, modify the remote-app/webpack.config.ts file as follows:
const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
name: 'remote-app',
filename: 'remoteAppEntry.js',
exposes: {
'./Test1': './src/app/test1/test1.ts',
'./Test2': './src/app/test2/test2.module.ts',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});
As you can see, this configuration is quite similar to configuring Module Federation in React. We also need to configure the following information:
- filename: This is the JS file that the shell app will fetch data from and load into the main application.
- exposes: This specifies the content that the shell app will consume.
- shared: This specifies the content that is shared between the shell app and the remote app.
Creating the Remote Component
The initial steps are similar to creating the Remote App:
ng new remote-component
ng add @angular-architects/module-federation --project=remote-component
Remember to choose a port different from the one you chose for the Remote App (I chose port 4202 here).
Next, I'll create a simple component:
app/test1/test1.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-test1',
templateUrl: './test1.html',
})
export class Test1Component { }
app/test1/test1.html
<div style="border: 1px dashed #000; padding: 10px">remote component: test1 component</div>
Next, modify the remote-component/webpack.config.ts file, similar to the Remote App:
const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
name: 'remote-component',
filename: 'remoteComponentEntry.js',
exposes: {
'./Test1Component': './src/app/test1/test1.ts',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});
Creating the Shell App
This will be the main application where we integrate the Remote App and Remote Component for use.
ng new shell-app
ng add @angular-architects/module-federation --project=shell-app
You need to choose a port different from the ports used for the Remote App and Remote Component (I'm using port 4200 here).
To implement the usage of the Remote Component:
app/test/test.html
<div style="border: 1px dashed #000; padding: 10px; margin-bottom: 10px">test component</div>
<ng-container #container></ng-container>
app/test/test.ts
import { loadRemoteModule } from '@angular-architects/module-federation';
import { Component, ViewChild, ViewContainerRef } from '@angular/core';
@Component({
selector: 'app-test',
templateUrl: './test.html',
})
export class TestComponent {
@ViewChild('container', { read: ViewContainerRef }) viewContainer!: ViewContainerRef;
async ngOnInit() {
try {
const { Test1Component } = await loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:4202/remoteComponentEntry.js',
exposedModule: './Test1Component',
});
this.viewContainer.createComponent(Test1Component);
} catch (error) {
console.error('Failed to load remote component:', error);
}
}
}
You can see that I'm using the loadRemoteModule function from @angular-architects/module-federation to fetch the module directly from the Remote Component and inject it into the container defined in the HTML. Pay attention to values like remoteEntry, exposedModule, and Test1Component—these are the details you defined in the Remote Component's webpack.config.ts.
Next, to use the Remote App:
app/app.html
app/app.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
templateUrl: './app.html',
styleUrl: './app.scss',
})
export class App { }
app/app.routes.ts
import { loadRemoteModule } from '@angular-architects/module-federation';
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'test',
loadComponent: () => import('./test/test').then((m) => m.TestComponent),
},
{
path: 'remote-app-test1',
loadComponent: () =>
loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:4201/remoteAppEntry.js',
exposedModule: './Test1',
})
.then((m) => m.Test1Component)
.finally(() => {
console.log('Load Remote module done!');
}),
},
{
path: 'remote-app-test2',
loadChildren: () =>
loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:4201/remoteAppEntry.js',
exposedModule: './Test2',
})
.then((m) => m.Test2Module)
},
];
You can see that I continue to use the loadRemoteModule function to load the module directly from the Remote App and use it as a route in the Shell App. Be sure to correctly input the values you defined in the Remote App's webpack.config.ts.
Now that the source code is prepared, to use it, start all three projects: Shell App (http://localhost:4200), Remote App (http://localhost:4201), and Remote Component (http://localhost:4202). You should see the results.
As you can see, even though three projects are started on different ports, the Remote App and Remote Component are used within the Shell App, so you only need to access the Shell App (http://localhost:4200) to use everything.
You can modify the content of any project directly, and you should see the changes applied immediately afterward.
The examples I've provided in this article are relatively simple because the main concept you need to understand is how to integrate the Shell App, Remote App, and Remote Component. From there, you can extend the functionality to use it in real-world projects as needed.
Happy coding!
See more articles here.
Comments
Post a Comment