0%

angular-module-federation

Introduction

This article will introduce Angular Module Federation, a new feature in Webpack 5 that allows you to share code between Angular applications.

Module Federation is a new feature in Webpack 5 that allows you to share code between applications. It enables your application to load remote modules at runtime from another application and share dependencies between applications. This feature is particularly useful for micro-frontends, where you have multiple applications that need to share code.

Steps

Create an empty angular workspace

Option --no-create-application is used to create an empty workspace without an initial application.

1
ng new angular-module-federation --no-create-application

Generate shell application shell

1
2
cd angular-module-federation
ng generate application shell

Generate remote application mfe1

1
ng generate application mfe1

Add plugin @angular-architects/module-federation to shell app

1
ng add @angular-architects/module-federation --project shell --port 4200 --type host

This command will do the following:

  1. Add @angular-architects/module-federation and ngx-build-plus to file package.json under workspace root.

  2. Add command "run:all": "node node_modules/@angular-architects/module-federation/src/server/mf-dev-server.js" to scripts section in file package.json. This command will start the module federation development server, which will run app shell and app mfe1 at the same time.

  3. Add file bootstrap.ts under projects/shell/src folder, its content was copied from main.ts

    1
    2
    3
    4
    5
    6
    import { bootstrapApplication } from '@angular/platform-browser';
    import { appConfig } from './app/app.config';
    import { AppComponent } from './app/app.component';

    bootstrapApplication(AppComponent, appConfig)
    .catch((err) => console.error(err));
  4. Update file projects/shell/src/main.ts to import bootstrap.ts(Note, when a file was imported, it was executed automatically)

    1
    import './bootstrap';
  5. Add file webpack.config.js under projects/shell folder,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
    module.exports = withModuleFederationPlugin({
    remotes: {
    "mfe1": "http://localhost:4200/remoteEntry.js",
    },
    shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
    },
    });
  6. Add file webpack.prod.config.js under projects/shell folder,

    1
    module.exports = require('./webpack.config');
  7. Update file angular.json under workspace root.

    1. projects/shell/architect/build/builder: "@angular-devkit/build-angular:application" —> ngx-build-plus:browser - The reason for this change is that module federation is a feature provided by WebPack, but "@angular-devkit/build-angular:application" is for ESBuild, so we need to change it to ngx-build-plus:browser which is for WebPack. see here for details.
    2. projects/shell/architect/build/builder/extraWebpackConfig: "projects/shell/webpack.config.js"
    3. projects/shell/architect/build/configurations/production/extraWebpackConfig: "projects/shell/webpack.prod.config.js"
    4. projects/shell/architect/serve/configurations/extraWebpackConfig: "projects/shell/webpack.prod.config.js"
    5. projects/shell/architect/serve/options:
      1
      2
      3
      4
      5
      "options": {
      "port": 4200,
      "publicHost": "http://localhost:4200",
      "extraWebpackConfig": "projects/shell/webpack.config.js"
      }

Note that: extraWebpackConfig is an option from ngx-build-plus, it allows you to add additional webpack configuration to the project.

Add plugin @angular-architects/module-federation to mfe1 app

1
ng add @angular-architects/module-federation --project mfe1 --port 4201 --type remote

This command will do the same thing as the previous command, but for the mfe1 app with a little difference.

  1. webpack.config.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

    module.exports = withModuleFederationPlugin({
    name: 'mfe1',
    exposes: {
    './Component': './projects/mfe1/src/app/app.component.ts',
    },
    shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
    },
    });
  2. Serve options in angular.json
    1
    2
    3
    4
    5
    "options": {
    "port": 4201,
    "publicHost": "http://localhost:4201",
    "extraWebpackConfig": "projects/mfe1/webpack.config.js"
    }

Add home component to shell app

1
ng generate component home --project=shell

Add product component to mfe1 app

app mfe1 will expose a component product to shell app.

1
ng generate component product --project=mfe1

Update webpack.config.js in app mfe1

We’ll expose the product component to the shell app.

1
2
3
4
5
6
7
8
9
10
11
const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

