Photo by Andrey Tikhonovskiy on Unsplash
π Angular Micro Frontends with Module Federation: How Closures Enable Dynamic Loading
Encapsulating Remote Modules, Lazy Loading Components, and Preventing Dependency Conflicts
Let's deep dive into implementing Angular Micro Frontends (MFEs) with Module Federation, focusing on how closures help isolate modules and dynamically load remote components.
1οΈβ£ What is Module Federation in Angular?
Module Federation, introduced in Webpack 5, allows multiple independent Angular apps to communicate while remaining separately deployable.
β
Load Angular modules dynamically at runtime
β
Isolate dependencies using closures
β
Improve performance by sharing common libraries
2οΈβ£ How Closures Play a Role in Angular Micro Frontends
π Why Use Closures?
Each Micro Frontend encapsulates its dependencies in a closure, preventing global namespace conflicts.
Lazy loading ensures that a module is loaded only when needed, reducing initial bundle size.
Isolated runtime execution means each MFE runs inside its own function scope.
πΉ Example: How Module Federation Uses Closures
const mfe1 = (() => {
let instance; // Closure keeps track of MFE instance
return {
init: function () {
if (!instance) {
instance = new AngularMFE(); // Closure ensures singleton instance
}
return instance;
}
};
})();
The closure hides internal state (
instance
), ensuring each MFE loads only once.When
mfe1.init()
is called, it either returns an existing instance or creates a new one.
3οΈβ£ Implementing Angular Micro Frontends (Step-by-Step)
Weβll create: β
Shell App (Host Application) β Loads micro frontends dynamically.
β
Remote App (Micro Frontend) β Exposes Angular components to the shell.
π Step 1: Install Webpack 5 Module Federation Plugin
Run:
ng add @angular-architects/module-federation
This installs Module Federation in your Angular app.
π Step 2: Setup Remote Micro Frontend
πΉ Configure webpack.config.js
for the Remote App
Modify projects/mfe1/webpack.config.js
:
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
output: {
publicPath: "http://localhost:4201/",
},
plugins: [
new ModuleFederationPlugin({
name: "mfe1",
filename: "remoteEntry.js",
exposes: {
"./Component": "./src/app/mfe1/mfe1.component.ts",
},
shared: ["@angular/core", "@angular/common", "@angular/router"],
}),
],
};
π Key Features of Closures Here
exposes: { "./Component": "./src/app/mfe1/mfe1.component.ts" }
πΉ Encapsulates MFE inside a closure so that it's accessible only via API calls.shared: ["@angular/core", "@angular/common"]
πΉ Prevents duplicate dependencies, reducing bundle size.
π Step 3: Setup Shell App (Host Application)
πΉ Configure webpack.config.js
for the Shell
Modify projects/shell/webpack.config.js
:
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
output: {
publicPath: "http://localhost:4200/",
},
plugins: [
new ModuleFederationPlugin({
name: "shell",
remotes: {
mfe1: "mfe1@http://localhost:4201/remoteEntry.js",
},
shared: ["@angular/core", "@angular/common", "@angular/router"],
}),
],
};
π Key Features of Closures Here
"mfe1@
http://localhost:4201/remoteEntry.js"
πΉ Calls the closure insidemfe1
to dynamically load its module at runtime.
π Step 4: Dynamically Load Remote Module in the Shell
Modify app-routing.module.ts
in shell
:
const routes: Routes = [
{ path: 'mfe1', loadChildren: () => import('mfe1/Component').then(m => m.Mfe1Component) }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
π What Happens Internally?
When navigating to
/mfe1
, the shell callsimport('mfe1/Component')
.Webpack fetches
remoteEntry.js
from mfe1.The
exposes
closure returns the Mfe1Component instance.The module is loaded into the host at runtime.
π Running the Micro Frontend System
- Start the remote app:
ng serve mfe1 --port 4201
- Start the shell app:
ng serve shell --port 4200
- Navigate to
http://localhost:4200/mfe1
β The Micro Frontend dynamically loads inside the shell! π
π₯ Key Benefits of Using Closures in Angular Micro Frontends
β
Encapsulation: Each MFE is self-contained inside a closure, preventing conflicts.
β
Lazy Loading: Components load only when needed, improving performance.
β
Singleton Instances: Closures ensure that MFEs donβt reload unnecessarily.