Setup with Angular CLI

Integrating Module Federation with Angular CLI promotes a modular development strategy, enabling dynamic code loading and sharing among various Angular projects. This guide thoroughly explores the necessary steps for this integration, with assistance from the @angular-architects/module-federation plugin.

Prerequisites

  • Angular CLI: Verify the installation of Angular CLI version 10 or higher in your development environment.
  • Plugin Installation: It's essential to have the @angular-architects/module-federation plugin installed in your project, as it enhances Angular CLI's capabilities to accommodate Module Federation.

Installation

Installing the Plugin:

To begin utilizing Module Federation, it's imperative to configure the Angular CLI to incorporate Module Federation during the build phase. Due to the CLI's encapsulation of Webpack, the introduction of a custom builder is required to unlock Module Federation's potential.

Provided by the @angular-architects/module-federation package, this custom builder facilitates the integration process. Start by using the ng add command to incorporate it into your projects:

ng add @angular-architects/module-federation --project shell --port 4200 --type host
ng add @angular-architects/module-federation --project mfe1 --port 4201 --type remote

For those utilizing Nx, the procedure deviates slightly. First, execute an npm install to acquire the library, followed by invoking the init schematic:

npm i @angular-architects/module-federation -D
ng g @angular-architects/module-federation:init --project shell --port 4200 --type host
ng g @angular-architects/module-federation:init --project mfe1 --port 4201 --type remote
TIP

The --type argument, introduced in version 14.3, meticulously ensures that only the requisite configuration is generated.

In our representation, the project labeled 'shell' encapsulates the code for the shell, whereas 'mfe1' denotes Micro Frontend 1. The execution of the aforementioned commands orchestrates several pivotal tasks:

  • Skeleton Generation: A rudimentary webpack.config.js skeleton is crafted to facilitate Module Federation integration.

  • Custom Builder Installation: A custom builder is installed, empowering Webpack within the CLI to utilize the generated webpack.config.js.

  • Port Assignment: A distinct port is allocated for the ng serve command, enabling simultaneous serving of multiple projects.

It's imperative to acknowledge that the generated webpack.config.js is a partial configuration solely dedicated to governing Module Federation. The remaining configurations are autonomously generated by the CLI, maintaining the usual workflow intact.

This setup lays down a robust foundation for harnessing Module Federation, enabling seamless code sharing and dynamic loading across your Angular CLI projects.

The Shell (Host) Configuration

In the realm of Module Federation, the Shell, also referred to as the Host, serves as a pivotal point of integration. This segment delineates the configuration of the Shell to support the lazy-loading of a FlightModule through routing.

Routing Configuration

Commence by defining the application routes, specifying a lazy-loaded FlightModule via a virtual path:

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

In this configuration, the path 'mfe1/Module' serves as a virtual representation, indicating that it does not physically exist within the Shell application. Instead, it acts as a reference to a module located in a separate project.

TypeScript Typing

To satisfy the TypeScript compiler, it's necessary to create a type definition for the virtual path:

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

This declaration guides the TypeScript compiler in understanding the virtual path, easing the import process.

Webpack Configuration

Further, instruct Webpack to resolve all paths prefixed with mfe1 to a remote project. This is achieved within the webpack.config.js file, generated earlier:

const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

module.exports = withModuleFederationPlugin({

   remotes: {
     "mfe1": "http://localhost:4201/remoteEntry.js",
   },

   shared: {
     ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
   },

});

In the remotes section, the path mfe1 is mapped to the remote micro-frontend, specifically to its remote entry point. This remote entry, generated by Webpack, contains crucial information for interacting with the micro-frontend.

