Demonstration of a distributed deployment architecture connected together in the frontend with webpack Module Federation.
- Install dependencies (make sure to use yarn because otherwise you won't get webpack@5)
yarn
- Start serving
yarn run serve
- Go to http://localhost:3000
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"
},
...
}
}
}
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.
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 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.
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 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.
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/'
]
})
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