Reputation: 21
I recently migrated and split my Angular 17 frontend application into several micro-frontends using Webpack Module Federation. My goal was to enable independent deployment of each micro-frontend (each representing a distinct business area) to gain better control over production deployments.
Each micro-frontend is entirely separate, with no communication or shared dependencies between them, except for a shared Angular components library called @common-frontend
.
The @common-frontend
library is the only shared dependency between the micro-frontends. To allow independent deployment of each micro-frontend, I configured @common-frontend
as follows in the shared
section:
shared: {
"@common-frontend": { singleton: false, strictVersion: false },
}
This setup ensures that each micro-frontend can use its own version of @common-frontend
without conflicts. So far, everything worked fine!
Recently, I added custom Angular Material form controls to the @common-frontend
library. These controls are used like this:
<mat-form-field appearance="outline">
<mat-label>Name</mat-label>
<lib-custom-input placeholder="name" formControlName="name" />
</mat-form-field>
Here’s where the problem arises. To make it easier to explain, let’s assume three Angular 17 applications:
@common-frontend
If B and C have different versions of @common-frontend
, everything works perfectly. When navigating between B and C, the appropriate chunks for @common-frontend
are fetched and loaded dynamically based on the respective app's version.
@common-frontend
If B and C share the same version of @common-frontend
, the first app I navigate to (e.g., B) fetches the chunk for @common-frontend
. However, when I then navigate to C, Webpack does not fetch the chunk again since it is already cached. This results in the following error when trying to use the custom Angular Material form controls:
Error: mat-form-field must contain a MatFormFieldControl.
This issue currently occurs only with
mat-form-field
. All other components from@common-frontend
work correctly, even when the same version is used across multiple micro-frontends.
I believe this issue stems from Angular’s injection context not being properly shared or initialized between micro-frontends. Refreshing the page resolves the issue, but this is not an acceptable solution for production.
Here are my goals:
@common-frontend
) across all micro-frontends.If anyone has faced similar issues or has insights into resolving this problem, I would greatly appreciate your help. 🙏
Thank you for your time and support! ❤
All the dependencies version are the same between all the app, excepts @common-frontend
"dependencies": {
"@common-frontend": "<CAN CHANGE>",
"@airbus/elements": "1.9.0",
"@airbus/icons": "1.8.1",
"@angular-architects/module-federation": "^17.0.8",
"@angular/animations": "^17.3.0",
"@angular/cdk": "17.3.1",
"@angular/common": "^17.3.0",
"@angular/compiler": "^17.3.0",
"@angular/core": "^17.3.0",
"@angular/forms": "^17.3.0",
"@angular/material": "17.3.1",
"@angular/platform-browser": "^17.3.0",
"@angular/platform-browser-dynamic": "^17.3.0",
"@angular/router": "^17.3.0",
"ngx-skeleton-loader": "7.0.0",
"ngx-toastr": "^16.2.0",
"rxjs": "~7.8.1",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
webpack.config.js (remote)
const { withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
const deps = require('./package.json').dependencies;
module.exports = withModuleFederationPlugin({
name: 'micro-frontend-a',
exposes: {
'./ARootModule': './src/app/root/root.module.ts',
},
shared: {
"@common-frontend": { singleton: false, strictVersion: false },
"@angular-architects/module-federation": { singleton: true, strictVersion: true, requiredVersion: deps['@angular-architects/module-federation'] },
"@angular/animations": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/animations'] },
"@angular/common": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/common'] },
"@angular/common/http": { strictVersion: true },
"@angular/compiler": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/compiler'] },
"@angular/core": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/core'] },
"@angular/forms": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/forms'] },
"@angular/platform-browser": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/platform-browser'] },
"@angular/platform-browser-dynamic": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/platform-browser-dynamic'] },
"@angular/router": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/router'] },
"@angular/material": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/material'] },
"@angular/cdk": { singleton: true, strictVersion: true, requiredVersion: deps['@angular/cdk'] },
(...)
}
});
webpack.config.js (host) => same without exposes: {}
for the host i use loadRemoteModule inside app.routes.ts
{
loadChildren: () =>
loadRemoteModule({
exposedModule: './ARootModule',
remoteEntry: environment.remote.a.url,
type: 'module'
}),
path: 'a'
},
Upvotes: 1
Views: 65