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.
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 |
Since v15.0.0 version of ngx-mfe library is compatible with Angular version
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).
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
Sync variant of providing 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
(Deprecated from v15.1.0) You can get
MfeRegistry
from DI :class AppComponent { constructor(public mfeRegistry: MfeRegistry) {} }
You can even get instace of MfeRegistry
like this:
const mfeRegistry: MfeRegistry = MfeRegistry.instace;
Async variant of providing mfeConfig:
NOTE: The application will wait for initialization and completes when the promise resolves or the observable completes.
Because under the hood used
APP_INITIALIZER
injection token with useFactory that returns Observale or Promise. More aboutAPP_INITIALIZER
Also you can provide mfeConfig with loading it from external resource as Observale<MfeConfig>
or Promise<MfeConfig>
, for this you should provide this type of object:
type NgxMfeAsyncConfig = {
/**
* A function to invoke to load a `MfeConfig`. The function is invoked with
* resolved values of `token`s in the `deps` field.
*/
useLoader: (...deps: any[]) => Observable<NgxMfeSyncConfig> | Promise<NgxMfeSyncConfig>;
/**
* A list of `token`s to be resolved by the injector. The list of values is then
* used as arguments to the `useLoader` function.
*/
deps?: any[];
};
For example:
mfeConfig: {
useLoader: (http: HttpClient): Observable<MfeConfig> =>
http.get<MfeConfig>('/manifest.json'),
deps: [HttpClient]
},
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:
<ng-template
mfeOutlet="test"
mfeOutletModule="MfeTestModule"
mfeOutletComponent="MfeTestComponent"
>
</ng-template>
Other variant:
<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 property loaderDelay
:
<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 with TemplateRef
, pass it to the appropriate properties loader
and fallback
:
<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:
Refactored:
MfeService
to RemoteComponentLoader
;MfeComponentsCache
to RemoteComponentsCache
;ModularRemoteComponent
type to RemoteComponentWithModule
;ngZone.runOutside
the loadMfe
function calls inside the RemoteComponentLoader
;ComponentWithNgModuleRef<TComponent, TModule>
, that holds component class Type<T>
and NgModuleRef
;RemoteComponentWithModule
from ComponentFactory
to ComponentWithNgModuleRef
;RemoteComponentLoader
(old name MfeService
) renamed function loadModularComponent
to loadComponentWithModule
loadComponentWithModule
inside class RemoteComponentLoader
from Promise<ComponentFactory<TComponent>>
to Promise<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 name MFE 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 type RemoteComponent
;
The validateMfe
function has been removed (it was used until version 2.0.0);
The loader
and fallback
properties in the NgxMfeOptions
has been changed from MFE string
to RemoteComponent
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 from LoadMfeOptions
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;Mfe string
has been removed from the library, the API of [mfeOutlet]
directive has changed:
mfeOutletLoader
and mfeOutletFallback
now accept only TemplateRef
, more details below.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 specify mfeOutletModule
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 from MfeComponentFactoryResolverOptions
to LoadMfeOptions
;@Input('mfeOutletLoader')' loader
and @Input('mfeOutletFallback') fallback
now accept only TemplateRef
, not TemplateRef
or Mfe string
. But you can still use micro-frontend component for loader
and fallback
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>
MfeComponentFactoryResolver
has been replaced with MfeService
and the API has been changed;MfeComponentFactoryResolverOptions
type has been removed;MfeComponentCache
not only saves ComponentFactory<T>
but also Type<T>
;ComponentFactory<T>
was replaced to ComponentWithNgModuleRef<TComponent, TModule>
;bindInputs()
and bindOutputs()
methods now require ComponentRef<any>
in the first argument, MfeOutletInputs
/MfeOutletOutputs
are method dependent in the second, and the third argument has been removed;DynamicComponentInputs
and DynamicComponentOutputs
types have been removed because these types are replaced in bindInputs()
and bindOutputs()
respectively by the ComponentRef<any>
type;validateInputs()
method has been removed;validateOutputs()
method is now private;loadMfeComponent
helper function;parseMfeString
helper function;loadMfeModule
helper function to loadMfe
and added optional parameter options: LoadMfeOptions
. LoadMfeOptions
has property a moduleName
, that sets a custom name for the Module class within the opened file, and has type
that specify type of Module Federation;MfeService
to MfeComponentFactoryResolver
;MfeComponentFactoryResolver
has the same method as MfeService
, but now it can accepts an optional options: MfeComponentFactoryResolver
parameter. This parameter extends LoadMfeOptions
type, added a componentName
parameter, that sets a custom name for the Component class.MfeOutletDirective
- options: MfeComponentFactoryResolver
, this parameter provided to resolveComponentFactory
method of the MfeComponentFactoryResolver
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 property mfeOutletOptions
inside MfeOutletDirective
and in the options parameter of the loadMfe
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 property mfeOutletOptions = { componentName: 'CustomName' }
;
IMfeModuleRootOptions
interface renamed to NgxMfeOptions
;delay
in the NgxMfeOptions
renamed to loaderDelay
;OPTIONS
injection token renamed to NGX_MFE_OPTIONS
;