Getting Started with Webpack Module Federation and Angular


Isn't it fun,

to instantly listen to a blog on the go? PLAY !

 
 

Federation-and-Angular

This blog shows how to use Webpack Module Federation jointly with Angular CLI and the @angular-architects/module-federation plugin. The purpose is to make a shell competent of loading anindividually compiled and deployed micro frontend:

Table of Content

Module Federation provides loading separately compiled and destroyed code into an application. This plugin makes Module Federation work jointly with Angular and the CLI.

Features

  1. It Generates the skeleton for a Module Federation config.
  2. It installs a custom builder to allow Module Federation.
  3. Distributing a new port to serve several projects at one time.

The module federation config is biased webpack configuration. It only has thing to control module federation. The rest is generated by the CLI as normal.

Since version 1.2 it also provides some advanced features like the following:

  1. Dynamic Module Federation Support
  2. Sharing Libs of a Monorepo

Usage

  1. Run this command in CLI: ng add @angular-architects/module-federation
  2. Regulate the generated webpack.config.js file
  3. Repeat these steps for further project in workspace if needed.

Opt-in into webpack 5 with CLI

First of all, you need to download and install yarn.

In an existing project you should run the following command:

ng config -generate cli.packageManager yarn

In Projects you should run the following command:
 

ng new workspace-name --packageManager yarn

Add the following to your package.json file(like before the dependencies section) to force the CLI into webpack 5:
 

"resolutions": {
    "webpack": "^5.0.0"
},

Run the yarn command to install all packages

Please ensure that the webpack 5 CLI support is experimental in CLI 11. Here we find a list of unresolved issues in the current version.

Getting Started

To get started we require an Angular CLI version for supporting webpack 3.

For opting-in, add the following code to your package.json like in front of dependency section:

"resolutions": {
    "webpack": "^5.0.0"
},

After that, install your dependencies again using the yarn command. Using yarn place of npm is vital because it uses the down resolutions section to force all installed dependencies like the CLI into using webpack 5.

To make the by default CLI use when calling commands like ng add or ng update, we can use the following command:

ng config cli.packageManager yarn

We should note that the CLI version v11.0.0-next.6 doesn’t support current recompilation in dev mode when using webpack 5. Then we require to restart the dev server after changes. This issue will get solved with one of the upcoming beta versions of CLI 11.



Activating Module Federation for Angular Projects

The testsintroduced here suggests that both, the shell and the microfrontend are projects in the same Angular workspace. For getting started, we require to tell the CLI to use module federation when building them. Despite that, as the CLI shields workspace from us we require a custom builder.

The package @angular-architects/module-federation allows such a custom builder. To get started you can just add “ng add” to your projects:

ng add @angular-architects/module-federation --project shell --port 4000
ng add @angular-architects/module-federation --project mfe1 --port 5000

While it is evident that the project shell having the code for the shell, mfe1 stands for Micro Frondend1.

The Shell (aka Host)

Let us start with the shell which will also be called the host in module federation. It uses the router to lazy load CarModule:

export const APP_ROUTES: Routes = [
  {
    path: '',
    component: HomeComponent,
    pathMatch: 'full'
  },
  {
    path: 'cars',
    loadChildren: () => import('mfe1/Module').then(m => m.CarsModule)
  },
];

Although, the path mfe1/Module which is imported here doesn’t exist within the shell. It is just a virtual path pointing to another project.

The mitigate the Typescript compiler, we require a typing for it:

declare module 'mfe1/Module';

Also, we require to tell webpack that all paths starting with mfe1 are pointing to another project. This can be done through the ModuleFederationPulgin in the generated webpack.config.js:

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

[...]

module.exports = {
  output: {
    uniqueName: "shell"
  },
  optimization: {
    // Only required to bypass a temporary bug
    runtimeChunk: false
  },
  plugins: [
    new ModuleFederationPlugin({
        remotes: {	
            'mfe1': "mfe1@http://localhost:5000/remoteEntry.js" 
        },
        shared: {
          "@angular/core": { singleton: true, strictVersion: true }, 
          "@angular/common": { singleton: true, strictVersion: true }, 
          "@angular/router": { singleton: true, strictVersion: true },
        [...]

        }
    }),
    [...]
  ],
};

The remotes section maps the internal name mfe1 to the same one determined within the individually compiled micro frontend. It also points to the path where the remote can be found –or to be more specific. To its remote entry. This is a little file generated by webpack when building the remote. Webpack loads it at runtime to get all the info required for communicating with micro frontend.

