This tutorial shows how to use Webpack Module Federation together with the Angular CLI and the @angular-architects/module-federation
plugin. The goal is to make a shell capable of loading a separately compiled and deployed microfrontend:
Important: This tutorial is written for Angular and Angular CLI 14 and higher. To find out about the small differences for lower versions of Angular and for the migration from such a lower version, please have a look to our migration guide.
In this part you will clone the starterkit and inspect its projects.
-
Clone the starterkit for this tutorial:
git clone https://github.com/manfredsteyer/module-federation-plugin-example.git --branch starter
-
Move into the project directory and install the dependencies with npm:
cd module-federation-plugin-example npm i
-
Start the shell (
ng serve shell -o
) and inspect it a bit:-
Click on the
flights
link. It leads to a dummy route. This route will later be used for loading the separately compiled Micro Frontend. -
Have a look to the shell's source code.
-
Stop the CLI (
CTRL+C
).
-
-
Do the same for the Micro Frontend. In this project, it's called
mfe1
(Micro Frontend 1) You can start it withng serve mfe1 -o
.
Now, let's activate and configure module federation:
-
Install
@angular-architects/module-federation
into the shell and into the micro frontend:ng add @angular-architects/module-federation --project mfe1 --type remote --port 4201 ng add @angular-architects/module-federation --project shell --type host --port 4200
This activates module federation, assigns a port for ng serve, and generates the skeleton of a module federation configuration.
-
Switch into the project
mfe1
and open the generated configuration fileprojects\mfe1\webpack.config.js
. It contains the module federation configuration formfe1
. Adjust it as follows:const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack'); module.exports = withModuleFederationPlugin({ name: 'mfe1', exposes: { // Update this whole line (both, left and right part): './Module': './projects/mfe1/src/app/flights/flights.module.ts' }, shared: { ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }), }, });
This exposes the
FlightsModule
under the Name./Module
. Hence, the shell can use this path to load it. -
Switch into the
shell
project and open the fileprojects\shell\webpack.config.js
. Make sure, the mapping in the remotes section uses port4201
(and hence, points to the Micro Frontend):const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack'); module.exports = withModuleFederationPlugin({ remotes: { // Check this line. Is port 4201 configured? "mfe1": "http://localhost:4201/remoteEntry.js", }, shared: { ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }), }, });
This references the separately compiled and deployed
mfe1
project. -
Open the
shell
's router config (projects\shell\src\app\app.routes.ts
) and add a route loading the Micro Frontend:{ path: 'flights', loadChildren: () => import('mfe1/Module').then(m => m.FlightsModule) },
Please note that the imported URL consists of the names defined in the configuration files above.
-
As the Url
mfe1/Module
does not exist at compile time, ease the TypeScript compiler by adding the following line to the fileprojects\shell\src\decl.d.ts
:declare module 'mfe1/Module';
Now, let's try it out!
-
Start the
shell
andmfe1
side by side:ng serve shell -o ng serve mfe1 -o
Hint: You might use two terminals for this.
-
After a browser window with the shell opened (
http://localhost:4200
), click onFlights
. This should load the Micro Frontend into the shell: -
Also, ensure yourself that the Micro Frontend also runs in standalone mode at http://localhost:4201:
Hint: You can also call the following script to start all projects at once: npm run run:all
. This script is added by the Module Federation plugin.
Congratulations! You've implemented your first Module Federation project with Angular!
Now, let's remove the need for registering the Micro Frontends upfront with with shell.
-
Switch to your
shell
application and open the fileprojects\shell\webpack.config.js
. Here, remove the registered remotes:remotes: { // "mfe1": "http://localhost:4201/remoteEntry.js", },
-
Open the file
app.routes.ts
and use the functionloadRemoteModule
instead of the dynamicimport
statement:import { loadRemoteModule } from '@angular-architects/module-federation'; [...] const routes: Routes = [ [...] { path: 'flights', loadChildren: () => loadRemoteModule({ type: 'module', remoteEntry: 'http://localhost:4201/remoteEntry.js', exposedModule: './Module' }) .then(m => m.FlightsModule) }, [...] ]
Remarks:
type: 'module'
is needed for Angular 13 or higher as beginning with version 13 the CLI emits EcmaScript modules instead of "plain old" JavaScript files. -
Restart both, the
shell
and the micro frontend (mfe1
). -
The shell should still be able to load the micro frontend. However, now it's loaded dynamically.
This was quite easy, wasn't it? However, we can improve this solution a bit. Ideally, we load the Micro Frontend's remoteEntry.js upfront.before Angular bootstraps. This file contains meta data about the Micro Frontend, esp. about its shared dependencies. Knowing about them upfront help Module Federation to avoid version conflicts.
-
Switch to the
shell
project and open the filemain.ts
. Adjust it as follows:import { loadRemoteEntry } from '@angular-architects/module-federation'; Promise.all([ loadRemoteEntry({ type: 'module', remoteEntry: 'http://localhost:4201/remoteEntry.js' }) ]) .catch(err => console.error('Error loading remote entries', err)) .then(() => import('./bootstrap')) .catch(err => console.error(err));
-
Restart both, the
shell
and the micro frontend (mfe1
). -
The shell should still be able to load the micro frontend.
So far, we just hardcoded the urls pointing to our Micro Frontends. However, in a real world scenario, we would rather get this information at runtime from a config file or a registry service. This is what this exercise is about.
-
Switch to the shell, and create a file
mf.manifest.json
in itsassets
folder (projects\shell\src\assets\mf.manifest.json
):{ "mfe1": "http://localhost:4201/remoteEntry.js" }
-
Adjust the shell's
main.ts
(projects/shell/src/main.ts
) as follows:import { loadManifest } from '@angular-architects/module-federation'; loadManifest('assets/mf.manifest.json') .catch(err => console.error('Error loading remote entries', err)) .then(() => import('./bootstrap')) .catch(err => console.error(err));
The imported
loadManifest
function also loads the remote entry points. -
Adjust the shell's lazy route pointing to the Micro Frontend as follows (
projects/shell/src/app/app.routes.ts
):{ path: 'flights', loadChildren: () => loadRemoteModule({ type: 'manifest', remoteName: 'mfe1', exposedModule: './Module' }) .then(m => m.FlightsModule) },
-
Restart both, the
shell
and the micro frontend (mfe1
). -
The shell should still be able to load the micro frontend.
Hint: The ng add
command used initially also provides an option --type dynamic-host
. This makes ng add to generate the mf.manifest.json
and generates the call to loadManifest
in the main.ts
.
-
Add a library to your monorepo:
ng g lib auth-lib
-
In your
tsconfig.json
in the workspace's root, adjust the path mapping forauth-lib
so that it points to the libs entry point:"auth-lib": [ "projects/auth-lib/src/public-api.ts" ]
-
As most IDEs only read global configuration files like the
tsconfig.json
once, restart your IDE (Alternatively, your IDE might also provide an option for reloading these settings). -
Switch to your
auth-lib
project and open the fileauth-lib.service.ts
(projects\auth-lib\src\lib\auth-lib.service.ts
). Adjust it as follows:import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class AuthLibService { private userName: string; public get user(): string { return this.userName; } constructor() { } public login(userName: string, password: string): void { // Authentication for **honest** users TM. (c) Manfred Steyer this.userName = userName; } }
-
Switch to your
shell
project and open itsapp.component.ts
(projects\shell\src\app\app.component.ts
). Use theAuthLibService
to login a user:import { AuthLibService } from 'auth-lib'; @Component({ selector: 'app-root', templateUrl: './app.component.html' }) export class AppComponent { title = 'shell'; constructor(private service: AuthLibService) { this.service.login('Max', null); } }
-
Switch to your
mfe1
project and open itsflights-search.component.ts
. Use the shared service to retrieve the current user's name:export class FlightsSearchComponent { // Add this: user = this.service.user; // And add that: constructor(private service: AuthLibService) { } [...] }
-
Open this component's template(
flights-search.component.html
) and data bind the propertyuser
:<div id="container"> <div>User: {{user}}</div> [...] </div>
-
Restart both, the
shell
and the micro frontend (mfe1
). -
In the shell, navigate to the micro frontend. If it shows the used user name
Max
, the library is shared.
Remarks: All the libraries of your Monorepo are shared by default. The next section shows how to select libraries to share.
So far, all dependencies have been shared. The used shareAll
function makes sure, all packages in your package.json
's dependencies
section are shared and by default, all monorepo-internal libraries like the auth-lib
are shared too.
While this makes getting started with Module Federation easy, we can get a more performant solution by directly defining what to share. This is because shared dependencies are not tree-shakable and they end up in a bundle of their on that needs to be loaded.
For explicitly sharing our dependencies, you could switch to the following configurations:
-
Shell's
webpack.config.js
(projects\shell\webpack.config.js
):// Import share instead of shareAll: const { share, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack'); module.exports = withModuleFederationPlugin({ remotes: { // Check this line. Is port 4201 configured? // "mfe1": "http://localhost:4201/remoteEntry.js", }, // Explicitly share packages: shared: share({ "@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, "@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, "@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, "@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, }), // Explicitly share mono-repo libs: sharedMappings: ['auth-lib'], });
-
Micro Frontend's
webpack.config.js
(projects\mfe1\webpack.config.js
):// Import share instead of shareAll: const { share, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack'); module.exports = withModuleFederationPlugin({ name: 'mfe1', exposes: { // Update this whole line (both, left and right part): './Module': './projects/mfe1/src/app/flights/flights.module.ts' }, // Explicitly share packages: shared: share({ "@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, "@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, "@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, "@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, }), // Explicitly share mono-repo libs: sharedMappings: ['auth-lib'], });
After that, restart the shell
and the Micro Frontend.
Have a look at this article series about Module Federation