the/experts. Blog

Cover image for Microfrontends with Angular and Module Federation (webpack)
rogerdejager
rogerdejager

Posted on • Updated on

Microfrontends with Angular and Module Federation (webpack)

Large enterprise solutions/businesses all run into the same problem, having large complex software monoliths for years where the codebase keeps growing. Backend developers already started taking action by implementing microservices, but frontend developers were still lacking a clean solution. Currently, many micro frontend solutions are using web components, but they might feel overcomplicated if you are more framework focussed/oriented.

Luckily for us, Webpack 5 now introduces: ModuleFederationPlugin. Which is a chunk of code that allows applications to run Javascript modules at runtime. It enables cross-team development while still bringing it together as a Single Page Application. But every tool has its pros and cons, so does Module Federation. So let's first check whether Module Federation is a tool for you.

Pros for Module Federation

Next to Module Federation there are also other options to create micro frontend architectures (web components or iFrame implementation). This is why you should consider all options. This architectural decision will be affecting your project in a long-term scope after all.

Module Federation is an excellent technology if:

  • You have a complex project that is expanding fast but without structure.
  • You can split your project into multiple self-sufficient parts
  • The project can be developed between different teams.
  • You are using a frontend framework (Angular, Vue, React) for the long term.

Cons of Module Federation

Next to the pros, there are also some cons which you should consider.
When some of these conditions are true you should probably search for a different solution for your micro frontend architecture.
Module Federation should not be used if:

  • Your application is a small business process
  • You are not sure and stable with your framework and major versions
  • One of the micro frontends want to switch to a different framework
  • You do not use Webpack 5 as a module bundler
  • Your application is on an old Angular version < 11.0.0

Activating Module Federation for Angular Projects

To enable Module Federations in your Angular project you need to make sure the shell and the micro frontends are in the same Angular workspace. We need to make sure the CLI uses module federation when building, this is accomplished by a custom builder.

The package @angular-architects/module-federation provides this custom builder. To add this builder type:

ng add @angular-architects/module-federation --project shell --port 5000 
ng add @angular-architects/module-federation --project mfe1 --port 3000
Enter fullscreen mode Exit fullscreen mode

The shell contains the shell code and mfe1 will contain the Micro Frontend 1 code.
The ng add does 3 things, it:

  • Generates a webpack.config.js to use module federation
  • Installing a customer builder to let the CLI use the webpack.config.js
  • Assigning a port for each project so both projects can be served together.

The Shell (aka Host)

Let me start explaining the shell, which can also be called the host in Module Federation. The shell uses the router to lazy load a micro frontend module.

app.routes.ts

export const APP_ROUTES: Routes = [ 
{ 
   path: '', component: HomeComponent, pathMatch: 'full' 
}, 
{ 
   path: 'games', loadChildren: () => 
   import('mfe1/Module').then(m => m.GamesModule) 
}, 
];
Enter fullscreen mode Exit fullscreen mode

Note that we are importing mfe1/Module which does not exist within the shell project. It’s a virtual path pointing towards our remote module. Since the module does not exist we need to ease our TypeScript compiler by declaring the module manually.

decl.d.ts

declare module 'mfe1/Module';
Enter fullscreen mode Exit fullscreen mode

Next up is telling webpack that all our paths starting with mfe1 are pointing towards this remoteModule. So lets adjust our webpack.config.js

webpack.config.js(shell)

module.exports = { 
  output: { 
     uniqueName: "shell", 
     publicPath: "auto" 
  }, 
  optimization: { 
     runtimeChunk: false 
  }, 
  resolve: { 
     alias: { 
        ...sharedMappings.getAliases(), 
     } 
  }, 
  plugins: [ 
     new ModuleFederationPlugin({ 
         remotes: { 
          "mfe1": "mfe1@http://localhost:3000/remoteEntry.js" 
         }, 
         shared: share({ 
           "@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, 
           "@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
           "@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
           "@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
           ...sharedMappings.getDescriptors() 
         }) 
     }), 
     sharedMappings.getPlugin() 
  ] 
};
Enter fullscreen mode Exit fullscreen mode

Let's start off by typing the uniqueName in the config. This will be generated by looking at your package.json settings. You can always set this manually to avoid name conflicts.
The remote section points to the path where the remote module can be found. This is a generated remoteEntry file when building your project. Webpack loads it at runtime to get all the information of your micro frontend. Note that this part is optional if you are using router-based micro frontends.

In the shared section you can share all libraries between the shell and the micro frontends. This will reduce the package size drastically since they are sharing the libraries. The strictVersion option makes Webpack emit an error when there are different incompatible versions found between the shell and micro frontend. The requiredVersion is an option to specify a strict version or can be set to auto to use the highest possible version available.

The micro frontend (aka Remote)

The micro frontend, also referred to as the remote application, is an ordinary Angular application. It has a default component and has its appModule with routing.

app.routes.ts

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

Next to the default route there is also a child route for the gamesModule

games.module.ts

@NgModule({ 
   imports: [ 
      CommonModule, 
      RouterModule.forChild(GAMES_ROUTES) 
   ], 
   declarations: [ 
        GamesSearchComponent 
   ] 
}) 
export class GamesModule { }
Enter fullscreen mode Exit fullscreen mode

This module has its own routes.

games.routes.ts

export const GAMES_ROUTES: Routes = [ 
  { 
    path: 'games-search', 
    component: GamesSearchComponent 
  } 
];
Enter fullscreen mode Exit fullscreen mode

To let the shell load the remoteModule (in this case the GamesModule) we need to reference the module inside the webpack.config.js.

webpack.config.js(mfe)

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); 
const mf = require("@angular-architects/module-federation/webpack"); 
const path = require("path"); 
const share = mf.share; 
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
    path.join(__dirname, './tsconfig.json'),
); 

module.exports = {
    output: {
        uniqueName: 'mfe1',
        publicPath: 'auto'
    },
    optimization: {
        runtimeChunk: false
    },
    resolve: {
        alias: {
            ...sharedMappings.getAliases(),
        }
    },
    plugins: [
        new ModuleFederationPlugin({

            name: 'mfe1',
            filename: 'remoteEntry.js',  // 2-3K w/ Meta Data
            exposes: {
                './Module': './projects/mfe1/src/app/games/games.module.ts',
            },

            shared: {
                '@angular/core': { singleton: true, strictVersion: true, requiredVersion: '^12.1.1' },
                '@angular/common': { singleton: true, strictVersion: true, requiredVersion: '^12.1.1' },
                '@angular/router': { singleton: true, strictVersion: true, requiredVersion: '^12.1.1' },
                '@angular/common/http': { singleton: true, strictVersion: true, requiredVersion: '^12.1.1' },

                // Uncomment for sharing lib of an Angular CLI or Nx workspace
                ...sharedMappings.getDescriptors()
            }

        }),
        sharedMappings.getPlugin(),
    ],
};
Enter fullscreen mode Exit fullscreen mode

This configuration of Webpack exposes the GamesModule under the public name Module. This way it is accessible for the shell.

Trying it out

To try it all out, let's start our shell and micro frontend together.

ng serve shell -o ng serve mfe1 -o
Enter fullscreen mode Exit fullscreen mode

Congratulations!

You just created a micro frontend within a shell that reloads during runtime. So start right away with those huge repo's and cut them up into nice microservice with fast CI/CD cycles!

Discussion (0)