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:
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 microfrontend. -
Have a look to the shell's source code.
-
Stop the CLI (
CTRL+C
).
-
-
Do the same for the microfrontend. In this project, it's called
mfe1
(Microfrontend 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 shell --port 5000 ng add @angular-architects/module-federation --project mfe1 --port 3000
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 ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); [...] module.exports = { [...], plugins: [ new ModuleFederationPlugin({ // For remotes (please adjust) name: "mfe1", filename: "remoteEntry.js", exposes: { // Update this: './Module': './projects/mfe1/src/app/flights/flights.module.ts', }, shared: { "@angular/core": { singleton: true, strictVersion: true }, "@angular/common": { singleton: true, strictVersion: true }, "@angular/router": { singleton: true, strictVersion: true }, [...] } }), [...] ], };
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
. Adjust it as follows:const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); [...] module.exports = { [...], plugins: [ new ModuleFederationPlugin({ // Make sure to use port 3000 remotes: { 'mfe1': "mfe1@http://localhost:3000/remoteEntry.js" }, shared: { "@angular/core": { singleton: true, strictVersion: true }, "@angular/common": { singleton: true, strictVersion: true }, "@angular/router": { singleton: true, strictVersion: true }, [...] } }), [...] ], };
This references the separately compiled and deployed
mfe1
project. There are some alternatives to configure its URL (see links at the end). -
Open the
shell
's router config (projects\shell\src\app\app.routes.ts
) and add a route loading the microfrontend:{ 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:5000
), click onFlights
. This should load the microfrontend into the shell: -
Also, ensure yourself that the microfrontend also runs in standalone mode at http://localhost:3000:
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 filewebpack.config.js
. Here, remove the registered remotes:remotes: { // Remove this line or comment it out: // "mfe1": "mfe1@http://localhost:3000/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({ remoteEntry: 'http://localhost:3000/remoteEntry.js', 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. 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 remote entry upfront before Angular bootstraps. In this early phase, Module Federation tries to determine the highest compatible versions of all dependencies. Let's assume, the shell provides version 1.0.0 of a dependency (specifying ^1.0.0 in its package.json
) and the micro frontend uses version 1.1.0 (specifying ^1.1.0 in its package.json
). In this case, they would go with version 1.1.0. However, this is only possible if the remote's entry is loaded upfront.
-
Switch to the
shell
project and open the filemain.ts
. Adjust it as follows:import { loadRemoteEntry } from '@angular-architects/module-federation'; Promise.all([ loadRemoteEntry('http://localhost:3000/remoteEntry.js', 'mfe1') ]) .catch(err => console.error('Error loading remote entries', err)) .then(() => import('./bootstrap')) .catch(err => console.error(err));
-
Open the file
app.routes.ts
and comment out (or remove) the propertyremoteEntry
:import { loadRemoteModule } from '@angular-architects/module-federation'; [...] const routes: Routes = [ [...] { path: 'flights', loadChildren: () => loadRemoteModule({ // remoteEntry: 'http://localhost:3000/remoteEntry.js', 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.
-
Add a library to your monorepo:
ng g lib auth-lib
-
In your
tsconfig.json
in the project'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). -
Open the
shell
'swebpack.config.js
and register the createdauth-lib
with thesharedMappings
:const sharedMappings = new mf.SharedMappings(); sharedMappings.register( path.join(__dirname, '../../tsconfig.json'), ['auth-lib'] // <-- Add this entry! );
-
Also open the micro frontends (
mfe1
)webpack.config.js
and do the same. -
Switch to your
auth-lib
project and open the fileauth-lib.service.ts
. Adjust it as follows:@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
. Use the sharedAuthLibService
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 { [...] user = this.service.user; constructor(private service: AuthLibService, [...]) { } [...] }
-
Open this component's template(
flights-search.component.html
) and data bind the propertyuser
:<div id="container"> <div>{{user}}</div> [...] </div>
-
Restart both, the
shell
and the micro frontend (mfe1
). -
In the shell, navigate to the micro frontend. If it shows the same user name, the library is shared.
Have a look at this article series about Module Federation