0%

angular-module-federation-deep-dive

Introduction

In this article, we will explore the Angular Module Federation feature in depth. Please read the first article in this series to get a basic understanding of Module Federation.

remoteEntry.js

Each time you call loadRemoteModule, Angular will fetch the remoteEntry.js file first from the remote server. What is this file? Let’s take a look at the remoteEntry.js file in the remote project.

You can find the remoteEntry.js file in the dist folder of the remote project. The file is generated by the ModuleFederationPlugin in the webpack configuration. But the file under the dist folder is minimized and hard to read. You can read it on Chrome network tab when you call loadRemoteModule.

This file is very large and contains lots of code, you can scroll down to the bottom or search container entry to find key part.

remoteEntry.js mainly contains the following information:

1. The list of modules that are exposed by the remote project.

In the following example, the remote project exposes a module named ./Component from its ./projects/mfe1/src/app/product/product.component.ts file.

1
2
3
4
5
var moduleMap = {
"./Component": () => {
return __webpack_require__.e("projects_mfe1_src_app_product_product_component_ts").then(() => (() => ((__webpack_require__(/*! ./projects/mfe1/src/app/product/product.component.ts */ 1585)))));
}
};

2. The list of shared libraries.

From the following code in remoteEntry.js the remote project shares the common angular and rxjs libraries.

1
2
3
4
5
6
7
8
9
10
11
12
13
switch(name) {
case "default": {
register("@angular/common/http", "17.3.0", () => (__webpack_require__.e("node_modules_angular_common_fesm2022_http_mjs-_f9340").then(() => (() => (__webpack_require__(6443))))));
register("@angular/common", "17.3.0", () => (__webpack_require__.e("node_modules_angular_common_fesm2022_common_mjs-_b4621").then(() => (() => (__webpack_require__(316))))));
register("@angular/core/primitives/signals", "17.3.0", () => (__webpack_require__.e("node_modules_angular_core_fesm2022_primitives_signals_mjs").then(() => (() => (__webpack_require__(5689))))));
register("@angular/core", "17.3.0", () => (__webpack_require__.e("node_modules_angular_core_fesm2022_core_mjs").then(() => (() => (__webpack_require__(7580))))));
register("@angular/platform-browser", "17.3.0", () => (__webpack_require__.e("node_modules_angular_platform-browser_fesm2022_platform-browser_mjs-_e89f0").then(() => (() => (__webpack_require__(436))))));
register("@angular/router", "17.3.0", () => (__webpack_require__.e("node_modules_angular_router_fesm2022_router_mjs-_5a221").then(() => (() => (__webpack_require__(5072))))));
register("rxjs/operators", "7.8.0", () => (__webpack_require__.e("node_modules_rxjs_dist_esm_operators_index_js").then(() => (() => (__webpack_require__(8219))))));
register("rxjs", "7.8.0", () => (__webpack_require__.e("node_modules_rxjs_dist_esm_index_js").then(() => (() => (__webpack_require__(845))))));
}
break;
}

Conclusion

  1. remoteEntry.js is a file that contains the list of modules that are exposed by the remote project.
  2. Angular fetches the remoteEntry.js file first before loading the remote module.
  3. The remoteEntry.js file is generated by the ModuleFederationPlugin in the webpack configuration.
  4. shell/host project does not have a remoteEntry.js file.

loadRemoteModule

How loadRemoteModule works in Angular Module Federation? Let’s take a look at the source code of the loadRemoteModule function in the @angular-architects/module-federation package.

The entry point is from the router file, we call loadRemoteModule function to load the remote module. The loadRemoteModule function is defined in the load-remote-module.ts file in the @angular-architects/module-federation package.

1
2
3
4
5
6
7
8
9
{
path: 'product',
loadComponent: () =>
loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:4201/remoteEntry.js',
exposedModule: './Component'
}).then(m => m.ProductComponent)
}

Here is the source code of the loadRemoteModule function from webpack:///node_modules/@angular-architects/module-federation-runtime/fesm2022/angular-architects-module-federation-runtime.mjs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
async function loadRemoteModule(optionsOrRemoteName, exposedModule) {
let loadRemoteEntryOptions;
let key;
let remoteEntry;
let options;
if (typeof optionsOrRemoteName === 'string') {
options = {
type: 'manifest',
remoteName: optionsOrRemoteName,
exposedModule: exposedModule,
};
}
else {
options = optionsOrRemoteName;
}
// To support legacy API (< ng 13)
if (!options.type) {
const hasManifest = Object.keys(config).length > 0;
options.type = hasManifest ? 'manifest' : 'script';
}
if (options.type === 'manifest') {
const manifestEntry = config[options.remoteName];
if (!manifestEntry) {
throw new Error('Manifest does not contain ' + options.remoteName);
}
options = {
type: manifestEntry.type,
exposedModule: options.exposedModule,
remoteEntry: manifestEntry.remoteEntry,
remoteName: manifestEntry.type === 'script' ? options.remoteName : undefined,
};
remoteEntry = manifestEntry.remoteEntry;
}
else {
remoteEntry = options.remoteEntry;
}
if (options.type === 'script') {
loadRemoteEntryOptions = {
type: 'script',
remoteEntry: options.remoteEntry,
remoteName: options.remoteName,
};
key = options.remoteName;
}
else if (options.type === 'module') {
loadRemoteEntryOptions = {
type: 'module',
remoteEntry: options.remoteEntry,
};
key = options.remoteEntry;
}
if (remoteEntry) {
await loadRemoteEntry(loadRemoteEntryOptions);
}
return await lookupExposedModule(key, options.exposedModule);
}

The key part is the last if branch in this file.
first: it load the remoteEntry.js file by calling loadRemoteEntry function.

1
2
3
if (remoteEntry) {
await loadRemoteEntry(loadRemoteEntryOptions);
}

loadREmoteEntry function then calls loadRemoteModuleEntry function to load the remote module.

1
2
3
4
5
6
7
8
9
10
// remoteEntry = 'http://localhost:4201/remoteEntry.js'
async function loadRemoteModuleEntry(remoteEntry) {
if (containerMap[remoteEntry]) {
return Promise.resolve();
}
return await import(/* webpackIgnore:true */ remoteEntry).then((container) => {
initRemote(container, remoteEntry);
containerMap[remoteEntry] = container;
});
}

We can see that it uses the import function to load the remoteEntry.js file. The import function is a dynamic import function that fetches the remoteEntry.js file from the remote server. After loading the remoteEntry.js file, it calls the initRemote function to initialize the remote container. This container is used to get the remote module later.

After loading the remoteEntry.js file, then it calls lookupExposedModule function to get the module from the remote project.

1
2
3
4
5
6
7
8
// key = 'http://localhost:4201/remoteEntry.js'
// exposedModule = './Component'
async function lookupExposedModule(key, exposedModule) {
const container = containerMap[key];
const factory = await container.get(exposedModule);
const Module = factory();
return Module;
}