/angular-module-federation

Demonstrations of Microfrontends in Angular

Primary LanguageTypeScript

Angular Module Federation

Demonstration of a distributed deployment architecture connected together in the frontend with webpack Module Federation.

Getting Started

  1. Install dependencies (make sure to use yarn because otherwise you won't get webpack@5)
    yarn
  2. Start serving
    yarn run serve
  3. Go to http://localhost:3000

Capabilities

Angular CLI & webpack

To extend the Angular CLI with a webpack configuration the @angular-builders/custom-webpack package is used. With it you can add a custom webpack configuration to the angular.json which extends the generated by the CLI.

{
  "build": {
    "builder": "@angular-builders/custom-webpack:browser",
    "options": {
      "customWebpackConfig": {
        "path": "projects/mf1/extra-webpack.config.ts"
      },
      ...
    }
  }
}

Shell

The shell display a toolbar with 2 links to the Microfrontends.
It defines two routes which load the remoteEntry.js file and the Angular module and its dependencies subsequently.

[
  {
    path: 'cathedrals',
    loadChildren: () => loadRemoteModule('http://localhost:3000/remoteEntry.js', 'mf1', 'Module')
      .then(module => module.CathedralModule)
  },
  {
    path: 'cities',
    loadChildren: () => loadRemoteModule('http://localhost:4000/remoteEntry.js', 'mf2', 'Module')
      .then(module => module.CityModule)
  }
]

You could of course set the routes dynamically by getting the information from a service registry or some form of configuration or whatever you please.

Module Federation Configuration

The configuration of the ModuleFederationPlugin is very simple because the Shell does not expose any modules and all remotes are dynamically loaded. It only contains the definition of the shared modules.

new ModuleFederationPlugin({
  shared: {
    '@angular/core': { eager: true },
    '@angular/common': { eager: true },
    '@angular/common/': { eager: true },
    '@angular/router': { eager: true },
    '@angular/cdk': { eager: true },
    '@angular/cdk/': { eager: true },
    '@angular/material': { eager: true },
    '@angular/material/': { eager: true },
    rxjs: { eager: true },
    'rxjs/': { eager: true }
  }
})

Microfrontend 1

Microfrontend 1 demonstrates that it is possible to have more than just one route in the dynamic module. It also includes some dependencies which are not already loaded in the shell and shows that it can load them dynamically. One more point is the loading of assets which is currently implemented by adding a mf1/ prefix to the image sources. To make that work the shell needs a proxy which points to http://localhost:3000 which is where the mf1 is configured to run in the serve target.

Module Federation Configuration

The configuration of the ModuleFederationPlugin contains the shared modules again and the points to the Module which it wants to be exposed.

new ModuleFederationPlugin({
  name: 'mf1',
  library: { type: 'var', name: 'mf1' },
  filename: 'remoteEntry.js',
  exposes: {
    Module: './projects/mf1/src/app/cathedral/cathedral.module.ts'
  },
  shared: [
    '@angular/core',
    '@angular/common',
    '@angular/common/',
    '@angular/router',
    '@angular/cdk/',
    '@angular/material/',
    'rxjs',
    'rxjs/'
  ]
})

Microfrontend 2

Microfrontend 2 demonstrates nothing more that Microfrontend 1 but shows that when two different microfrontends need the same dependency, which is also defined as shared, that whichever microfrontend is loaded first will load this dependency and upon change of microfrontend the module is not loaded again.
To see the effect take a look at the network console in your Browser and check that a JavaScript file with a name similar to node_modules_angular_material___ivy_ngcc___fesm2015_icon_js.js is only loaded for the first microfrontend.

Module Federation Configuration

The configuration is very similar to microfrontend 1 with the only difference being in the name and exposed module.$

new ModuleFederationPlugin({
  name: 'mf2',
  library: { type: 'var', name: 'mf2' },
  filename: 'remoteEntry.js',
  exposes: {
    Module: './projects/mf2/src/app/city/city.module.ts'
  },
  shared: [
    '@angular/core',
    '@angular/core',
    '@angular/common',
    '@angular/common/',
    '@angular/router',
    '@angular/cdk',
    '@angular/cdk/',
    '@angular/material',
    '@angular/material/',
    'rxjs',
    'rxjs/'
  ]
})

Limitations

One could build all three projects with ng build and serve them with a webserver, but one would need a proxy to make the images of mf1 work. See proxy.conf.js