module.exports = withModuleFederationPlugin({
name: 'mfe1',
exposes: {
'./Component': './projects/mfe1/src/app/product/product.component.ts',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});

Update app.routers.ts in app mfe1

1
2
3
4
5
6
export const routes: Routes = [
{
path: 'product',
component: ProductComponent,
}
];

Update webpack.config.js in app shell

Note the generated port in webpack.config.js, its value is 4200, we should change it to 4201 since mfe1 runs on this port.

1
2
3
4
5
6
7
8
9
const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
remotes: {
"mfe1": "http://localhost:4201/remoteEntry.js", // <--- 4201
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});

Update app.routes.ts in app shell

Add a route path product to load the product component remotely from mfe1.

1
2
3
4
5
6
7
8
9
10
export const routes: Routes = [
{
path: 'home',
component: HomeComponent,
},
{
path: 'product',
loadChildren: () => import('mfe1/Component').then(m => m.ProductComponent),
}
];

Add typescript declaration file decl.d.ts under projects/shell/src

If you don’t add this file, you will get an error when you import the remote module mfe1 in the shell app.

1
declare module 'mfe1/Component';

Workspace structure

Here is the file structure of the workspace till now.

1
2
3
4
5
6
7
8
9
10
11
12
angular-module-federation/
├─ projects/
│ ├─ mfe1/
│ │ ├─ src/
│ │ ├─ webpack.config.js
│ │ ├─ webpack.prod.config.js
│ ├─ shell/
│ │ ├─ src/
│ │ ├─ webpack.config.js
│ │ ├─ webpack.prod.config.js
├─ angular.json
├─ package.json

Start the development server

You can run shell and mef1 app separately by running the following command.

1
2
ng serve shell - o
ng serve mfe1 - o

Or run them at the same time by running the following command.

1
npm run run:all

Change the shell app url in browser to localhost:4200/product, you will see the product component from mfe1 app.

Remote entry file

If you look into the webpack.config.js file in the shell app, you will see the following configuration.

1
2
3
remotes: {
"mfe1": "http://localhost:4200/remoteEntry.js",
},

Here we use remoteEntry.js as the entry point of the remote expose module, this is the default file name, However, if you want to change it, you can update the configuration in the webpack.config.js file in the mfe1 app.

1
2
3
4
module.exports = withModuleFederationPlugin({
// ...
filename: 'myEntryName.js',
});

and then update the configuration in the webpack.config.js file in the shell app.

1
2
3
4
5
6
module.exports = withModuleFederationPlugin({
// ...
remotes: {
"mfe1": "http://localhost:4200/myEntryName.js",
},
});

namedChunk

When you run the shell app, switch to network tab in the browser’s developer tool, you will see the response of the javascript file name as a random string, that’s because Webpack generates a random name for the chunk file, you can change it to a named chunk by adding the following configuration in the angular.json file in the mfe1 app.(under architect | build | configuration | development)

1
2
3
4
5
module.exports = withModuleFederationPlugin({
// ...
filename: 'myEntryName.js',
namedChunk: true,
});

Dynamic loading

You can remove the remotes part in app shell’s webpack.config.js file, and update the app.routes.ts file in the shell app to load the remote module dynamically.

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

You can found the options of withModuleFederationPlugin in WebPack’s official documentation here, unfortunately, some property was even not mentioned in this document, you can found them in the source code of the plugin(schema file) from here

loadRemoteEntry vs loadRemoteModule

loadRemoteEntry is used to load the remote entry file, and loadRemoteModule is used to load the remote module.

loadRemoteEntry is not required, but nice to have, here is the description from github
If somehow possible, load the remoteEntry upfront. This allows Module Federation to take the remote’s metadata in consideration when negotiating the versions of the shared libraries.

For this, you could call loadRemoteEntry BEFORE bootstrapping Angular:

1
2
3
4
5
6
7
8
9
10
11
12
// main.ts
import { loadRemoteEntry } from '@angular-architects/module-federation';

Promise.all([
loadRemoteEntry({
type: 'module',
remoteEntry: 'http://localhost:3000/remoteEntry.js',
}),
])
.catch((err) => console.error('Error loading remote entries', err))
.then(() => import('./bootstrap'))
.catch((err) => console.error(err));

References

  1. Mono repo with Angular CLI: https://angular.dev/reference/configs/file-structure#multiple-projects
  2. Module federation with Angular: https://module-federation.io/practice/frameworks/angular/angular-cli.html
  3. https://github.com/manfredsteyer/ngx-build-plus/