For development purposes, hardcoding the remote entry's URL is sufficient. However, a dynamic approach is necessary for production environments. The concept of dynamic remotes is further explored in a dedicated documentation page on Dynamic Remotes.

  • The shared property specifies the npm packages to be shared between the Shell and the micro-frontend(s). By using the shareAll helper method, all dependencies listed in your package.json are shared. While this facilitates a quick setup, it may lead to an excessive number of shared dependencies, which could be a concern for optimization.
  • The combination of singleton: true and strictVersion: true settings instructs Webpack to throw a runtime error if there is a version mismatch between the Shell and the micro-frontend(s). Changing strictVersion to false would instead result in a runtime warning.
  • The requiredVersion: 'auto' option, provided by the @angular-architects/module-federation plugin, automatically determines the version from your package.json, helping to prevent version-related issues.

Configuring the Remote

The Micro-frontend, referred to as the Remote in the context of Module Federation, follows a structure similar to a standard Angular application. It includes specific routes within the AppModule and contains a FlightsModule for handling flight-related functionalities. This section details the steps to ensure the FlightsModule is seamlessly loaded into the Shell (Host).

Route Definition

Begin by establishing the basic routes within the AppModule:

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

This simple routing setup navigates to a HomeComponent when the application is accessed.

Module Creation

Proceed to create a FlightsModule to handle flight-related operations:

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

This module contains a route to a FlightsSearchComponent defined as follows:

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

Exposing Modules via Webpack Configuration

To enable the loading of FlightsModule into the Shell, it's imperative to expose it through the Remote's Webpack configuration:

const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

module.exports = withModuleFederationPlugin({

   name: 'mfe1',

   exposes: {
     './Module': './projects/mfe1/src/app/flights/flights.module.ts',
   },

   shared: {
     ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
   },

});

In this configuration:

  • The name property identifies the micro-frontend as mfe1.
  • The exposes property signifies the exposure of FlightsModule under the public name Module, allowing its consumption by the Shell.
  • The shared property lists the libraries to be shared with the Shell, using the shareAll method to share all dependencies found in your package.json. The singleton: true and strictVersion: true properties ensure that a single version of shared libraries is used, and a runtime error is triggered in case of version incompatibility, respectively.

Testing the Configuration

Having set up the Shell (Host) and Micro-frontend (Remote), it's time to test the configuration to ensure the seamless integration of Module Federation.

Running the Applications

1. Starting the Shell and Micro-frontend:

Kickstart both the Shell and micro-frontend using the following commands:

ng serve shell -o
ng serve mfe1 -o

Upon executing these commands, the Shell and Micro-frontend will be served, and the respective applications will open in your default web browser.

2. Loading the Micro-frontend:

Navigate to the Flights section in the Shell, and observe the Micro-frontend being dynamically loaded.

The plugin also installs a handy npm script run:all during the ng-add and init schematics, allowing for simultaneous serving of all applications:

npm run run:all

For serving selected applications, append their names as command line arguments:

npm run run:all shell mfe1

A Closer Look at Main.ts

Delving into the main.ts file, you might notice a slight deviation from the usual:

import('./bootstrap')
    .catch(err => console.error(err));

The standard code typically found in main.ts has been migrated to a newly created bootstrap.ts file. This strategic move, orchestrated by the @angular-architects/module-federation plugin, enhances the Module Federation's ability to make informed decisions about library versions at runtime. The use of asynchronous dynamic imports enables Module Federation to accurately determine and load the correct versions of shared libraries.

Optimizing Dependency Sharing

While the initial setup with shareAll offers a straightforward and operational configuration, it may lead to the creation of unnecessarily large shared bundles. For a more refined approach to managing shared dependencies, it's advisable to shift from using shareAll to employing the share helper. This allows for more precise control over which dependencies are shared:

// Replace shareAll with share:
const { share, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

module.exports = withModuleFederationPlugin({

    // Specify the packages to share:
    shared: share({
        "@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
        "@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
        "@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
        "@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
    }),

});

In this configuration, the share helper allows for explicit sharing of selected packages, enabling a more optimized bundle sharing, and a potential reduction in the load times.