While determining the URL of remote entry, that way is feasible for development. We require a more dynamic method for production. Hopefully, there are certain options for doing this.

The property sharing having the names of libraries our shell shares with the micro frontend. The togetherness of singleton:true and strictVersion:true makes webpack eject a runtime error when the shell and the microfrontend require different incompatible versions. If we ignored or missed strictVersion or set it to false. Webpack would only eject a warning at runtime.

In addition to the settings for the ModuleFederalPlugin, we also require to place some options in the output section.

The uniqueName is used to represent the host or remote in the produced bundles. By default wepack uses the name from package.json for this. In the interest of ignore name conflicts when using monorepos with several applications, it is appropriate to set the uniqueName manually.

The Micro frontend (aka Remote)

The microfrontend, also considered as a remote with terms of module federationlooks like common Angular application. It has routes determined inside the AppModule:

export const APP_ROUTES: Routes = [
  { path: '', component: HomeComponent, pathMatch: 'full'}
];

Also, there is a CarsModule:

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild(CARS_ROUTES)
  ],
  declarations: [
    CarsSearchComponent
  ]
})
export class CarsModule { }

This module has its own routes

export const CARS_ROUTES: Routes = [
  {
    path: 'cars-search',
    component: CarsSearchComponent
  }
];

Wants to Talk with Our Highly Skilled Angular Developer ?

Your Search ends here.


For the purpose of making it possible to load the CarsModule into the shell, we also require to refer the ModuleFederationPlugin in the remote’s webpack configuration:

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/cars/cars.module.ts',
        },
        shared: {
          "@angular/core": { singleton: true, strictVersion: true }, 
          "@angular/common": { singleton: true, strictVersion: true }, 
          "@angular/router": { singleton: true, strictVersion: true },
        [...]  
        }
    }),
    [...]
  ],
};

Standalone Mode for Microfrontend

For Microfrontends that also can be executed without the shell, we require to take care about one little stuff: Projects configure using the ModuleFederationPlugin require to load shared libraries using dynamic imports.

The motive is that these imports are asynchronous and so the infrastructure has some time to decide upon which version of shared libraries to use. This is specifically important when the shell and the micro frontend allows different version of the libraries shared. By default, webpack tries to load the highest consistent version. If there is not such a stuff as “the highest consistent version”, Module federation gives certain fallbacks. They are also described in the article considered.

So that aren’t continuously confrontend with this limitation, it is advisable to load the whole application via a dynamic import instead. The entry point of the application in an Angular CLI project this is generally the main.ts file, thus only consists of a single dynamic imports:

import('./bootstrap');

This loads another Typescript module which is called bootstrap.ts. It takes care of bootstrapping the app:

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

As you see here, the bootstrap.ts file having the code which is normally found in main.ts

For try everything out, we just require to start the shell and microfrontend:

ng serve shell -o
ng serve mfe1 -o

After that, when clicks on Cars in the shell, the microfrontend is get loaded.

Bonus: Remote Entry Loading

As we discussed above, the microfrontend’s remote entry can be specified in the shell’s webpack configuration. Despite that, this requests us the foresee the microforntend’s URL when compiling the shell.

As substitute, we can also load the remote entry by referencing it with a script tag:

This script can be created dynamically like by using server-side templates or by operating the DOM on the client side.

To make this work we require to switch the remoteType in the shell’s config to var:

new ModuleFederationPlugin({
  remoteType: 'var',
  [...]
})

There are also more dynamic steps to inform the shell just at runtime how many microfrontends to respect, what is their names and where to find them.

Conclusion

The implementation of microfrontends has so far involved several tricks and workarounds. Webpack Module Federation finally provides a simple and solid solution for it. To improve best performance, libraries can and be shared and strategies for dealing with conflicting versions can be configured.

This script can be created dynamically like by using server-side templates or by operating the DOM on the client side.

To make this work we require to switch the remoteType in the shell’s config to var:

new ModuleFederationPlugin({
  remoteType: 'var',
  [...]
})

There are also more dynamic steps to inform the shell just at runtime how many microfrontends to respect, what is their names and where to find them.

Conclusion

The implementation of microfrontends has so far involved several tricks and workarounds. Webpack Module Federation finally provides a simple and solid solution for it. To improve best performance, libraries can and be shared and strategies for dealing with conflicting versions can be configured.