avatarLovepreet Singh

Summary

This article delves into the concept of Module Federation in Webpack 5, explaining how it allows separately compiled chunks to be loaded and shared dependencies to be used, facilitating micro-frontend development.

Abstract

The article begins by introducing Module Federation, a feature introduced in Webpack 5 that enables micro-frontend development by allowing separately compiled JavaScript code to be loaded and used together. The author explains the difference between static and dynamic module federation and provides a simple example of two applications, Remote and Host, to illustrate how Module Federation works. The Remote application, which finds the sum of numbers, exposes its functionality to the Host application, which needs to find the average of numbers. The author then explains the concepts of Module Federation, such as Remote (expose), Host (consume), and Remote Entry, and provides a detailed walkthrough of configuring Module Federation on both the Remote and Host sides. The article concludes by mentioning upcoming articles on dynamic module federation, managing shared libraries, and implementing Module Federation in Angular.

Bullet points

  • Module Federation is a feature introduced in Webpack 5 that allows separately compiled JavaScript code to be loaded and used together, facilitating micro-frontend development.
  • Module Federation enables apps to split into smaller pieces that can be compiled and bundled independently but can be loaded and used together.
  • Module Federation is not a Microfrontend Framework but a tool to help build Microfrontends.
  • An app can both be a remote and a host, and how a host consumes a remote leads to two types of module federation: static and dynamic.
  • Static module federation occurs when the URL of the remote is known at compile time, while dynamic module federation occurs when the location of the remote is only known at runtime.
  • The author provides a simple example of two applications, Remote and Host, to illustrate how Module Federation works.
  • The Remote application exposes its functionality to the Host application, which needs to find the average of numbers.
  • The author explains the concepts of Module Federation, such as Remote (expose), Host (consume), and Remote Entry.
  • The author provides a detailed walkthrough of configuring Module Federation on both the Remote and Host sides.
  • The article concludes by mentioning upcoming articles on dynamic module federation, managing shared libraries, and implementing Module Federation in Angular.

Webpack Module Federation: Clearly Explained With a Simple Example

This is part 3 of the series on Module Federation.

  1. Module Federation Concepts: Clearly Explained with an Example
  2. Module Federation using Angular Standalone Components — In 6 Steps
  3. Dynamic Module Federation in Angular

In October 2020, Webpack 5 was officially released, and with it came a new ground-breaking concept and a feature called Module Federation. This article will delve into Module Federation through a straightforward example. Instead of relying on any framework or third-party plugins to use module federation, I will create examples from scratch to better explain how module federation works. At the end of the article, you will have a solid understanding of this concept. You can find the source code used in this article here.

What is Module Federation?

Webpack, as we all know, is used to compile and bundle Javascript code. It also helps to break our code into various small chunks that we can load at runtime when required. Before Webpack 5, the loading of chunks was restricted to those that had been compiled and bundled together.

However, Webpack 5 changed everything by introducing a plugin that allows loading separately compiled chunks. Not only that, module federation also allows apps to share dependencies instead of loading them multiple times. This feature, known as module federation, enabled developers to split their apps into smaller pieces that can be compiled and bundled independently but can be loaded and used together, facilitating micro-frontend development.

Update: Module Federation will no longer be a part of Webpack but a separate library. And there is also some work on Native Federation.

Loading separately compiled chunks using module federation

People often confuse module federation as a micro-frontend framework. Still, the sole purpose of module federation is to allow separately compiled javascript code to be loaded, whether it is a full-fledged application module or a simple function.

Module Federation is not a Microfrontend Framework, but a tool to help building Microfrontends.

Now, that we understand what module federation is, let’s look into an example to see how it works internally.

A Tale of Two Applications

Let’s assume there is an application named Remote whose purpose is to find the sum of numbers. The numbers are given by the user in a comma-separated string. Remote is converting this string to an array, finding the sum of this array, and displaying the sum to the user. The application is working happily until…

One day, an application named Host approached Remote explaining that it needed to find the average of numbers. However, the numbers are provided as a comma-separated string by the user and the Host doesn’t know how to convert it to an array for finding the sum. The host asked if it could somehow use the same functionality that the Remote is using for converting string to an array.

Sharing modules between applications using module federation

Let’s look at how module federation can help us to achieve this request. But first, let’s have a look at some concepts of module federation.

Concepts of Module Federation

Before we start using module federation, we need to know the following two terms.

Remote — Expose

An app can decide what javascript parts it wants to expose so that other apps can use these parts. An app that exposes something is called remote in module federation terms.

Host — Consume

Declare what an app wants to consume and from where to consume this part or the URL of the remote. An app that consumes something is called a host.

Remote Entry

When a remote exposes some javascript parts, a remote entry file is generated that contains minimal information needed to consume these exposed parts.

Host consumes the exposed modules using remote entry file

An app can both be a remote and a host as well. How a host consumes a remote leads to two types of module federation.

Static Module Federation

When the URL of the remote is known at compile time, it is known as static module federation.

Dynamic Module Federation

If the location of the remote is only known at runtime, then we call it a dynamic module federation.

Both will behave in the same manner except for some version conflict resolution changes, which we will see in another article.

Don’t worry if you didn’t get these exactly, we will go into more details later.

Applications Structure (Optional)

I have kept the applications simple to keep the focus on the module federation concept. The Webpack entry point(where the bundling starts) is the main.js which is asynchronously importing utils.js. We are loading it async to make sure that shared libraries are available before we run utils.js code.

