Tutorial: Getting Started with Webpack Module Federation and Angular
Manfred Steyer
Posted on November 29, 2020
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:
This topic is also an important part of our Angular Workshop with a focus on Architectures (100% Online, Advanced, available in English and German)
Part 1: Clone and Inspect the Starterkit
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
-
Have a look to the
package.json
. You should find this section:
"resolutions": { "webpack": "5.0.0" },
This section makes yarn to install webpack 5 for the CLI (and for all the other libraries depending on webpack).
-
Move into the project directory and install the dependencies with yarn:
cd module-federation-plugin-example yarn
You really need to install the dependencies with yarn because providing resolutions as shown above is a yarn feature.
-
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.Please ignore depreaction warnings. They are a temporal issue in the current CLI beta when using webpack 5.
-
Have a look to the shell's source code.
Please note that the current CLI beta lacks some features when using it with webpack 5, e. g. reloading an application in debug mode (when using ng serve). Hence, you have to restart ng serve after changing a source file. This is just a temporal limitation and will be solved with one of the upcoming versions.
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
.
Part 2: Activate and Configure Module Federation
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 = { output: { uniqueName: "mfe1" }, optimization: { // Only needed to bypass a temporary bug runtimeChunk: false }, plugins: [ new ModuleFederationPlugin({ // For remotes (please adjust) name: "mfe1", filename: "remoteEntry.js", exposes: { './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 = { output: { uniqueName: "shell" }, optimization: { // Only needed to bypass a temporary bug runtimeChunk: false }, plugins: [ new ModuleFederationPlugin({ 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';
Part 3: Try it out
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!
Part 4: Switch to Dynamic Federation
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.
Step 5: Share a Library of Your Monorepo
-
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.
More Details on Module Federation
Have a look at this article series about Module Federation
Angular Trainings, Workshops, and Consulting
Posted on November 29, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.