Building a MicroFrontend setup using Angular 12: Part 2 — putting the pieces together

Jonathan Cardoz
7 min readJan 14, 2022

--

Photo by Sigmund on Unsplash

Disclaimer: This is my first set of articles on Medium and since I haven’t blogged or written anything (other than social media posts or restaurant reviews) for a couple of years, please bear with me. Comments are welcome.

I hope this set of articles helps you in understanding Micro Frontends (referred to as MFEs from now on), to setup your project and not make the same set of mistakes that my team and I did.

The other articles in the series are linked below

Setting up the shell (host) and other MFEs (remotes)

TL;DR:

Create a host and a remote MFE. The shell/host MFE is used to contain the other remote MFEs. You can refer to the code from the repository here

For the purpose of this example and for our real-world solutions as well — we need to have a host MFE and other MFEs — called remotes. Simply put, the host is the MFE application which is what contains or hosts the other applications. The host is also the application which the user interacts with — and loads initially.

The remote is the application which is loaded into the host — using a combination of module federation plugin and Angular’s module loading technique.

Using the microFrontend builder to extend our setup

TL;DR:

We use the Angular Architects package to extend our setup and turn it into an MicroFrontend one

Now that we have setup our application using an Angular workspace, and are able to run our projects locally, we need to include the MicroFrontend part into our projects. In order for us to use the build setup without modifying (read ejecting) the project Webpack configuration, we use the custom builder by Angular Architects. This enables us to still use the different Angular commands (ng build, ng run etc.) while also allowing us to configure the MFE part.

In order to use the builder, we need to include it as a dependency. Run the following CLI command from your terminal:

ng add @angular-architects/module-federation

This will add the dependency into your package.json file

...
"dependencies": {
"@angular-architects/module-federation": "14.0.1",...

You should have two applications in your workspace — in order to run this next command — the application names are shell and app1 . Part 1 has steps on how to add applications to your workspace.

To setup the two applications, run the following commands:

ng add @angular-architects/module-federation --project shell --port 8000ng add @angular-architects/module-federation --project app1 --port 8001

After running this command — you should see a couple of changes to certain files in your workspace.

The angular.json file would have been modified to include the new builder

builder type updated in the angular.json file

Similarly, package.json gets an update which shows a new run:all command

In addition to making updates, there is a webpack.config.js and a webpack.config.prod.js file added to the root of both shell and app1 projects

webpack.config.js added to the project root

Note: Even after running this command, both your applications should run as usual when you run the ng serve command.

Configure the two applications to work as a shell and remote (Static Federation)

TL;DR:

Refer to the Github link to review the setup along with the finished configuration.

I am going to split this section further into two sub-sections: one for configuring the shell application and the other for configuring the remote application app1. So let’s jump into it.

To configure the shell app, you will need to make edits to both the routing and the webpack.config.js file. We also need to add a type file.

Add the following line to the remotes section within the webpack.config.js file. Note: the port number should be the same one specified earlier for your app1. If you are serving app1 on port 3000, use 3000 here as well.

remotes: {"app1": "http://localhost:8001/remoteEntry.js"},

remoteEntry.js is the file name which will be emitted by app1, and is used to identify the remote by shell. This is Static Federation, as mentioned in the section title — telling the shell about the remote before hand.

For routing, you need to add a specific route where the remote module will be loaded. Let’s use sample as the path.

In the app-routing.module.ts, add the following route:

{path: 'sample',loadChildren: () => import('app1/Module').then(m => m.SampleModule)}

Finally, add the following declaration file decl.d.ts in the src folder of the shell app. This is to appease the Typescript compiler.

declare module 'app1/Module';

Now that the shell app is configured, let’s configure the remote app, app1.

Here, you will need to create a module which will be the loaded by the shell app, setup routing for this module, and updating the webpack.config.js file

Create the sample module using the CLI command

ng generate module sample --project app1 --route sample --module app

Note: If you need more information about the command parameters check out the CLI reference here. This should add the sample folder to your project. The project structure should be something like this:

sample module added within the app1 project

Next, we update the webpack.config.js to emit the remoteEntry file and mention the module which is being exposed, namely sample module.

name: "app1",filename: "remoteEntry.js",exposes: {'./Module': './projects/app1/src/app/sample/sample.module.ts',}

Once you have made the following changes, you should be good to run your apps. Restart both applications if previously running. One thing to note is that when you run app1 you should see a new file being emitted remoteEntry.js

When you open the shell app and look at the network tab, you should see a call to fetch this remoteEntry.js file.

Navigate to the sample route to see sample works! printed on the browser — this is being fetched from the remote application. That’s all there is to it, you have now connected the shell application and the remote application. Pat yourself on the back for a job well done!

Dynamic federation: We can also negate the need for registering the micro frontend upfront with shell.

Remove / comment the remote entry change, within the webpack.config.js

// "app1": "http://localhost:8001/remoteEntry.js",

In the app-routing.module.ts, use the function loadRemoteModule to load in the app1 MFE.

{path: 'sample',loadChildren: () =>loadRemoteModule({type: 'module',remoteEntry: 'http://localhost:8001/remoteEntry.js',exposedModule: './Module',}).then((m) => m.SampleModule),}

This will now only load the sample module — when the route is hit.

You can verify this from the network tab. When you click on the sample route link, only then a request for remoteEntry.js is made.

Note: type: 'module' is required for Angular 13.1 or higher as beginning with version 13, the CLI emits EcmaScript modules instead of plain old JavaScript files.

Handling common dependencies

TL;DR:

Shared libraries can be loaded only once — instead of multiple instances with each application

Using the custom builder also allows us to group together common dependencies and load them only once — instead of multiple times per application. We can define libraries as singletons. The snippet below is present in the webpack.config.js file

"@angular/core": { 
singleton: true,
strictVersion: true,
requiredVersion: 'auto'
}

The first property singleton indicates that the Angular core library should only be loaded a single time. strictVersion when set to true will throw a run-time error in case the particular dependency is not installed. requiredVersion set to auto will refer to your package.json to identify which version should be installed. In addition to auto, you can also provide a range value for requiredVersion. Refer to this excellent article on version mismatch from Angular Architects.

Using a common component setup

TL;DR:

Use a common component setup to handle repeated patterns in your applications. If you do not have an overall idea of which pieces of your app are common, complete a couple of sprints before moving these out to a library

In our project, we were lucky enough to have a component library setup before beginning development. Our design team was also aware about our process of building components and then stitching them together to build out application pages. In such a case, you can easily start building out a component library. If however, you do not have an overall idea, another approach is to build out your pages, then once you have completed 2 or 3 sprints, identify the common pieces which can be repurposed.

In Part 3 of the series, we will identify some of the problems that come up when working with this setup, and how you can overcome them.

Thank you for reading, and see you in the next one.

--

--