The source code is available here.

Application structure for both the remote and host

The utils.js has helper methods for the application. We are using the loadash-es library to find the sum of the array.

The above architecture is the same for both the Remote and Host. The only difference is in the content of the utils.js file and the webpack.config.js file. Although I am a simple Webpack config and code, I have commented out most of the things for explanation.

Configure Module Federation — Remote Side

We will now start module federation configuration from the remote side. As we read above, we need to expose certain parts that the host can consume. Let’s look at the Webpack configuration needed to expose certain parts.

// File - webpack.config.js

new ModuleFederationPlugin({
  name: "remote",
  filename: "remoteEntry.js",
  exposes: {
    "./utils": "./utils",
  },
  library: {
    type: "module",
  },
  shared: ["lodash-es"],
})

We are using ModuleFederationPlugin to enable module federation in the app. It has the following configuration. Name: The unique name given to this app. Filename: The remote entry filename that Webpack will generate. Exposes: Expose given files from this remote. Library: Whether to expose the files as a module, script, var, etc. Shared: Libraries that remote wants to share with the host(will go into more details in another article).

The most important part here is the exposes config. We are exposing the utils file and exposing it as ‘utils’. We can expose it with any name we want and the host can use this name to consume. Let’s also quickly look into the utils.js file.

Webpack will create separate chunks for each shared library in the dist folder, which leads to a drawback also as tree shaking is no longer working.

// File - utils.js

// This is exported for consumers for this remote
export function stringToArray(str) {
  return str.split(",").map((n) => parseInt(n));
}

// Rest of the code

Because the host wants the string-to-array functionality, we are exporting this function from the utils.js file.

If you will now run the following command and see the dist folder, you will find a remoteEntry.js file.

npm run build

This file contains two main parts: The Webpack runtime and the information about the exposed parts. The runtime is used to consume the remote and share libraries between the remote and the host. Again, a topic for another article.

Let’s check the important part that contains information on the exposed parts.

// File - remoteEntry.js
var moduleMap = {
 "./utils": () => {
  return __webpack_require__.e(672).then(() => (() => ((__webpack_require__(672)))));
 }
};
var get = (module, getScope) => {
 // Code to get a module from this remote
};
var init = (shareScope, initScope) => {
 // Code to initialize this remote container
};

// ....
// Finally expose get and init method for the host
var __webpack_exports__get = __webpack_exports__.get;
var __webpack_exports__init = __webpack_exports__.init;
export { __webpack_exports__get as get, __webpack_exports__init as init };

You can see that the module map contains the file that we are exposing and the promise to load this file for the remote.

The init and get functions are used to initialize this remote container and then get the required module from this initialized container.

Another important part here is the scope, which is ‘default’ by default :), containing all the shared modules between different remotes and the host. When initializing, the Webpack runtime decides whether the remote must put its shared libraries in the shared scope as the required version might already be available. We will go into much detail about sharing libraries in another article.

Configure Module Federation — Host Side

Now, it is time for the host to consume the exposed parts by the remote. So, let's look into the Webpack config file of the host.

new ModuleFederationPlugin({
  name: "remote",
  filename: "remoteEntry.js",
  remotes: {
    remote: "http://localhost:3001/remoteEntry.js",
  },
  library: {
    type: "module",
  },
  shared: ["lodash-es"],
}),

Almost all the configuration is the same as remote except the remotes and exposes part. The host doesn’t contain any exposed parts but it contains the remotes config object. The object declares the name of the remote it wants and from where to find that remote. We are pointing to the remoteEntry.js file that contains the information to load remote modules.

We are using static module federation here, so directly giving the URL of the remote in the config. I will have another article going into dynamic module federation as the purpose of this article is to clarify concepts of the module federation.

Now let’s look at our utils.js file that is using the remote exposed module.

// File - utils.js

// Here, we are importing the remote module
const { stringToArray } = await import("remote/utils");
import { sum } from "lodash-es";

window.calculateAverage = function () {
  const str = document.getElementById("input").value;
  // Use external function to convert string to array
  const arr = stringToArray(str);
  const average = sum(arr) / arr.length;

  document.getElementById("result").innerText = average;
};

You can see at the first line, we are asynchronously loading the exposed module by the remote. We are using the dynamic import function to load the module. The format of the import path is like the following

import("{{NAME_GIVEN_BY_THE_HOST}}/{{EXPOSED_MODULE_BY_REMOTE}}");

And that is all, you can see how the host can load the remote function like it is part of the same application. Now, if we look at the network tools of the browser, you will see we are loading the remoteEntry.js file and various other chunks.

Host loading remote entry file and exposed modules

Another important thing to notice here is that we are only loading the lodash-es library chunk once as both the host and the remote have the same version. Again, we will look into different strategies that Webpack is using for managing shared libraries in another article.

Upcoming Articles

  1. Dynamic Module Federation
  2. How Module federation is managing shared libraries? And what will happen when there are different versions of shared libs?
  3. Implement Module federation in Angular from scratch using only the Webpack plugin.
  4. Module Federation using web components and the problems associated.
  5. …. Still Deciding

Thank you for reading till the end!

Please subscribe and stay tuned for the next article.

JavaScript
Webpack
Module Federation
Angular
Micro Frontends
Recommended from ReadMedium