The Microfrontend Revolution: Module Federation in Webpack 5

Module Federation allows loading separately compiled program parts. This makes it an official solution for implementing microfrontends.


This article is part of a series:

 

2020-10-13: Updated to use webpack 5

Important: This first part of the article series shows Module Federation with a simple "TypeScript-only example". If you look for an example also using Angular, please directly jump to the 2nd part of this series.

The Module Federation integrated in Webpack beginning with version 5 allows the loading of separately compiled program parts. Hence, it finally provides an official solution for the implementation of microfrontends.

Until now, when implementing microfrontends, you had to dig a little into the bag of tricks. One reason is surely that current build tools and frameworks do not know this concept. Module Federation initiates a change of course here.

It allows an approach called Module Federation for referencing program parts that are not yet known at compile time. These can be self-compiled microfrontends. In addition, the individual program parts can share libraries with each other, so that the individual bundles do not contain any duplicates.

In this article, I will show how to use Module Federation using a simple example. The source code can be found here.

Example

The example used here consists of a shell, which is able to load individual, separately provided microfrontends if required:

Shell with microfrontend

The shell is represented here by the black navigation bar. The micro front end through the framed area shown below. Also, the microfrontend can also be started without a shell

Microfrontends in standalone mode

This is necessary to enable separate development and testing. It can also be advantageous for weaker clients, such as mobile devices, to only have to load the required program part.

Concepts of Module Federation

In the past, the implementation of scenarios like the one shown here was difficult, especially since tools like Webpack assume that the entire program code is available when compiling. Lazy loading is possible, but only from areas that were split off during compilation.

With microfrontend architectures, in particular, one would like to compile and provide the individual program parts separately. In addition, mutual referencing via the respective URL is necessary. Hence, constructs like this would be desirable:

import('http://other-microfrontend');

Since this is not possible for the reasons mentioned, one had to resort to approaches such as externals and manual script loading. Fortunately, this will change with the Federation module in Webpack 5.

The idea behind it is simple: A so-called host references a remote using a configured name. What this name refers to is not known at compile time:

The host accesses the remote using a configured name

This reference is only resolved at runtime by loading a so-called remote entry point. It is a minimal script that provides the actual external url for such a configured name.

Implementation of the Host

The host is a JavaScript application that loads a remote when needed. A dynamic import is used for this.

The following host loads the component mfe1/component in this way -- mfe1 is the name of a configured remote and component the name of a file (an EcmaScript module) it provides.

const rxjs = await import('rxjs');

const container = document.getElementById('container');
const flightsLink = document.getElementById('flights');

rxjs.fromEvent(flightsLink, 'click').subscribe(async _ => {
    const module = await import('mfe1/component');
    const elm = document.createElement(module.elementName);
    […]    
    container.appendChild(elm);
});

Webpack would normally take this reference into account when compiling and split off a separate bundle for it. To prevent this, the ModuleFederationPlugin is used:

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

[…]
 output: {
      publicPath: "http://localhost:5000/",
      uniqueName: 'shell',
      […]
 },
plugins: [
  new ModuleFederationPlugin({
    name: "shell",
    library: { type: "var", name: "shell" },
    remoteType: "var",
    remotes: {
      mfe1: "mfe1"
    },
    shared: ["rxjs"]
  })
]

With its help, the remote mfe1 (Microfrontend 1) is defined. The configuration shown here maps the internal application name mfe1 to the same official name. Webpack does not include any import that now relates to mfe1 in the bundles generated at compile time.

Libraries that the host should share with the remotes are mentioned in the shared array. In the case shown, this is rxjs. This means that the entire application only needs to load this library once. Without this specification, rxjs would end up in the bundles of the host as well as those of all remotes.

For this to work without problems, the host and remote must agree on a common version.

In addition to the settings for the ModuleFederationPlugin, we also need to place some options in the output section. The publicPath defines the URL under which the application can later be found. This reveals where the individual bundles of the application but also their assets, e.g. pictures or styles, can be found.

The uniqueName is used to represents the host or remote in the generated bundles. By default, webpack uses the name from package.json for this. In order to avoid name conflicts when using Monorepos with several applications, it is recommended to set the uniqueName manually.

Loading Shared Libraries

For loading shared libraries, we must use dynamic imports:

const rxjs = await import('rxjs');

They are asynchronous and this gives webpack the time necessary to decide upon which version to use and to load it. This is especially important in cases where different remotes and hosts use different versions of the same library. In general, webpack tries to load the highest compatible version. More about negotiating versions and dealing with version mismatches this can be found in a later article of this series.

To bypass this issue, it's a good idea to load the whole application with dynamic imports in the entry point used. For instance, the Micro Frontend could use a main.ts which looks like this:

import('./component');

This gives webpack the time needed for the negotiation and loading the shared libraries when the application starts. Hence, in the rest of the application one can always use static ("traditional") imports like

import * as rxjs from 'rxjs';

Implementation of the Remote

The remote is also a standalone application. In the case considered here, it is based on Web Components:

class Microfrontend1 extends HTMLElement {

    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
    }

    async connectedCallback() {
        this.shadowRoot.innerHTML = `[…]`;
    }
}

const elementName = 'microfrontend-one';
customElements.define(elementName, Microfrontend1);

export { elementName };

Instead of web components, any JavaScript constructs or components based on frameworks can also be used. In this case, the frameworks can be shared between the remotes and the host as shown.

The webpack configuration of the remote, which also uses the ` ModuleFederationPlugin '', exports this component with the property exposes under the name component:

 output: {
      publicPath: "http://localhost:3000/",
      uniqueName: 'mfe1',
      […]
 },
 […]
 plugins: [
    new ModuleFederationPlugin({
      name: "mfe1",
      library: { type: "var", name: "mfe1" },
      filename: "remoteEntry.js",
      exposes: {
        './component': "./mfe1/component"
      },
      shared: ["rxjs"]
    })
]    

The name component refers to the corresponding file. In addition, this configuration defines the name mfe1 for the remote. To access the remote, the host uses a path that consists of the two configured names, mfe1 and component. This results in the instruction shown above:

import('mfe1/component')

However, the host must know the URL at which it finds mfe1. The next section shows how this can be accomplished.

Connect Host to Remote

To give the host the option to resolve the name mfe1, the host must load a remote entry point. This is a script that the ModuleFederationPlugin generates when the remote is compiled.

The name of this script can be found in the filename property shown in the previous section. The url of the microfrontend is taken from the publicPath property. This means that the url of the remote must already be known when it is compiled. Fortunately, there is already a PR which removed this need.

Now this script is only to be integrated into the host:

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

At runtime it can now be observed that the instruction

import('mfe1/component');

causes the host to load the remote from its own url (which is localhost:3000 in our case):

Laden des Remotes von anderer Url

Conclusion and Outlook

The Module Federation integrated in Webpack beginning with version 5 fills a large gap for microfrentends. Finally, separately compiled and provided program parts can be reloaded and already loaded libraries can be reused.

However, the teams involved in developing such applications must manually ensure that the individual parts interact. This also means that you have to define and comply with contracts between the individual microfrontends, but also that you have to agree on a version for each of the shared libraries.

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.

Unsere Angular-Schulungen

Aktuelle Blog-Artikel

Nur einen Schritt entfernt!

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

Jetzt anfragen!