A library for working with MFE in Angular in a plugin-based approach and with Angular routing.
If you have production build issues check this issue. This issue has been fixed in version 2.0.0.
Have problems with updates? Check out the migration guides.
- Version Compliance
- Motivation
- Features
- Examples
- Conventions
- Configuring
- Display MFE in HTML template / plugin-based approach
- Display Angular v14 Standalone Components
- Passing Data to the MFE Component via mfeOutlet directive
- Load MFE by Route
- Changelog
ngx-mfe | v1.0.0 | v1.0.5 | v2.0.0 | v3.0.0 |
---|---|---|---|---|
Angular | v12.0.0 | v13.0.0 | v13.0.0 | v14.0.0 |
@angular-architects/module-federation | v12.0.0 | v14.0.0 | v14.0.0 | v14.3.0 |
When Webpack 5 came along and the Module Federation plugin, it became possible to separately compile and deploy code for front-end applications, thereby breaking up a monolithic front-end application into separate and independent MicroFrontEnd (MFE) applications.
The ngx-mfe is an extension of the functionality of the @angular-architects/module-federation. Using @angular-architects/module-federation you could only upload one micro-frontend per page (in the Routing), this limitation was the main reason for the creation of this library.
The key feature of the ngx-mfe library is ability to work with micro-frontends directly in the HTML template using a plugin-based approach. You can load more than one micro-frontend per page.
You can use both ngx-mfe and @angular-architects/module-federation libs together in the same project.
🔥 Load multiple micro-frontend directly from an HTML template with the ability to display a loader component during loading and a fallback component when an error occurs during loading and/or rendering of the mfe component.
🔥 Easy to use, just declare structural directive *mfeOutlet
in your template.
🔥 Supports Angular Standalone Components.
🔥 More convenient way to load MFE via Angular Routing.
🔥 It's easy to set up different remoteEntryUrl MFEs for different builds (dev/prod/etc).
- Example of an application using ngx-mfe v1.
- Example of an application using ngx-mfe v2.
- Example of an application using ngx-mfe v3 with Angular 14 Standalone Components.
- Here you can find a series of articles about Micro-frontends/Module Federation and a step-by-step guide to building an application with Micro-frontends.
-
To display a standalone MFE component, you only need to the component file itself.
A standalone component is a component that does not have any dependencies provided or imported in the module where that component is declared.
Since Angular v14 standalone component it is component that marked with
standalone: true
in@Component({...})
decorator.When you display a standalone MFE component through
[mfeOutlet]
directive you must omit[mfeOutletModule]
input.// Standalone Component - standalone.component.ts import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-standalone', standalone: true, imports: [CommonModule], template: ` <p>Standalone component works!</p> `, styles: [], }) export class StandaloneComponent {}
// dashboard-mfe webpack.config { new ModuleFederationPlugin({ name: 'dashboard-mfe', filename: 'remoteEntry.js', exposes: { StandaloneComponent: 'apps/dashboard-mfe/src/app/standalone.component.ts', }, [...] }); }
<!-- shell-app --> <ng-template mfeOutlet="dashboard-mfe" mfeOutletComponent="StandaloneComponent" > </ng-template>
-
To display an MFE component with dependencies in the module where the component was declared, you must expose both the component file and the module file from ModuleFederationPlugin.
This approach is widely used and recommended.
When you display this type of MFE component with the
[mfeOutlet]
directive, you must declare an input[mfeOutletModule]
with the value of the exposed module name. -
The file key of an exposed Module or Component (declared in the ModuleFederationPlugin in the 'expose' property) must match the class name of that file.
For the plugin-based approach, when loads MFE using
[mfeOutlet]
directive you must declare Component in the exposed Module and the Component name must match the file key of an exposed Component class.// webpack.config { new ModuleFederationPlugin({ name: 'dashboard-mfe', filename: 'remoteEntry.js', exposes: { // EntryModule is the key of the entry.module.ts file and corresponds to the exported EntryModule class from this file. EntryModule: 'apps/dashboard-mfe/src/app/remote-entry/entry.module.ts', // the EntryComponent is key of file entry.module.ts, and match to exported EntryComponent class from that file. EntryComponent: 'apps/dashboard-mfe/src/app/remote-entry/entry.component.ts', }, [...] }); }
If the name of Module doesn't match, you can specify a custom name for this Module in the @Input() property
mfeOutletOptions = { componentName: 'CustomName' }
of[mfeOutlet]
directive, and pass{ moduleName: 'CustomName' }
options to theloadMfe()
function;If the name of Component doesn't match, you can specify a custom name for this Component in the @Input() property
mfeOutletOptions = { componentName: 'CustomName' }
of[mfeOutlet]
directive, and pass{ moduleName: 'CustomName' }
options to theloadMfe()
function; -
You must follow the rule that only one Component must be declared for an exposed Module. This is known as SCAM (Single Component Angular Module) pattern.
Add the ngx-mfe library to a shared property in the ModuleFederationPlugin inside webpack.config.js file for each application in your workspace.
module.exports = {
[...]
plugins: [
[...]
new ModuleFederationPlugin({
remotes: {},
shared: share({
[...]
"ngx-mfe": {
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
includeSecondaries: true
},
...sharedMappings.getDescriptors(),
}),
library: {
type: 'module'
},
}),
[...]
],
[...]
};
To configure this library, you must import MfeModule.forRoot(options: NgxMfeOptions)
into the root module of the Host app(s) and the root module of the Remote apps in order for Remote to work correctly when running as a standalone application:
For feature modules just import
MfeModule
without options, where, you may need the functionality of the library, for example, theMfeOutlet
directive.
For core / app module:
@NgModule({
imports: [
MfeModule.forRoot({
mfeConfig: {
"dashboard-mfe": "http://localhost:4201/remoteEntry.js",
"loaders-mfe": "http://localhost:4202/remoteEntry.js",
"fallbacks-mfe": "http://localhost:4203/remoteEntry.js"
},
preload: ['loaders-mfe', 'fallbacks-mfe'],
loader: {
app: 'loaders',
module: 'SpinnerModule',
component: 'SpinnerComponent',
},
loaderDelay: 500,
fallback: {
app: 'fallbacks',
module: 'MfeFallbackModule',
component: 'MfeFallbackComponent',
},
}),
],
})
export class AppModule {}
For feature module:
@NgModule({
imports: [
MfeModule,
],
})
export class Feature1Module {}
-
mfeConfig - object where key is micro-frontend app name specified in
ModuleFederationPlugin
(webpack.config.js) and value is remoteEntryUrl string. All data will be sets to MfeRegistry.Key it's the name same specified in webpack.config.js of MFE (Remote) in option name in
ModuleFederationPlugin
.Value set the following pattern:
{url}/{remoteEntrypointFilename}
.-
url
is the url where the remote application is hosted. -
remoteEntrypointFilename
is the filename supplied in the remote's webpack configuration.Example http://localhost:4201/remoteEntry.js
You can get
MfeRegistry
from DI:class AppComponent { constructor(public mfeRegistry: MfeRegistry) {} }
Or you can even get
MfeRegistry
without DI, because this class is written as a singleton:const mfeRegistry: MfeRegistry = MfeRegistry.getInstance();
-
-
preload (Optional) - a list of micro-frontend names, their bundles (remoteEntry.js) will be loaded and saved in the cache when the application starts.
Next options are only works in plugin-based approach with MfeOutletDirective
:
-
loaderDelay (Optional) - Specifies the minimum loader display time in ms. This is to avoid flickering when the micro-frontend loads very quickly.
By default is 0.
-
loader (Optional) - Displayed when loading the micro-frontend. Implements the
RemoteComponent
interface.Example:
// Globally uses the "SpinnerComponent" loader component declared in the "SpinnerModule" of the app "loaders". loader: { app: 'loaders', module: 'SpinnerModule', component: 'SpinnerComponent', },
For better UX, add loader micro-frontends to the
preload
. -
fallback (Optional) - Displayed when loading or compiling a micro-frontend with an error. Implements the
RemoteComponent
interface.Example:
// Globally uses the "MfeFallbackComponent" fallback component declared in the "MfeFallbackModule" of the app "fallbacks". fallback: { app: 'fallbacks', module: 'MfeFallbackModule', component: 'MfeFallbackComponent', },
For better UX, add fallback micro-frontends to the
preload
.
You can get all configured options by injecting NGX_MFE_OPTIONS
by DI:
class AppComponent {
constructor(@Inject(NGX_MFE_OPTIONS) public options: NgxMfeOptions) {}
}
This approach allows us to load micro-frontends directly from HTML.
The advantages of this approach are that we can display several MFEs at once on the same page, even display several of the same MFEs.
More about plugin-based approach here.
Full code of this example can be found at https://github.com/dkhrunov/ngx-mfe-test.
Example app:
An example webpack.config.js that exposes the "MfeTestComponent" (brown border in the screenshot above):
// webpack.config.js
return {
[...]
resolve: {
alias: sharedMappings.getAliases(),
},
plugins: [
new ModuleFederationPlugin({
name: 'test',
exposes: {
MfeTestModule: 'apps/test/src/app/mfe-test/mfe-test.module.ts',
MfeTestComponent: 'apps/test/src/app/mfe-test/mfe-test.component.ts',
},
filename: 'remoteEntry',
shared: share({ ... }),
}),
sharedMappings.getPlugin(),
],
};
- Just display the component "MfeTestComponent" inside other MFE component "Form" from "address-form" app:
One variant:
```html
<ng-template
mfeOutlet="test"
mfeOutletModule="MfeTestModule"
mfeOutletComponent="MfeTestComponent"
>
</ng-template>
```
Other variant:
```html
<ng-container
*mfeOutlet="
'test';
module: 'MfeTestModule';
component: 'MfeTestComponent'
"
>
</ng-container>
```
> These two examples are equal and display the MFE "MfeTestComponent".
-
You can pass/bind
@Input
and@Output
props to MFE component:<!-- form.component.html file --> <ng-container *mfeOutlet=" 'test'; module: 'MfeTestModule'; component: 'MfeTestComponent'; inputs: { text: text$ | async }; outputs: { click: onClick }; " ></ng-container>
// form.component.ts file @Component({ selector: 'app-form', templateUrl: './form.component.html', styleUrls: ['./form.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class FormComponent { [...] // timer emits after 1 second, then every 2 seconds public readonly text$: Observable<number> = timer(1000, 2000); // on click log to console event public onClick(event: MouseEvent): void { console.log('clicked', event); } [...] }
If you try to bind a @Output() property that is not in the component, then an error will fall into the console: "Output someOutput is not output of SomeComponent."
If you try to pass a non-function, then an error will fall into the console: "Output someOutput must be a function."
-
To override the default loader delay, configured in
MfeModule.forRoot({ ... })
, provide custom number in ms to propertyloaderDelay
:<ng-container *mfeOutlet=" 'test'; module: 'MfeTestModule'; component: 'MfeTestComponent'; loaderDelay: 1000 " ></ng-container>
-
To override the default loader and fallback MFE components, configured in
MfeModule.forRoot({ ... })
, specify content withTemplateRef
, pass it to the appropriate propertiesloader
andfallback
:<ng-container *mfeOutlet=" 'test'; module: 'MfeTestModule'; component: 'MfeTestComponent'; loader: loaderTpl; fallback: fallbackTpl " ></ng-container> <ng-template #loaderTpl> <div>loading...</div> </ng-template> <ng-template #fallbackTpl> <div>Ooops! Something went wrong</div> </ng-template>
<!-- TemplateRef that render loader as MFE component --> <ng-template mfeOutlet="test" mfeOutletModule="MfeTestModule" mfeOutletComponent="MfeTestComponent" [mfeOutletLoader]="loaderMfeTpl" ></ng-template> <ng-template #loaderMfeTpl> <ng-template mfeOutlet="loaders-mfe" mfeOutletModule="SpinnerModule" mfeOutletComponent="SpinnerComponent" [mfeOutletLoader]="undefined" [mfeOutletLoaderDelay]="0" > </ng-template> </ng-template>
-
You can also provide a custom injector for a component like this:
<ng-template mfeOutlet="test" mfeOutletModule="MfeTestModule" mfeOutletComponent="MfeTestComponent" [mfeOutletInjector]="customInjector" ></ng-template>
Example app:
An example webpack.config.js that exposes the "StandaloneComponent" (green border in the screenshot above):
// webpack.config.js
return {
[...]
resolve: {
alias: sharedMappings.getAliases(),
},
plugins: [
new ModuleFederationPlugin({
name: 'test',
exposes: {
[...]
StandaloneComponent: 'apps/test/src/app/standalone/standalone.component.ts',
},
filename: 'remoteEntry',
shared: share({ ... }),
}),
sharedMappings.getPlugin(),
],
};
// standalone.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-standalone',
standalone: true,
imports: [CommonModule],
template: ` <p>Standalone component works!</p> `,
styles: [],
})
export class StandaloneComponent {}
<!-- form.component.html of the address-form app -->
[...]
<h3>Angular v14 Standalone component loaded as MFE:</h3>
<ng-template
mfeOutlet="test"
mfeOutletComponent="StandaloneComponent"
></ng-template>
After using this library for some time, as the author of this library, I came to the conclusion that using @Inputs and @Outputs of an MFE component through the [mfeOutletInputs]
[mfeOutletOutputs]
properties is not the best practice. Try to make your MFE components as independent as possible from the external environment. But if you still have to pass some values to the component, you can do it in two ways:
-
As I wrote above through the properties
[mfeOutletInputs]
[mfeOutletOutputs]
component.html:
<ng-template mfeOutlet="test" mfeOutletModule="MfeTestModule" mfeOutletComponent="MfeTestComponent" [mfeOutletInputs]="{ text: text$ | async }" [mfeOutletOutputs]="{ click: onClick }" > </ng-template>
component.ts
@Component({ ... }) export class Component { public text$ = new BehaviorSubject<string>('Test string'); constructor() { } public onClick(bool: MouseEvent): void { console.log('login', bool); } }
-
The second way is to create a new injector and add the necessary data for the MFE component to it. The
[mfeOutlet]
directive has the[mfeOutletInjector]
property through which you can pass the desired injector, when the component is created, the previously passed injector in the[mfeOutletInjector]
property will be used instead of the current injector.component.html:
<ng-template mfeOutlet="test" mfeOutletModule="MfeTestModule" mfeOutletComponent="MfeTestComponent" [mfeOutletInjector]="testComponentInjector" > </ng-template>
component.ts
@Component({ ... }) export class Component { public readonly testComponentInjector: Injector; constructor(private readonly _injector: Injector) { this.testComponentInjector = Injector.create({ parent: this._injector, providers: [ { provide: TEST_DATA, useValue: data, }, ], }); } }
To use micro-frontends in Routing, you must import and apply the helper function called loadMfe
, like in the example below:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { loadMfe } from '@dkhrunov/ng-mfe';
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => loadMfe('dashboard-mfe', 'EntryModule'),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking' })],
exports: [RouterModule],
})
export class AppRoutingModule {}
Fixed:
- Fix error, if the fallback is also unavailable, then simply clear the view;
Refactored:
- Renamed
MfeService
toRemoteComponentLoader
; - Renamed
MfeComponentsCache
toRemoteComponentsCache
; - Renamed
ModularRemoteComponent
type toRemoteComponentWithModule
; - Wrapped to
ngZone.runOutside
theloadMfe
function calls inside theRemoteComponentLoader
; - Added new type
ComponentWithNgModuleRef<TComponent, TModule>
, that holds component classType<T>
andNgModuleRef
; - Changed cached value for
RemoteComponentWithModule
fromComponentFactory
toComponentWithNgModuleRef
; - In
RemoteComponentLoader
(old nameMfeService
) renamed functionloadModularComponent
toloadComponentWithModule
- Changed return type of method
loadComponentWithModule
inside classRemoteComponentLoader
fromPromise<ComponentFactory<TComponent>>
toPromise<ComponentWithNgModuleRef<TComponent, TModule>>
;
Why has the API changed? - The problem is that when you use the [mfeOutlet]
directive issue, it tries to find the component inside the compiled module by name (as a string), but in runtime the class name will be optimized and replaced with a short character. For example, you have a class TestComponent
, it can be changed to the class name a
and this causes this error.
-
To properly use the plugin-based approach in a micro-frontend architecture, or simply if you are use
[mfeOutlet]
directive, you must now expose both the component file and module file in which the component is declared to the ModuleFederationPlugin.Rarerly : or, if your micro-frontend component is standalone (a standalone component is a component that does not have any dependencies declared or imported in the module where that component is declared), then it is sufficient to provide just that component file to the ModuleFederationPlugin;
-
Now ngx-mfe does not use
Micro-frontend string
(or anouther nameMFE string
) is a kebab-case style string and matches the pattern"mfe-app-name/exposed-file-name"
(it was used until version 2.0.0); -
MFE string
has been replaced by a new typeRemoteComponent
; -
The
validateMfe
function has been removed (it was used until version 2.0.0); -
The
loader
andfallback
properties in theNgxMfeOptions
has been changed fromMFE string
toRemoteComponent
type:Before v2.0.0:
@NgModule({ declarations: [AppComponent], imports: [ BrowserModule, BrowserAnimationsModule, MfeModule.forRoot({ mfeConfig: { "dashboard-mfe": "http://localhost:4201/remoteEntry.js", "loaders-mfe": "http://localhost:4202/remoteEntry.js", "fallbacks-mfe": "http://localhost:4203/remoteEntry.js" }, loader: 'loaders/spinner', fallback: 'fallbacks/mfe-fallback', }), ], bootstrap: [AppComponent], }) export class AppModule {}
Since v2.0.0:
@NgModule({ declarations: [AppComponent], imports: [ BrowserModule, BrowserAnimationsModule, MfeModule.forRoot({ mfeConfig: { "dashboard-mfe": "http://localhost:4201/remoteEntry.js", "loaders-mfe": "http://localhost:4202/remoteEntry.js", "fallbacks-mfe": "http://localhost:4203/remoteEntry.js" }, loader: { app: 'loaders', module: 'SpinnerModule', component: 'SpinnerComponent', }, fallback: { app: 'fallbacks', module: 'MfeFallbackModule', component: 'MfeFallbackComponent', }, }), ], bootstrap: [AppComponent], }) export class AppModule {}
-
Removed
moduleName
property fromLoadMfeOptions
type; -
Now, wherever you need to specify the name of the exposed file through the config in the webpack.config in the ModuleFederationPlugin, you must specify exactly the same name as in the config itself, the kebab-style name was used earlier.
// webpack.config.js exposes: { // LoginModule name of the exposed file login.module.ts LoginModule: 'apps/auth-mfe/src/app/login/login.module.ts', },
Before v2.0.0:
loadMfe('auth-mfe/login-module')
Since v2.0.0:
loadMfe('auth-mfe' 'LoginModule')
-
Arguments changed in
LoadMfe
function:Before v2.0.0:
async function loadMfe<T = unknown>(mfeString: string, options?: LoadMfeOptions): Promise<Type<T>> {}
Since v2.0.0:
async function loadMfe<T = unknown>(remoteApp: string, exposedFile: string, options?: LoadMfeOptions): Promise<Type<T>> {}
remoteApp
- is the name of the remote app as specified in the webpack.config.js file in the ModuleFederationPlugin in the name property;exposedFile
- is the key (or name) of the exposed file specified in the webpack.config.js file in the ModuleFederationPlugin in the exposes property;
-
Since the
Mfe string
has been removed from the library, the API of[mfeOutlet]
directive has changed:mfeOutletLoader
andmfeOutletFallback
now accept onlyTemplateRef
, more details below.- To load a standalone component, you must specify the following details:
mfeOutlet
with the name of the application,mfeOutletComponent
with the name of the component's open file from the ModuleFederationPlugin in webpack.config. But to load a non-standalone component, you must additionally specifymfeOutletModule
with the name of the open module file in which the component is declared for the ModuleFederationPlugin in webpack.config.
-
@Input('mfeOutletOptions')' options
changed type fromMfeComponentFactoryResolverOptions
toLoadMfeOptions
; -
@Input('mfeOutletLoader')' loader
and@Input('mfeOutletFallback') fallback
now accept onlyTemplateRef
, notTemplateRef
orMfe string
. But you can still use micro-frontend component forloader
andfallback
in the[mfeOutlet]
, like in the example below:<!-- With Mfe loader --> <ng-template mfeOutlet="dashboard-mfe" mfeOutletModule="EntryModule" mfeOutletComponent="EntryComponent" [mfeOutletLoader]="loaderMfe" > </ng-template> <!-- Mfe component for loader --> <ng-template #loaderMfe> <!-- For loader Mfe you should set mfeOutletLoader to undefined, and mfeOutletLoaderDelay to 0. For better UX. --> <ng-template mfeOutlet="loaders-mfe" mfeOutletModule="SpinnerModule" mfeOutletComponent="SpinnerComponent" [mfeOutletLoader]="undefined" [mfeOutletLoaderDelay]="0" > </ng-template> </ng-template> <!-- With simple HTML content as loader --> <ng-template mfeOutlet="dashboard-mfe" mfeOutletModule="EntryModule" mfeOutletComponent="EntryComponent" [mfeOutletLoader]="loaderMfe" > </ng-template> <!-- Simple HTML content. --> <ng-template #loader> <div>loading...</div> </ng-template>
- The
MfeComponentFactoryResolver
has been replaced withMfeService
and the API has been changed; - The
MfeComponentFactoryResolverOptions
type has been removed;
- Now the
MfeComponentCache
not only savesComponentFactory<T>
but alsoType<T>
; - In version 2.1.0
ComponentFactory<T>
was replaced toComponentWithNgModuleRef<TComponent, TModule>
;
- The
bindInputs()
andbindOutputs()
methods now requireComponentRef<any>
in the first argument,MfeOutletInputs
/MfeOutletOutputs
are method dependent in the second, and the third argument has been removed; - The
DynamicComponentInputs
andDynamicComponentOutputs
types have been removed because these types are replaced inbindInputs()
andbindOutputs()
respectively by theComponentRef<any>
type; - The
validateInputs()
method has been removed; - The
validateOutputs()
method is now private;
-
Deleted the
loadMfeComponent
helper function; -
Deleted the
parseMfeString
helper function; -
Renamed the
loadMfeModule
helper function toloadMfe
and added optional parameteroptions: LoadMfeOptions
.LoadMfeOptions
has property amoduleName
, that sets a custom name for the Module class within the opened file, and hastype
that specify type of Module Federation; -
Renamed the
MfeService
toMfeComponentFactoryResolver
; -
MfeComponentFactoryResolver
has the same method asMfeService
, but now it can accepts an optionaloptions: MfeComponentFactoryResolver
parameter. This parameter extendsLoadMfeOptions
type, added acomponentName
parameter, that sets a custom name for the Component class. -
Added new Input prop to the
MfeOutletDirective
-options: MfeComponentFactoryResolver
, this parameter provided toresolveComponentFactory
method of theMfeComponentFactoryResolver
when resolving the component factory of MFE. -
Since v1.1.0 you don't need to expose from
ModuleFederationPlugin
for plugin-based approach both Module and Component, just specify the Module file.The exposed Module key must match the name of the exposed module without the 'Module' suffix. Also, if the name doesn't match, you can specify a custom Module name in the options
{ moduleName: 'CustomName' }
in the propertymfeOutletOptions
insideMfeOutletDirective
and in the options parameter of theloadMfe
helper function.For the plugin-based approach, when loads MFE using
MfeOutletDirective
you must declare Component in the exposed Module and the Component name must match the exposed Module key without suffix 'Component'. Also, if the name doesn't match, you can specify a custom Component name in the Input propertymfeOutletOptions = { componentName: 'CustomName' }
;
IMfeModuleRootOptions
interface renamed toNgxMfeOptions
;- Property
delay
in theNgxMfeOptions
renamed toloaderDelay
; OPTIONS
injection token renamed toNGX_MFE_OPTIONS
;