The Microfrontend Revolution – Part 2: Module Federation with Angular

With Module Federation we can use Angular and its router to lazy load separately compiled and deployed microfrontends.


This article is part of a series:


 

Big thanks to Zack Jackson, the mastermind behind Module Federation, who helped me bypassing some pitfalls.

In my previous article, I've shown how to use Module Federation which will be part of webpack 5 to implement microfrontends. This article brings Angular into play and shows how to create an Angular-based microfrontend shell using the router to lazy load the separately compiled, and deployed microfrontends.

Besides using Angular, the result looks similar as in the previous article:

Shell

The loaded microfrontend is shown within the red dashed border. Also, the microfrontend can be used without the shell:

Microfrontend without Shell

The source code of the used example can be found in my GitHub account.

The Shell (aka Host)

Let's start with the shell which would also be called the host with terms of module federation. It uses the router to lazy load a FlightModule:

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

However, the path mfe1/Module which is imported here, does not exist within the shell. It's just a virtual path pointing to another project.

To ease the TypeScript compiler, we need a typing for it:

// decl.d.ts
declare module 'mfe1/Module';

Also, we need to tell webpack that all paths starting with mfe1 are pointing to an other project. This can be done with the ContainerReferencePlugin:

new ContainerReferencePlugin({
  remoteType: 'var',
  remotes: {
    mfe1: "mfe1"
  },
  overrides: [
      "@angular/core", 
      "@angular/common", 
      "@angular/router"
  ]
}),

The remotes section maps the internal name mfe1 to the same one defined within the separately compiled microfrontend. The concrete path of this project isn't defined here. That happens later when the shell loads a so called remote entry point provided by the microfrontend.

Also, overrides contains the names of libraries our shell shares with the microfrontend.

While the ContainerReferencePlugin shown here is intended for the shell, the ContainerPlugin used below is the one for configuring the microfrontends.

Perhaps you've noticed that the previous article only used the ModuleFederationPlugin. However, this plugin is just delegating to both, the ContainerReferencePlugin and the ContainerPlugin. Projects configured with it can be used as a shell and a microfrontend at the same time.

The Microfrontend (aka Remote)

The microfrontend -- also referred to as a remote with terms of module federation -- looks like an ordinary Angular application. It has routes defined within in the AppModule:

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

Also, there is a FlightsModule:

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild(FLIGHTS_ROUTES)
  ],
  declarations: [
    FlightsSearchComponent
  ]
})
export class FlightsModule { }

This module has some routes of its own:

export const FLIGHTS_ROUTES: Routes = [
    {
      path: 'flights-search',
      component: FlightsSearchComponent
    }
];

In order to make it possible to load the FlightsModule into the shell, we need to reference the ContainerPlugin in our webpack configuration:

new ContainerPlugin({
  name: "mfe1",
  filename: "remoteEntry.js",
  exposes: {
    Module: './projects/mfe1/src/app/flights/flights.module.ts'
  },
  library: { type: "var", name: "mfe1" },
  overridables: [
      "@angular/core", 
      "@angular/common", 
      "@angular/router"
  ]
}),

The configuration shown here exposes the FlightModule under the public name Module. The section overridables points to the libraries shared with the shell.

Also, the microfrontend's configuration must point to the location it will be deployed at:

output: {
    publicPath: "http://localhost:3000/",
    [...]
},

Obviously, it would be better if we could specify this path at runtime. Fortunately, when this article was written, there was already a Pull Request for this.

Standalone-Mode for Microfrontend

For microfrontends that also can be executed without the shell, we need to take care about one tiny thing: Projects configured with the ContainerPlugin or the ModuleFederationPlugin need to load shared libraries using dynamic imports!.

The reason is that these imports are asynchronous and so the infrastructure has some time to check which libraries are already loaded by the shell and which ones need to be loaded in addition.

Unfortunately, the Angular Compiler assumes that some parts of Angular are imported in a static way. Also, the CLI scaffolds such code.

To bypass this show-stopper, we can use a tiny trick: Let's move the contents of the entry point main.ts into a file called bootstrap.ts. Then, dynamically import bootstrap.ts in main.ts.

import('./bootstrap');

This dynamic import gives the infrastructure the time necessary for loading everything the shell hasn't already loaded. Hence, in the remaining parts of the application -- e. g. in main.ts -- we can leverage the usual static imports:

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));

This trick is not needed for the shell shown above because it's configured with the ContainerReferencePlugin. If we configured it with the ModuleFederationPlugin instead, we would need to go with this workaround, however.

Connecting the Shell and the Microfrontend

Now, everything left to do, is letting the shell know where to find the microfrontend. For this, we just load the remote entry point created when webpack bundles the microfrontend:

<script src="http://localhost:3000/remoteEntry.js"></script>

This is just a tiny script bridging the gap between the shell and the microfrontend. As the result, the shell can now load the separately compiled FlightsModule using the path mfe1/Module:

Connecting the Shell and the Microfrontend

Evaluation and Outlook

With Module Federation we have a proper and official solution for building microfrontends with webpack and hence the Angular CLI for the first time.

It will be part of webpack 5 which was in BETA when writing this. As the current version of the CLI is still using webpack 4, we need a custom webpack build. This will hopefully change in the near future.

Also, we need to deal with the fact that shared libraries need to be referenced via dynamic imports. Their asynchronous behavior allows the infrastructure to load the libraries not already provided by the shell. As outlined, we can bypass this by using the ContainerReferencePlugin for the shell and by just dynamically importing the the bootstrapping logic in the microfrontends.

Don't Miss Anything!


Subscribe to our newsletter to get all the information about Angular.


* By subscribing to our newsletter, you agree with our privacy policy.

Aktuelle Blog-Artikel

Nur einen Schritt entfernt!

Stellen Sie noch heute Ihre Anfrage,
wir beraten Sie gerne!

Jetzt anfragen!