├── .editorconfig ├── .gitignore ├── LICENCE ├── README.md ├── angular.json ├── migration-guide-to-v2.md ├── migration-guide.md ├── package-lock.json ├── package.json ├── projects └── ngx-mfe │ ├── LICENCE │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── decorators │ │ │ ├── index.ts │ │ │ └── track-changes.decorator.ts │ │ ├── directives │ │ │ ├── index.ts │ │ │ ├── mfe-outlet.directive.spec.ts │ │ │ └── mfe-outlet.directive.ts │ │ ├── helpers │ │ │ ├── delay.ts │ │ │ ├── index.ts │ │ │ └── load-mfe.ts │ │ ├── injection-tokens │ │ │ ├── index.ts │ │ │ └── options.token.ts │ │ ├── interfaces │ │ │ ├── index.ts │ │ │ ├── mfe-config.interface.ts │ │ │ ├── ngx-mfe-options.interface.ts │ │ │ └── remote-component.interface.ts │ │ ├── mfe.module.ts │ │ ├── registry │ │ │ ├── index.ts │ │ │ └── mfe-registry.ts │ │ ├── services │ │ │ ├── dynamic-component-binding.spec.ts │ │ │ ├── dynamic-component-binding.ts │ │ │ ├── index.ts │ │ │ ├── remote-component-loader.spec.ts │ │ │ ├── remote-component-loader.ts │ │ │ ├── remote-components-cache.spec.ts │ │ │ └── remote-components-cache.ts │ │ └── types │ │ │ ├── component-with-ng-module-ref.ts │ │ │ ├── index.ts │ │ │ ├── mfe-outlet-inputs.ts │ │ │ └── mfe-outlet-outputs.ts │ ├── public-api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .history/* 32 | 33 | # misc 34 | /.angular/cache 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Denis Khrunov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular micro-frontend library - ngx-mfe 2 | 3 | A library for working with MFE in Angular in a plugin-based approach and with Angular routing. 4 | 5 | > If you have production build issues check this [issue](https://github.com/dkhrunov/ngx-mfe/issues/7). __This issue has been fixed in version 2.0.0.__ 6 | 7 | Have problems with updates? Check out the [migration guides](../../migration-guide.md). 8 | 9 | ## Contents 10 | 11 | - [Version Compliance](#version-compliance) 12 | - [Motivation](#motivation) 13 | - [Features](#features) 14 | - [Examples](#examples) 15 | - [Conventions](#conventions) 16 | - [Configuring](#configuring) 17 | - [Display MFE in HTML template / plugin-based approach](#display-mfe-in-html-template--plugin-based-approach) 18 | - [Display Angular v14 Standalone Components](#display-angular-v14-standalone-components) 19 | - [Passing Data to the MFE Component via mfeOutlet directive](#passing-data-to-the-mfe-component-via-mfeoutlet-directive) 20 | - [Load MFE by Route](#load-mfe-by-route) 21 | - [Changelog](#changelog) 22 | 23 | ## Version Compliance 24 | ngx-mfe | v1.0.0 | v1.0.5 | v2.0.0 | v3.0.0 | 25 | --------------------------------------| ------- | ------- | ------- | ------- | 26 | Angular | v12.0.0 | v13.0.0 | v13.0.0 | v14.0.0 | 27 | @angular-architects/module-federation | v12.0.0 | v14.0.0 | v14.0.0 | v14.3.0 | 28 | 29 | **Since v15.0.0 version of ngx-mfe library is compatible with Angular version** 30 | 31 | ## Motivation 32 | 33 | 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 **M**icro**F**ront**E**nd (MFE) applications. 34 | 35 | The **ngx-mfe** is an extension of the functionality of the [@angular-architects/module-federation](https://www.npmjs.com/package/@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. 36 | 37 | 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. 38 | 39 | > You can use both **ngx-mfe** and **@angular-architects/module-federation** libs together in the same project. 40 | 41 | ## Features 42 | 43 | 🔥 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. 44 | 45 | 🔥 Easy to use, just declare structural directive `*mfeOutlet` in your template. 46 | 47 | 🔥 Supports Angular Standalone Components. 48 | 49 | 🔥 More convenient way to load MFE via Angular Routing. 50 | 51 | 🔥 It's easy to set up different remoteEntryUrl MFEs for different builds (dev/prod/etc). 52 | 53 | ## Examples 54 | 55 | - [Example of an application using ngx-mfe v1.](https://github.com/dkhrunov/ngx-mfe-test/tree/lesson_4) 56 | - [Example of an application using ngx-mfe v2.](https://github.com/dkhrunov/ngx-mfe-test/tree/update-to-ngx-mfe-v2) 57 | - [Example of an application using ngx-mfe v3 with Angular 14 Standalone Components.](https://github.com/dkhrunov/ngx-mfe-test) 58 | - [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.](https://dekh.medium.com/angular-micro-frontend-architecture-part-1-3-the-concept-of-micro-frontend-architecture-2ff56a5ac264) 59 | 60 | ## Conventions 61 | 62 | 1. To display a standalone MFE component, you only need to __the component file itself__. 63 | 64 | > A standalone component is a component that does not have any dependencies provided or imported in the module where that component is declared. 65 | > 66 | > Since Angular v14 standalone component it is component that marked with `standalone: true` in `@Component({...})` decorator. 67 | 68 | When you display a standalone MFE component through `[mfeOutlet]` directive you must omit `[mfeOutletModule]` input. 69 | 70 | ```typescript 71 | // Standalone Component - standalone.component.ts 72 | import { Component } from '@angular/core'; 73 | import { CommonModule } from '@angular/common'; 74 | 75 | @Component({ 76 | selector: 'app-standalone', 77 | standalone: true, 78 | imports: [CommonModule], 79 | template: `

Standalone component works!

`, 80 | styles: [], 81 | }) 82 | export class StandaloneComponent {} 83 | ``` 84 | 85 | ```typescript 86 | // dashboard-mfe webpack.config 87 | { 88 | new ModuleFederationPlugin({ 89 | name: 'dashboard-mfe', 90 | filename: 'remoteEntry.js', 91 | exposes: { 92 | StandaloneComponent: 'apps/dashboard-mfe/src/app/standalone.component.ts', 93 | }, 94 | [...] 95 | }); 96 | } 97 | ``` 98 | 99 | ```html 100 | 101 | 105 | 106 | ``` 107 | 108 | 2. 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. 109 | 110 | > This approach is widely used and recommended. 111 | 112 | 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. 113 | 114 | 3. 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. 115 | 116 | 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. 117 | 118 | ```typescript 119 | // webpack.config 120 | { 121 | new ModuleFederationPlugin({ 122 | name: 'dashboard-mfe', 123 | filename: 'remoteEntry.js', 124 | exposes: { 125 | // EntryModule is the key of the entry.module.ts file and corresponds to the exported EntryModule class from this file. 126 | EntryModule: 'apps/dashboard-mfe/src/app/remote-entry/entry.module.ts', 127 | // the EntryComponent is key of file entry.module.ts, and match to exported EntryComponent class from that file. 128 | EntryComponent: 'apps/dashboard-mfe/src/app/remote-entry/entry.component.ts', 129 | }, 130 | [...] 131 | }); 132 | } 133 | ``` 134 | 135 | > 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 the `loadMfe()` function; 136 | 137 | > 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 the `loadMfe()` function; 138 | 139 | 4. You must follow the rule that only one Component must be declared for an exposed Module. This is known as SCAM (**S**ingle **C**omponent **A**ngular **M**odule) pattern. 140 | 141 | ## Configuring 142 | 143 | Add the **ngx-mfe** library to a shared property in the ModuleFederationPlugin inside webpack.config.js file for each application in your workspace. 144 | 145 | ```typescript 146 | module.exports = { 147 | [...] 148 | plugins: [ 149 | [...] 150 | new ModuleFederationPlugin({ 151 | remotes: {}, 152 | shared: share({ 153 | [...] 154 | "ngx-mfe": { 155 | singleton: true, 156 | strictVersion: true, 157 | requiredVersion: 'auto', 158 | includeSecondaries: true 159 | }, 160 | ...sharedMappings.getDescriptors(), 161 | }), 162 | library: { 163 | type: 'module' 164 | }, 165 | }), 166 | [...] 167 | ], 168 | [...] 169 | }; 170 | ``` 171 | 172 | 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: 173 | 174 | > For feature modules just import `MfeModule` without options, where, you may need the functionality of the library, for example, the `MfeOutlet` directive. 175 | 176 | For core / app module: 177 | ```typescript 178 | @NgModule({ 179 | imports: [ 180 | MfeModule.forRoot({ 181 | mfeConfig: { 182 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js", 183 | "loaders-mfe": "http://localhost:4202/remoteEntry.js", 184 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js" 185 | }, 186 | preload: ['loaders-mfe', 'fallbacks-mfe'], 187 | loader: { 188 | app: 'loaders', 189 | module: 'SpinnerModule', 190 | component: 'SpinnerComponent', 191 | }, 192 | loaderDelay: 500, 193 | fallback: { 194 | app: 'fallbacks', 195 | module: 'MfeFallbackModule', 196 | component: 'MfeFallbackComponent', 197 | }, 198 | }), 199 | ], 200 | }) 201 | export class AppModule {} 202 | ``` 203 | 204 | For feature module: 205 | ```typescript 206 | @NgModule({ 207 | imports: [ 208 | MfeModule, 209 | ], 210 | }) 211 | export class Feature1Module {} 212 | ``` 213 | 214 | ### List of all available options: 215 | 216 | - **mfeConfig** 217 | 218 | ---------------- 219 | 220 | **Sync variant of providing mfeConfig:** 221 | 222 | ---------------- 223 | 224 | 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](https://github.com/dkhrunov/ngx-mfe/blob/master/projects/ngx-mfe/src/lib/registry/mfe-registry.ts). 225 | 226 | **Key** it's the name same specified in webpack.config.js of MFE (Remote) in option name in `ModuleFederationPlugin`. 227 | 228 | **Value** set the following pattern: `{url}/{remoteEntrypointFilename}`. 229 | 230 | - `url` is the url where the remote application is hosted. 231 | 232 | - `remoteEntrypointFilename` is the filename supplied in the remote's webpack configuration. 233 | 234 | Example 235 | 236 | > (Deprecated from v15.1.0) You can get `MfeRegistry` from DI : 237 | > 238 | > ```typescript 239 | > class AppComponent { 240 | > 241 | > constructor(public mfeRegistry: MfeRegistry) {} 242 | > } 243 | > ``` 244 | 245 | You can even get instace of `MfeRegistry` like this: 246 | 247 | ```typescript 248 | const mfeRegistry: MfeRegistry = MfeRegistry.instace; 249 | ``` 250 | 251 | ---------------- 252 | 253 | **Async variant of providing mfeConfig:** 254 | 255 | ---------------- 256 | 257 | > NOTE: The application will wait for initialization and completes when the promise resolves or the observable completes. 258 | > 259 | > Because under the hood used `APP_INITIALIZER` injection token with useFactory that returns Observale or Promise. [More about `APP_INITIALIZER`](https://angular.io/api/core/APP_INITIALIZER) 260 | 261 | 262 | Also you can provide mfeConfig with loading it from external resource as `Observale` or `Promise`, for this you should provide this type of object: 263 | 264 | ```typescript 265 | type NgxMfeAsyncConfig = { 266 | /** 267 | * A function to invoke to load a `MfeConfig`. The function is invoked with 268 | * resolved values of `token`s in the `deps` field. 269 | */ 270 | useLoader: (...deps: any[]) => Observable | Promise; 271 | /** 272 | * A list of `token`s to be resolved by the injector. The list of values is then 273 | * used as arguments to the `useLoader` function. 274 | */ 275 | deps?: any[]; 276 | }; 277 | ``` 278 | 279 | For example: 280 | 281 | ```typescript 282 | mfeConfig: { 283 | useLoader: (http: HttpClient): Observable => 284 | http.get('/manifest.json'), 285 | deps: [HttpClient] 286 | }, 287 | ``` 288 | 289 | 290 | - **preload** (Optional) - a list of micro-frontend names, their bundles (remoteEntry.js) will be loaded and saved in the cache when the application starts. 291 | 292 | Next options are only works in plugin-based approach with `MfeOutletDirective`: 293 | 294 | - **loaderDelay** (Optional) - Specifies the minimum loader display time in ms. This is to avoid flickering when the micro-frontend loads very quickly. 295 | 296 | *By default is 0.* 297 | 298 | - **loader** (Optional) - Displayed when loading the micro-frontend. Implements the `RemoteComponent` interface. 299 | 300 | *Example:* 301 | ```typescript 302 | // Globally uses the "SpinnerComponent" loader component declared in the "SpinnerModule" of the app "loaders". 303 | loader: { 304 | app: 'loaders', 305 | module: 'SpinnerModule', 306 | component: 'SpinnerComponent', 307 | }, 308 | ``` 309 | 310 | > For better UX, add loader micro-frontends to the `preload`. 311 | 312 | - **fallback** (Optional) - Displayed when loading or compiling a micro-frontend with an error. Implements the `RemoteComponent` interface. 313 | 314 | *Example:* 315 | ```typescript 316 | // Globally uses the "MfeFallbackComponent" fallback component declared in the "MfeFallbackModule" of the app "fallbacks". 317 | fallback: { 318 | app: 'fallbacks', 319 | module: 'MfeFallbackModule', 320 | component: 'MfeFallbackComponent', 321 | }, 322 | ``` 323 | 324 | > For better UX, add fallback micro-frontends to the `preload`. 325 | 326 | You can get all configured options by injecting `NGX_MFE_OPTIONS` by DI: 327 | 328 | ```typescript 329 | class AppComponent { 330 | constructor(@Inject(NGX_MFE_OPTIONS) public options: NgxMfeOptions) {} 331 | } 332 | ``` 333 | 334 | ## Display MFE in HTML template / plugin-based approach 335 | 336 | This approach allows us to load micro-frontends directly from HTML. 337 | 338 | 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. 339 | 340 | > More about plugin-based approach [here](https://dekh.medium.com/angular-micro-frontend-architecture-part-3-3-mfe-plugin-based-approach-f36dc9849b0). 341 | 342 | > Full code of this example can be found at https://github.com/dkhrunov/ngx-mfe-test. 343 | 344 | Example app: 345 | 346 | ![image](https://user-images.githubusercontent.com/25565058/187071276-11e1dd5c-6fe4-4d7c-94df-2bf74331d900.png) 347 | 348 | An example webpack.config.js that exposes the "MfeTestComponent" (brown border in the screenshot above): 349 | 350 | ```js 351 | // webpack.config.js 352 | return { 353 | [...] 354 | resolve: { 355 | alias: sharedMappings.getAliases(), 356 | }, 357 | plugins: [ 358 | new ModuleFederationPlugin({ 359 | name: 'test', 360 | exposes: { 361 | MfeTestModule: 'apps/test/src/app/mfe-test/mfe-test.module.ts', 362 | MfeTestComponent: 'apps/test/src/app/mfe-test/mfe-test.component.ts', 363 | }, 364 | filename: 'remoteEntry', 365 | shared: share({ ... }), 366 | }), 367 | sharedMappings.getPlugin(), 368 | ], 369 | }; 370 | ``` 371 | 372 | 1. Just display the component "MfeTestComponent" inside other MFE component "Form" from "address-form" app: 373 | 374 | One variant: 375 | ```html 376 | 381 | 382 | ``` 383 | 384 | Other variant: 385 | ```html 386 | 393 | 394 | ``` 395 | 396 | > These two examples are equal and display the MFE "MfeTestComponent". 397 | 398 | 2. You can pass/bind `@Input` and `@Output` props to MFE component: 399 | 400 | ```html 401 | 402 | 411 | ``` 412 | 413 | ```typescript 414 | // form.component.ts file 415 | @Component({ 416 | selector: 'app-form', 417 | templateUrl: './form.component.html', 418 | styleUrls: ['./form.component.scss'], 419 | changeDetection: ChangeDetectionStrategy.OnPush, 420 | }) 421 | export class FormComponent { 422 | [...] 423 | // timer emits after 1 second, then every 2 seconds 424 | public readonly text$: Observable = timer(1000, 2000); 425 | 426 | // on click log to console event 427 | public onClick(event: MouseEvent): void { 428 | console.log('clicked', event); 429 | } 430 | [...] 431 | } 432 | ``` 433 | 434 | > If you try to bind a @Output() property that is not in the component, then an error will fall into the console: 435 | > "Output **someOutput** is not output of **SomeComponent**." 436 | > 437 | > If you try to pass a non-function, then an error will fall into the console: 438 | > "Output **someOutput** must be a function." 439 | 440 | 3. To override the default loader delay, configured in `MfeModule.forRoot({ ... })`, provide custom number in ms to property `loaderDelay`: 441 | 442 | ```html 443 | 451 | ``` 452 | 453 | 4. 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`: 454 | 455 | ```html 456 | 465 | 466 | 467 |
loading...
468 |
469 | 470 | 471 |
Ooops! Something went wrong
472 |
473 | ``` 474 | 475 | ```html 476 | 477 | 483 | 484 | 485 | 492 | 493 | 494 | ``` 495 | 496 | 6. You can also provide a custom injector for a component like this: 497 | 498 | ```html 499 | 505 | ``` 506 | 507 | ## Display Angular v14 Standalone components 508 | 509 | Example app: 510 | 511 | ![image](https://user-images.githubusercontent.com/25565058/187071276-11e1dd5c-6fe4-4d7c-94df-2bf74331d900.png) 512 | 513 | An example webpack.config.js that exposes the "StandaloneComponent" (green border in the screenshot above): 514 | 515 | ```js 516 | // webpack.config.js 517 | return { 518 | [...] 519 | resolve: { 520 | alias: sharedMappings.getAliases(), 521 | }, 522 | plugins: [ 523 | new ModuleFederationPlugin({ 524 | name: 'test', 525 | exposes: { 526 | [...] 527 | StandaloneComponent: 'apps/test/src/app/standalone/standalone.component.ts', 528 | }, 529 | filename: 'remoteEntry', 530 | shared: share({ ... }), 531 | }), 532 | sharedMappings.getPlugin(), 533 | ], 534 | }; 535 | ``` 536 | 537 | ```typescript 538 | // standalone.component.ts 539 | 540 | import { Component } from '@angular/core'; 541 | import { CommonModule } from '@angular/common'; 542 | 543 | @Component({ 544 | selector: 'app-standalone', 545 | standalone: true, 546 | imports: [CommonModule], 547 | template: `

Standalone component works!

`, 548 | styles: [], 549 | }) 550 | export class StandaloneComponent {} 551 | ``` 552 | 553 | ```html 554 | 555 | [...] 556 |

Angular v14 Standalone component loaded as MFE:

557 | 561 | ``` 562 | 563 | ## Passing Data to the MFE Component via mfeOutlet directive 564 | 565 | 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: 566 | 567 | 1. As I wrote above through the properties `[mfeOutletInputs]` `[mfeOutletOutputs]` 568 | 569 | component.html: 570 | ```html 571 | 578 | 579 | ``` 580 | 581 | component.ts 582 | ```typescript 583 | @Component({ ... }) 584 | export class Component { 585 | public text$ = new BehaviorSubject('Test string'); 586 | 587 | constructor() { } 588 | 589 | public onClick(bool: MouseEvent): void { 590 | console.log('login', bool); 591 | } 592 | } 593 | ``` 594 | 595 | 2. 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. 596 | 597 | component.html: 598 | ```html 599 | 605 | 606 | ``` 607 | 608 | component.ts 609 | ```typescript 610 | @Component({ ... }) 611 | export class Component { 612 | public readonly testComponentInjector: Injector; 613 | 614 | constructor(private readonly _injector: Injector) { 615 | this.testComponentInjector = Injector.create({ 616 | parent: this._injector, 617 | providers: [ 618 | { 619 | provide: TEST_DATA, 620 | useValue: data, 621 | }, 622 | ], 623 | }); 624 | } 625 | } 626 | ``` 627 | 628 | ## Load MFE by Route 629 | 630 | To use micro-frontends in Routing, you must import and apply the helper function called `loadMfe`, like in the example below: 631 | 632 | ```typescript 633 | import { NgModule } from '@angular/core'; 634 | import { RouterModule, Routes } from '@angular/router'; 635 | import { loadMfe } from '@dkhrunov/ng-mfe'; 636 | 637 | const routes: Routes = [ 638 | { 639 | path: 'dashboard', 640 | loadChildren: () => loadMfe('dashboard-mfe', 'EntryModule'), 641 | }, 642 | ]; 643 | 644 | @NgModule({ 645 | imports: [RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking' })], 646 | exports: [RouterModule], 647 | }) 648 | export class AppRoutingModule {} 649 | ``` 650 | 651 | ## Changelog 652 | 653 | ### Changes in __v2.1.0__ 654 | Fixed: 655 | - Fix error, if the fallback is also unavailable, then simply clear the view; 656 | 657 | Refactored: 658 | - Renamed `MfeService` to `RemoteComponentLoader`; 659 | - Renamed `MfeComponentsCache` to `RemoteComponentsCache`; 660 | - Renamed `ModularRemoteComponent` type to `RemoteComponentWithModule`; 661 | - Wrapped to `ngZone.runOutside` the `loadMfe` function calls inside the `RemoteComponentLoader`; 662 | - Added new type `ComponentWithNgModuleRef`, that holds component class `Type` and `NgModuleRef`; 663 | - Changed cached value for `RemoteComponentWithModule` from `ComponentFactory` to `ComponentWithNgModuleRef`; 664 | - In `RemoteComponentLoader` (old name `MfeService`) renamed function `loadModularComponent` to `loadComponentWithModule` 665 | - Changed return type of method `loadComponentWithModule` inside class `RemoteComponentLoader` from `Promise>` to `Promise>`; 666 | 667 | ### Changes in __v2.0.0__ (_Breaking changes_) 668 | 669 | __Why has the API changed?__ - The problem is that when you use the `[mfeOutlet]` directive [issue](https://github.com/dkhrunov/ngx-mfe/issues/7), 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. 670 | 671 | #### General: 672 | - 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. 673 | 674 | __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; 675 | 676 | - 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__); 677 | 678 | - `MFE string` has been replaced by a new type `RemoteComponent`; 679 | 680 | - The `validateMfe` function has been removed (__it was used until version 2.0.0__); 681 | 682 | - The `loader` and `fallback` properties in the `NgxMfeOptions` has been changed from `MFE string` to `RemoteComponent` type: 683 | 684 | Before v2.0.0: 685 | ```typescript 686 | @NgModule({ 687 | declarations: [AppComponent], 688 | imports: [ 689 | BrowserModule, 690 | BrowserAnimationsModule, 691 | MfeModule.forRoot({ 692 | mfeConfig: { 693 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js", 694 | "loaders-mfe": "http://localhost:4202/remoteEntry.js", 695 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js" 696 | }, 697 | loader: 'loaders/spinner', 698 | fallback: 'fallbacks/mfe-fallback', 699 | }), 700 | ], 701 | bootstrap: [AppComponent], 702 | }) 703 | export class AppModule {} 704 | ``` 705 | 706 | Since v2.0.0: 707 | ```typescript 708 | @NgModule({ 709 | declarations: [AppComponent], 710 | imports: [ 711 | BrowserModule, 712 | BrowserAnimationsModule, 713 | MfeModule.forRoot({ 714 | mfeConfig: { 715 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js", 716 | "loaders-mfe": "http://localhost:4202/remoteEntry.js", 717 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js" 718 | }, 719 | loader: { 720 | app: 'loaders', 721 | module: 'SpinnerModule', 722 | component: 'SpinnerComponent', 723 | }, 724 | fallback: { 725 | app: 'fallbacks', 726 | module: 'MfeFallbackModule', 727 | component: 'MfeFallbackComponent', 728 | }, 729 | }), 730 | ], 731 | bootstrap: [AppComponent], 732 | }) 733 | export class AppModule {} 734 | ``` 735 | 736 | - Removed `moduleName` property from `LoadMfeOptions` type; 737 | - 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. 738 | ```javascript 739 | // webpack.config.js 740 | exposes: { 741 | // LoginModule name of the exposed file login.module.ts 742 | LoginModule: 'apps/auth-mfe/src/app/login/login.module.ts', 743 | }, 744 | ``` 745 | 746 | Before v2.0.0: 747 | ```typescript 748 | loadMfe('auth-mfe/login-module') 749 | ``` 750 | 751 | Since v2.0.0: 752 | ```typescript 753 | loadMfe('auth-mfe' 'LoginModule') 754 | ``` 755 | 756 | #### LoadMfe function: 757 | - Arguments changed in `LoadMfe` function: 758 | 759 | Before v2.0.0: 760 | ```typescript 761 | async function loadMfe(mfeString: string, options?: LoadMfeOptions): Promise> {} 762 | ``` 763 | 764 | Since v2.0.0: 765 | ```typescript 766 | async function loadMfe(remoteApp: string, exposedFile: string, options?: LoadMfeOptions): Promise> {} 767 | ``` 768 | - `remoteApp` - is the name of the remote app as specified in the webpack.config.js file in the ModuleFederationPlugin in the __name__ property; 769 | - `exposedFile` - is the key (or name) of the exposed file specified in the webpack.config.js file in the ModuleFederationPlugin in the __exposes__ property; 770 | 771 | #### MfeOutlet directive: 772 | - Since the `Mfe string` has been removed from the library, the API of `[mfeOutlet]` directive has changed: 773 | 1. `mfeOutletLoader` and `mfeOutletFallback` now accept only `TemplateRef`, more details below. 774 | 2. 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 specify `mfeOutletModule` with the name of the open module file in which the component is declared for the ModuleFederationPlugin in webpack.config. 775 | 3. 776 | - `@Input('mfeOutletOptions')' options` changed type from `MfeComponentFactoryResolverOptions` to `LoadMfeOptions`; 777 | - `@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: 778 | 779 | ```html 780 | 781 | 787 | 788 | 789 | 790 | 791 | 792 | 799 | 800 | 801 | 802 | 803 | 809 | 810 | 811 | 812 | 813 |
loading...
814 |
815 | ``` 816 | 817 | #### MfeComponentFactoryResolver: 818 | - The `MfeComponentFactoryResolver` has been replaced with `MfeService` and the API has been changed; 819 | - The `MfeComponentFactoryResolverOptions` type has been removed; 820 | 821 | #### MfeComponentCache 822 | - Now the `MfeComponentCache` not only saves `ComponentFactory` but also `Type`; 823 | - In version 2.1.0 `ComponentFactory` was replaced to `ComponentWithNgModuleRef`; 824 | 825 | #### DynamicComponentBinding 826 | - The `bindInputs()` and `bindOutputs()` methods now require `ComponentRef` in the first argument, `MfeOutletInputs`/`MfeOutletOutputs` are method dependent in the second, and the third argument has been removed; 827 | - The `DynamicComponentInputs` and `DynamicComponentOutputs` types have been removed because these types are replaced in `bindInputs()` and `bindOutputs()` respectively by the `ComponentRef` type; 828 | - The `validateInputs()` method has been removed; 829 | - The `validateOutputs()` method is now private; 830 | 831 | --------------- 832 | 833 | ### Changes in __v1.1.0__: 834 | 835 | - Deleted the `loadMfeComponent` helper function; 836 | - Deleted the `parseMfeString` helper function; 837 | - Renamed the `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; 838 | - Renamed the `MfeService` to `MfeComponentFactoryResolver`; 839 | - `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. 840 | - Added new Input prop to the `MfeOutletDirective` - `options: MfeComponentFactoryResolver`, this parameter provided to `resolveComponentFactory` method of the `MfeComponentFactoryResolver` when resolving the component factory of MFE. 841 | - 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. 842 | 843 | 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. 844 | 845 | 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' }`; 846 | 847 | --------------- 848 | 849 | ### Changes in __v1.0.8__: 850 | 851 | - `IMfeModuleRootOptions` interface renamed to `NgxMfeOptions`; 852 | - Property `delay` in the `NgxMfeOptions` renamed to `loaderDelay`; 853 | - `OPTIONS` injection token renamed to `NGX_MFE_OPTIONS`; 854 | 855 | --------------- 856 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-mfe": { 7 | "projectType": "library", 8 | "root": "projects/ngx-mfe", 9 | "sourceRoot": "projects/ngx-mfe/src", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "project": "projects/ngx-mfe/ng-package.json" 16 | }, 17 | "configurations": { 18 | "production": { 19 | "tsConfig": "projects/ngx-mfe/tsconfig.lib.prod.json" 20 | }, 21 | "development": { 22 | "tsConfig": "projects/ngx-mfe/tsconfig.lib.json" 23 | } 24 | }, 25 | "defaultConfiguration": "production" 26 | }, 27 | "test": { 28 | "builder": "@angular-devkit/build-angular:karma", 29 | "options": { 30 | "main": "projects/ngx-mfe/src/test.ts", 31 | "tsConfig": "projects/ngx-mfe/tsconfig.spec.json", 32 | "karmaConfig": "projects/ngx-mfe/karma.conf.js" 33 | } 34 | } 35 | } 36 | } 37 | }, 38 | "cli": { 39 | "analytics": "f4cceced-74e6-402c-ba25-05b16ec1da51" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /migration-guide-to-v2.md: -------------------------------------------------------------------------------- 1 | # Migration Guide to v2 2 | 3 | __Why has the API changed?__ - The problem is that when you use the `[mfeOutlet]` directive [issue](https://github.com/dkhrunov/ngx-mfe/issues/7), 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. 4 | 5 | ## General: 6 | - 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. 7 | 8 | __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; 9 | 10 | - 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__); 11 | 12 | - `MFE string` has been replaced by a new type `RemoteComponent`; 13 | 14 | - The `validateMfe` function has been removed (__it was used until version 2.0.0__); 15 | 16 | - The `loader` and `fallback` properties in the `NgxMfeOptions` has been changed from `MFE string` to `RemoteComponent` type: 17 | 18 | Before v2.0.0: 19 | ```typescript 20 | @NgModule({ 21 | declarations: [AppComponent], 22 | imports: [ 23 | BrowserModule, 24 | BrowserAnimationsModule, 25 | MfeModule.forRoot({ 26 | mfeConfig: { 27 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js", 28 | "loaders-mfe": "http://localhost:4202/remoteEntry.js", 29 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js" 30 | }, 31 | loader: 'loaders/spinner', 32 | fallback: 'fallbacks/mfe-fallback', 33 | }), 34 | ], 35 | bootstrap: [AppComponent], 36 | }) 37 | export class AppModule {} 38 | ``` 39 | 40 | Since v2.0.0: 41 | ```typescript 42 | @NgModule({ 43 | declarations: [AppComponent], 44 | imports: [ 45 | BrowserModule, 46 | BrowserAnimationsModule, 47 | MfeModule.forRoot({ 48 | mfeConfig: { 49 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js", 50 | "loaders-mfe": "http://localhost:4202/remoteEntry.js", 51 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js" 52 | }, 53 | loader: { 54 | app: 'loaders', 55 | module: 'SpinnerModule', 56 | component: 'SpinnerComponent', 57 | }, 58 | fallback: { 59 | app: 'fallbacks', 60 | module: 'MfeFallbackModule', 61 | component: 'MfeFallbackComponent', 62 | }, 63 | }), 64 | ], 65 | bootstrap: [AppComponent], 66 | }) 67 | export class AppModule {} 68 | ``` 69 | 70 | - Removed `moduleName` property from `LoadMfeOptions` type; 71 | - 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. 72 | ```javascript 73 | // webpack.config.js 74 | exposes: { 75 | // LoginModule name of the exposed file login.module.ts 76 | LoginModule: 'apps/auth-mfe/src/app/login/login.module.ts', 77 | }, 78 | ``` 79 | 80 | Before v2.0.0: 81 | ```typescript 82 | loadMfe('auth-mfe/login-module') 83 | ``` 84 | 85 | Since v2.0.0: 86 | ```typescript 87 | loadMfe('auth-mfe' 'LoginModule') 88 | ``` 89 | 90 | ## LoadMfe function: 91 | - Arguments changed in `LoadMfe` function: 92 | 93 | Before v2.0.0: 94 | ```typescript 95 | async function loadMfe(mfeString: string, options?: LoadMfeOptions): Promise> {} 96 | ``` 97 | 98 | Since v2.0.0: 99 | ```typescript 100 | async function loadMfe(remoteApp: string, exposedFile: string, options?: LoadMfeOptions): Promise> {} 101 | ``` 102 | - `remoteApp` - is the name of the remote app as specified in the webpack.config.js file in the ModuleFederationPlugin in the __name__ property; 103 | - `exposedFile` - is the key (or name) of the exposed file specified in the webpack.config.js file in the ModuleFederationPlugin in the __exposes__ property; 104 | 105 | ## MfeOutlet directive: 106 | - Since the `Mfe string` has been removed from the library, the API of `[mfeOutlet]` directive has changed: 107 | 1. `mfeOutletLoader` and `mfeOutletFallback` now accept only `TemplateRef`, more details below. 108 | 2. 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 specify `mfeOutletModule` with the name of the open module file in which the component is declared for the ModuleFederationPlugin in webpack.config. 109 | 3. 110 | - `@Input('mfeOutletOptions')' options` changed type from `MfeComponentFactoryResolverOptions` to `LoadMfeOptions`; 111 | - `@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: 112 | 113 | ```html 114 | 115 | 121 | 122 | 123 | 124 | 125 | 126 | 133 | 134 | 135 | 136 | 137 | 143 | 144 | 145 | 146 | 147 |
loading...
148 |
149 | ``` 150 | 151 | ## MfeComponentFactoryResolver: 152 | - The `MfeComponentFactoryResolver` has been replaced with `MfeService` and the API has been changed; 153 | - The `MfeComponentFactoryResolverOptions` type has been removed; 154 | 155 | ## MfeComponentCache 156 | - Now the `MfeComponentCache` not only saves `ComponentFactory` but also `Type`; 157 | - In version 2.1.0 `ComponentFactory` was replaced to `ComponentWithNgModuleRef`; 158 | 159 | ## DynamicComponentBinding 160 | - The `bindInputs()` and `bindOutputs()` methods now require `ComponentRef` in the first argument, `MfeOutletInputs`/`MfeOutletOutputs` are method dependent in the second, and the third argument has been removed; 161 | - The `DynamicComponentInputs` and `DynamicComponentOutputs` types have been removed because these types are replaced in `bindInputs()` and `bindOutputs()` respectively by the `ComponentRef` type; 162 | - The `validateInputs()` method has been removed; 163 | - The `validateOutputs()` method is now private; -------------------------------------------------------------------------------- /migration-guide.md: -------------------------------------------------------------------------------- 1 | # Migration Guides 2 | 3 | I'm trying to make as few breaking changes as possible. However, sometimes I need to break backwards compatibility in order to add a new feature or fix a bug. 4 | 5 | - [From version 1 and 2](./migration-guide-to-v2.md.md) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-mfe", 3 | "version": "15.1.0", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "GitHub", 7 | "url": "https://github.com/dkhrunov/ngx-mfe" 8 | }, 9 | "author": { 10 | "name": "Denis Khrunov", 11 | "email": "therealpanda98@gmail.com" 12 | }, 13 | "keywords": [ 14 | "microfrontend", 15 | "mfe", 16 | "angular-microfrontend", 17 | "angular-mfe", 18 | "microcomponent", 19 | "angular" 20 | ], 21 | "scripts": { 22 | "ng": "ng", 23 | "start": "ng serve", 24 | "build": "ng build", 25 | "watch": "ng build --watch --configuration development", 26 | "test": "ng test" 27 | }, 28 | "private": true, 29 | "dependencies": { 30 | "@angular-architects/module-federation": "^19.0.3", 31 | "@angular/animations": "^19.2.5", 32 | "@angular/common": "^19.2.5", 33 | "@angular/compiler": "^19.2.5", 34 | "@angular/core": "^19.2.5", 35 | "@angular/forms": "^19.2.5", 36 | "@angular/platform-browser": "^19.2.5", 37 | "@angular/platform-browser-dynamic": "^19.2.5", 38 | "@angular/router": "^19.2.5", 39 | "rxjs": "~7.5.5", 40 | "tslib": "^2.3.0", 41 | "zone.js": "~0.15.0" 42 | }, 43 | "devDependencies": { 44 | "@angular-devkit/build-angular": "^19.2.6", 45 | "@angular/cli": "^19.2.6", 46 | "@angular/compiler-cli": "^19.2.5", 47 | "@types/jasmine": "~3.8.0", 48 | "@types/node": "^12.11.1", 49 | "jasmine-core": "~3.8.0", 50 | "karma": "~6.3.0", 51 | "karma-chrome-launcher": "~3.1.0", 52 | "karma-coverage": "~2.0.3", 53 | "karma-jasmine": "~4.0.0", 54 | "karma-jasmine-html-reporter": "~1.7.0", 55 | "ng-packagr": "^19.2.1", 56 | "typescript": "^5.4.2" 57 | } 58 | } -------------------------------------------------------------------------------- /projects/ngx-mfe/LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Denis Khrunov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /projects/ngx-mfe/README.md: -------------------------------------------------------------------------------- 1 | # Angular micro-frontend library - ngx-mfe 2 | 3 | A library for working with MFE in Angular in a plugin-based approach and with Angular routing. 4 | 5 | > If you have production build issues check this [issue](https://github.com/dkhrunov/ngx-mfe/issues/7). __This issue has been fixed in version 2.0.0.__ 6 | 7 | Have problems with updates? Check out the [migration guides](../../migration-guide.md). 8 | 9 | ## Contents 10 | 11 | - [Version Compliance](#version-compliance) 12 | - [Motivation](#motivation) 13 | - [Features](#features) 14 | - [Examples](#examples) 15 | - [Conventions](#conventions) 16 | - [Configuring](#configuring) 17 | - [Display MFE in HTML template / plugin-based approach](#display-mfe-in-html-template--plugin-based-approach) 18 | - [Display Angular v14 Standalone Components](#display-angular-v14-standalone-components) 19 | - [Passing Data to the MFE Component via mfeOutlet directive](#passing-data-to-the-mfe-component-via-mfeoutlet-directive) 20 | - [Load MFE by Route](#load-mfe-by-route) 21 | - [Changelog](#changelog) 22 | 23 | ## Version Compliance 24 | ngx-mfe | v1.0.0 | v1.0.5 | v2.0.0 | v3.0.0 | 25 | --------------------------------------| ------- | ------- | ------- | ------- | 26 | Angular | v12.0.0 | v13.0.0 | v13.0.0 | v14.0.0 | 27 | @angular-architects/module-federation | v12.0.0 | v14.0.0 | v14.0.0 | v14.3.0 | 28 | 29 | **Since v15.0.0 version of ngx-mfe library is compatible with Angular version** 30 | 31 | ## Motivation 32 | 33 | 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 **M**icro**F**ront**E**nd (MFE) applications. 34 | 35 | The **ngx-mfe** is an extension of the functionality of the [@angular-architects/module-federation](https://www.npmjs.com/package/@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. 36 | 37 | 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. 38 | 39 | > You can use both **ngx-mfe** and **@angular-architects/module-federation** libs together in the same project. 40 | 41 | ## Features 42 | 43 | 🔥 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. 44 | 45 | 🔥 Easy to use, just declare structural directive `*mfeOutlet` in your template. 46 | 47 | 🔥 Supports Angular Standalone Components. 48 | 49 | 🔥 More convenient way to load MFE via Angular Routing. 50 | 51 | 🔥 It's easy to set up different remoteEntryUrl MFEs for different builds (dev/prod/etc). 52 | 53 | ## Examples 54 | 55 | - [Example of an application using ngx-mfe v1.](https://github.com/dkhrunov/ngx-mfe-test/tree/lesson_4) 56 | - [Example of an application using ngx-mfe v2.](https://github.com/dkhrunov/ngx-mfe-test/tree/update-to-ngx-mfe-v2) 57 | - [Example of an application using ngx-mfe v3 with Angular 14 Standalone Components.](https://github.com/dkhrunov/ngx-mfe-test) 58 | - [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.](https://dekh.medium.com/angular-micro-frontend-architecture-part-1-3-the-concept-of-micro-frontend-architecture-2ff56a5ac264) 59 | 60 | ## Conventions 61 | 62 | 1. To display a standalone MFE component, you only need to __the component file itself__. 63 | 64 | > A standalone component is a component that does not have any dependencies provided or imported in the module where that component is declared. 65 | > 66 | > Since Angular v14 standalone component it is component that marked with `standalone: true` in `@Component({...})` decorator. 67 | 68 | When you display a standalone MFE component through `[mfeOutlet]` directive you must omit `[mfeOutletModule]` input. 69 | 70 | ```typescript 71 | // Standalone Component - standalone.component.ts 72 | import { Component } from '@angular/core'; 73 | import { CommonModule } from '@angular/common'; 74 | 75 | @Component({ 76 | selector: 'app-standalone', 77 | standalone: true, 78 | imports: [CommonModule], 79 | template: `

Standalone component works!

`, 80 | styles: [], 81 | }) 82 | export class StandaloneComponent {} 83 | ``` 84 | 85 | ```typescript 86 | // dashboard-mfe webpack.config 87 | { 88 | new ModuleFederationPlugin({ 89 | name: 'dashboard-mfe', 90 | filename: 'remoteEntry.js', 91 | exposes: { 92 | StandaloneComponent: 'apps/dashboard-mfe/src/app/standalone.component.ts', 93 | }, 94 | [...] 95 | }); 96 | } 97 | ``` 98 | 99 | ```html 100 | 101 | 105 | 106 | ``` 107 | 108 | 2. 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. 109 | 110 | > This approach is widely used and recommended. 111 | 112 | 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. 113 | 114 | 3. 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. 115 | 116 | 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. 117 | 118 | ```typescript 119 | // webpack.config 120 | { 121 | new ModuleFederationPlugin({ 122 | name: 'dashboard-mfe', 123 | filename: 'remoteEntry.js', 124 | exposes: { 125 | // EntryModule is the key of the entry.module.ts file and corresponds to the exported EntryModule class from this file. 126 | EntryModule: 'apps/dashboard-mfe/src/app/remote-entry/entry.module.ts', 127 | // the EntryComponent is key of file entry.module.ts, and match to exported EntryComponent class from that file. 128 | EntryComponent: 'apps/dashboard-mfe/src/app/remote-entry/entry.component.ts', 129 | }, 130 | [...] 131 | }); 132 | } 133 | ``` 134 | 135 | > 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 the `loadMfe()` function; 136 | 137 | > 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 the `loadMfe()` function; 138 | 139 | 4. You must follow the rule that only one Component must be declared for an exposed Module. This is known as SCAM (**S**ingle **C**omponent **A**ngular **M**odule) pattern. 140 | 141 | ## Configuring 142 | 143 | Add the **ngx-mfe** library to a shared property in the ModuleFederationPlugin inside webpack.config.js file for each application in your workspace. 144 | 145 | ```typescript 146 | module.exports = { 147 | [...] 148 | plugins: [ 149 | [...] 150 | new ModuleFederationPlugin({ 151 | remotes: {}, 152 | shared: share({ 153 | [...] 154 | "ngx-mfe": { 155 | singleton: true, 156 | strictVersion: true, 157 | requiredVersion: 'auto', 158 | includeSecondaries: true 159 | }, 160 | ...sharedMappings.getDescriptors(), 161 | }), 162 | library: { 163 | type: 'module' 164 | }, 165 | }), 166 | [...] 167 | ], 168 | [...] 169 | }; 170 | ``` 171 | 172 | 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: 173 | 174 | > For feature modules just import `MfeModule` without options, where, you may need the functionality of the library, for example, the `MfeOutlet` directive. 175 | 176 | For core / app module: 177 | ```typescript 178 | @NgModule({ 179 | imports: [ 180 | MfeModule.forRoot({ 181 | mfeConfig: { 182 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js", 183 | "loaders-mfe": "http://localhost:4202/remoteEntry.js", 184 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js" 185 | }, 186 | preload: ['loaders-mfe', 'fallbacks-mfe'], 187 | loader: { 188 | app: 'loaders', 189 | module: 'SpinnerModule', 190 | component: 'SpinnerComponent', 191 | }, 192 | loaderDelay: 500, 193 | fallback: { 194 | app: 'fallbacks', 195 | module: 'MfeFallbackModule', 196 | component: 'MfeFallbackComponent', 197 | }, 198 | }), 199 | ], 200 | }) 201 | export class AppModule {} 202 | ``` 203 | 204 | For feature module: 205 | ```typescript 206 | @NgModule({ 207 | imports: [ 208 | MfeModule, 209 | ], 210 | }) 211 | export class Feature1Module {} 212 | ``` 213 | 214 | ### List of all available options: 215 | 216 | - **mfeConfig** 217 | 218 | ---------------- 219 | 220 | **Sync variant of providing mfeConfig:** 221 | 222 | ---------------- 223 | 224 | 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](https://github.com/dkhrunov/ngx-mfe/blob/master/projects/ngx-mfe/src/lib/registry/mfe-registry.ts). 225 | 226 | **Key** it's the name same specified in webpack.config.js of MFE (Remote) in option name in `ModuleFederationPlugin`. 227 | 228 | **Value** set the following pattern: `{url}/{remoteEntrypointFilename}`. 229 | 230 | - `url` is the url where the remote application is hosted. 231 | 232 | - `remoteEntrypointFilename` is the filename supplied in the remote's webpack configuration. 233 | 234 | Example 235 | 236 | > (Deprecated from v15.1.0) You can get `MfeRegistry` from DI : 237 | > 238 | > ```typescript 239 | > class AppComponent { 240 | > 241 | > constructor(public mfeRegistry: MfeRegistry) {} 242 | > } 243 | > ``` 244 | 245 | You can even get instace of `MfeRegistry` like this: 246 | 247 | ```typescript 248 | const mfeRegistry: MfeRegistry = MfeRegistry.instace; 249 | ``` 250 | 251 | ---------------- 252 | 253 | **Async variant of providing mfeConfig:** 254 | 255 | ---------------- 256 | 257 | > NOTE: The application will wait for initialization and completes when the promise resolves or the observable completes. 258 | > 259 | > Because under the hood used `APP_INITIALIZER` injection token with useFactory that returns Observale or Promise. [More about `APP_INITIALIZER`](https://angular.io/api/core/APP_INITIALIZER) 260 | 261 | 262 | Also you can provide mfeConfig with loading it from external resource as `Observale` or `Promise`, for this you should provide this type of object: 263 | 264 | ```typescript 265 | type NgxMfeAsyncConfig = { 266 | /** 267 | * A function to invoke to load a `MfeConfig`. The function is invoked with 268 | * resolved values of `token`s in the `deps` field. 269 | */ 270 | useLoader: (...deps: any[]) => Observable | Promise; 271 | /** 272 | * A list of `token`s to be resolved by the injector. The list of values is then 273 | * used as arguments to the `useLoader` function. 274 | */ 275 | deps?: any[]; 276 | }; 277 | ``` 278 | 279 | For example: 280 | 281 | ```typescript 282 | mfeConfig: { 283 | useLoader: (http: HttpClient): Observable => 284 | http.get('/manifest.json'), 285 | deps: [HttpClient] 286 | }, 287 | ``` 288 | 289 | 290 | - **preload** (Optional) - a list of micro-frontend names, their bundles (remoteEntry.js) will be loaded and saved in the cache when the application starts. 291 | 292 | Next options are only works in plugin-based approach with `MfeOutletDirective`: 293 | 294 | - **loaderDelay** (Optional) - Specifies the minimum loader display time in ms. This is to avoid flickering when the micro-frontend loads very quickly. 295 | 296 | *By default is 0.* 297 | 298 | - **loader** (Optional) - Displayed when loading the micro-frontend. Implements the `RemoteComponent` interface. 299 | 300 | *Example:* 301 | ```typescript 302 | // Globally uses the "SpinnerComponent" loader component declared in the "SpinnerModule" of the app "loaders". 303 | loader: { 304 | app: 'loaders', 305 | module: 'SpinnerModule', 306 | component: 'SpinnerComponent', 307 | }, 308 | ``` 309 | 310 | > For better UX, add loader micro-frontends to the `preload`. 311 | 312 | - **fallback** (Optional) - Displayed when loading or compiling a micro-frontend with an error. Implements the `RemoteComponent` interface. 313 | 314 | *Example:* 315 | ```typescript 316 | // Globally uses the "MfeFallbackComponent" fallback component declared in the "MfeFallbackModule" of the app "fallbacks". 317 | fallback: { 318 | app: 'fallbacks', 319 | module: 'MfeFallbackModule', 320 | component: 'MfeFallbackComponent', 321 | }, 322 | ``` 323 | 324 | > For better UX, add fallback micro-frontends to the `preload`. 325 | 326 | You can get all configured options by injecting `NGX_MFE_OPTIONS` by DI: 327 | 328 | ```typescript 329 | class AppComponent { 330 | constructor(@Inject(NGX_MFE_OPTIONS) public options: NgxMfeOptions) {} 331 | } 332 | ``` 333 | 334 | ## Display MFE in HTML template / plugin-based approach 335 | 336 | This approach allows us to load micro-frontends directly from HTML. 337 | 338 | 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. 339 | 340 | > More about plugin-based approach [here](https://dekh.medium.com/angular-micro-frontend-architecture-part-3-3-mfe-plugin-based-approach-f36dc9849b0). 341 | 342 | > Full code of this example can be found at https://github.com/dkhrunov/ngx-mfe-test. 343 | 344 | Example app: 345 | 346 | ![image](https://user-images.githubusercontent.com/25565058/187071276-11e1dd5c-6fe4-4d7c-94df-2bf74331d900.png) 347 | 348 | An example webpack.config.js that exposes the "MfeTestComponent" (brown border in the screenshot above): 349 | 350 | ```js 351 | // webpack.config.js 352 | return { 353 | [...] 354 | resolve: { 355 | alias: sharedMappings.getAliases(), 356 | }, 357 | plugins: [ 358 | new ModuleFederationPlugin({ 359 | name: 'test', 360 | exposes: { 361 | MfeTestModule: 'apps/test/src/app/mfe-test/mfe-test.module.ts', 362 | MfeTestComponent: 'apps/test/src/app/mfe-test/mfe-test.component.ts', 363 | }, 364 | filename: 'remoteEntry', 365 | shared: share({ ... }), 366 | }), 367 | sharedMappings.getPlugin(), 368 | ], 369 | }; 370 | ``` 371 | 372 | 1. Just display the component "MfeTestComponent" inside other MFE component "Form" from "address-form" app: 373 | 374 | One variant: 375 | ```html 376 | 381 | 382 | ``` 383 | 384 | Other variant: 385 | ```html 386 | 393 | 394 | ``` 395 | 396 | > These two examples are equal and display the MFE "MfeTestComponent". 397 | 398 | 2. You can pass/bind `@Input` and `@Output` props to MFE component: 399 | 400 | ```html 401 | 402 | 411 | ``` 412 | 413 | ```typescript 414 | // form.component.ts file 415 | @Component({ 416 | selector: 'app-form', 417 | templateUrl: './form.component.html', 418 | styleUrls: ['./form.component.scss'], 419 | changeDetection: ChangeDetectionStrategy.OnPush, 420 | }) 421 | export class FormComponent { 422 | [...] 423 | // timer emits after 1 second, then every 2 seconds 424 | public readonly text$: Observable = timer(1000, 2000); 425 | 426 | // on click log to console event 427 | public onClick(event: MouseEvent): void { 428 | console.log('clicked', event); 429 | } 430 | [...] 431 | } 432 | ``` 433 | 434 | > If you try to bind a @Output() property that is not in the component, then an error will fall into the console: 435 | > "Output **someOutput** is not output of **SomeComponent**." 436 | > 437 | > If you try to pass a non-function, then an error will fall into the console: 438 | > "Output **someOutput** must be a function." 439 | 440 | 3. To override the default loader delay, configured in `MfeModule.forRoot({ ... })`, provide custom number in ms to property `loaderDelay`: 441 | 442 | ```html 443 | 451 | ``` 452 | 453 | 4. 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`: 454 | 455 | ```html 456 | 465 | 466 | 467 |
loading...
468 |
469 | 470 | 471 |
Ooops! Something went wrong
472 |
473 | ``` 474 | 475 | ```html 476 | 477 | 483 | 484 | 485 | 492 | 493 | 494 | ``` 495 | 496 | 6. You can also provide a custom injector for a component like this: 497 | 498 | ```html 499 | 505 | ``` 506 | 507 | ## Display Angular v14 Standalone components 508 | 509 | Example app: 510 | 511 | ![image](https://user-images.githubusercontent.com/25565058/187071276-11e1dd5c-6fe4-4d7c-94df-2bf74331d900.png) 512 | 513 | An example webpack.config.js that exposes the "StandaloneComponent" (green border in the screenshot above): 514 | 515 | ```js 516 | // webpack.config.js 517 | return { 518 | [...] 519 | resolve: { 520 | alias: sharedMappings.getAliases(), 521 | }, 522 | plugins: [ 523 | new ModuleFederationPlugin({ 524 | name: 'test', 525 | exposes: { 526 | [...] 527 | StandaloneComponent: 'apps/test/src/app/standalone/standalone.component.ts', 528 | }, 529 | filename: 'remoteEntry', 530 | shared: share({ ... }), 531 | }), 532 | sharedMappings.getPlugin(), 533 | ], 534 | }; 535 | ``` 536 | 537 | ```typescript 538 | // standalone.component.ts 539 | 540 | import { Component } from '@angular/core'; 541 | import { CommonModule } from '@angular/common'; 542 | 543 | @Component({ 544 | selector: 'app-standalone', 545 | standalone: true, 546 | imports: [CommonModule], 547 | template: `

Standalone component works!

`, 548 | styles: [], 549 | }) 550 | export class StandaloneComponent {} 551 | ``` 552 | 553 | ```html 554 | 555 | [...] 556 |

Angular v14 Standalone component loaded as MFE:

557 | 561 | ``` 562 | 563 | ## Passing Data to the MFE Component via mfeOutlet directive 564 | 565 | 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: 566 | 567 | 1. As I wrote above through the properties `[mfeOutletInputs]` `[mfeOutletOutputs]` 568 | 569 | component.html: 570 | ```html 571 | 578 | 579 | ``` 580 | 581 | component.ts 582 | ```typescript 583 | @Component({ ... }) 584 | export class Component { 585 | public text$ = new BehaviorSubject('Test string'); 586 | 587 | constructor() { } 588 | 589 | public onClick(bool: MouseEvent): void { 590 | console.log('login', bool); 591 | } 592 | } 593 | ``` 594 | 595 | 2. 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. 596 | 597 | component.html: 598 | ```html 599 | 605 | 606 | ``` 607 | 608 | component.ts 609 | ```typescript 610 | @Component({ ... }) 611 | export class Component { 612 | public readonly testComponentInjector: Injector; 613 | 614 | constructor(private readonly _injector: Injector) { 615 | this.testComponentInjector = Injector.create({ 616 | parent: this._injector, 617 | providers: [ 618 | { 619 | provide: TEST_DATA, 620 | useValue: data, 621 | }, 622 | ], 623 | }); 624 | } 625 | } 626 | ``` 627 | 628 | ## Load MFE by Route 629 | 630 | To use micro-frontends in Routing, you must import and apply the helper function called `loadMfe`, like in the example below: 631 | 632 | ```typescript 633 | import { NgModule } from '@angular/core'; 634 | import { RouterModule, Routes } from '@angular/router'; 635 | import { loadMfe } from '@dkhrunov/ng-mfe'; 636 | 637 | const routes: Routes = [ 638 | { 639 | path: 'dashboard', 640 | loadChildren: () => loadMfe('dashboard-mfe', 'EntryModule'), 641 | }, 642 | ]; 643 | 644 | @NgModule({ 645 | imports: [RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking' })], 646 | exports: [RouterModule], 647 | }) 648 | export class AppRoutingModule {} 649 | ``` 650 | 651 | ## Changelog 652 | 653 | ### Changes in __v2.1.0__ 654 | Fixed: 655 | - Fix error, if the fallback is also unavailable, then simply clear the view; 656 | 657 | Refactored: 658 | - Renamed `MfeService` to `RemoteComponentLoader`; 659 | - Renamed `MfeComponentsCache` to `RemoteComponentsCache`; 660 | - Renamed `ModularRemoteComponent` type to `RemoteComponentWithModule`; 661 | - Wrapped to `ngZone.runOutside` the `loadMfe` function calls inside the `RemoteComponentLoader`; 662 | - Added new type `ComponentWithNgModuleRef`, that holds component class `Type` and `NgModuleRef`; 663 | - Changed cached value for `RemoteComponentWithModule` from `ComponentFactory` to `ComponentWithNgModuleRef`; 664 | - In `RemoteComponentLoader` (old name `MfeService`) renamed function `loadModularComponent` to `loadComponentWithModule` 665 | - Changed return type of method `loadComponentWithModule` inside class `RemoteComponentLoader` from `Promise>` to `Promise>`; 666 | 667 | ### Changes in __v2.0.0__ (_Breaking changes_) 668 | 669 | __Why has the API changed?__ - The problem is that when you use the `[mfeOutlet]` directive [issue](https://github.com/dkhrunov/ngx-mfe/issues/7), 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. 670 | 671 | #### General: 672 | - 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. 673 | 674 | __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; 675 | 676 | - 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__); 677 | 678 | - `MFE string` has been replaced by a new type `RemoteComponent`; 679 | 680 | - The `validateMfe` function has been removed (__it was used until version 2.0.0__); 681 | 682 | - The `loader` and `fallback` properties in the `NgxMfeOptions` has been changed from `MFE string` to `RemoteComponent` type: 683 | 684 | Before v2.0.0: 685 | ```typescript 686 | @NgModule({ 687 | declarations: [AppComponent], 688 | imports: [ 689 | BrowserModule, 690 | BrowserAnimationsModule, 691 | MfeModule.forRoot({ 692 | mfeConfig: { 693 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js", 694 | "loaders-mfe": "http://localhost:4202/remoteEntry.js", 695 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js" 696 | }, 697 | loader: 'loaders/spinner', 698 | fallback: 'fallbacks/mfe-fallback', 699 | }), 700 | ], 701 | bootstrap: [AppComponent], 702 | }) 703 | export class AppModule {} 704 | ``` 705 | 706 | Since v2.0.0: 707 | ```typescript 708 | @NgModule({ 709 | declarations: [AppComponent], 710 | imports: [ 711 | BrowserModule, 712 | BrowserAnimationsModule, 713 | MfeModule.forRoot({ 714 | mfeConfig: { 715 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js", 716 | "loaders-mfe": "http://localhost:4202/remoteEntry.js", 717 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js" 718 | }, 719 | loader: { 720 | app: 'loaders', 721 | module: 'SpinnerModule', 722 | component: 'SpinnerComponent', 723 | }, 724 | fallback: { 725 | app: 'fallbacks', 726 | module: 'MfeFallbackModule', 727 | component: 'MfeFallbackComponent', 728 | }, 729 | }), 730 | ], 731 | bootstrap: [AppComponent], 732 | }) 733 | export class AppModule {} 734 | ``` 735 | 736 | - Removed `moduleName` property from `LoadMfeOptions` type; 737 | - 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. 738 | ```javascript 739 | // webpack.config.js 740 | exposes: { 741 | // LoginModule name of the exposed file login.module.ts 742 | LoginModule: 'apps/auth-mfe/src/app/login/login.module.ts', 743 | }, 744 | ``` 745 | 746 | Before v2.0.0: 747 | ```typescript 748 | loadMfe('auth-mfe/login-module') 749 | ``` 750 | 751 | Since v2.0.0: 752 | ```typescript 753 | loadMfe('auth-mfe' 'LoginModule') 754 | ``` 755 | 756 | #### LoadMfe function: 757 | - Arguments changed in `LoadMfe` function: 758 | 759 | Before v2.0.0: 760 | ```typescript 761 | async function loadMfe(mfeString: string, options?: LoadMfeOptions): Promise> {} 762 | ``` 763 | 764 | Since v2.0.0: 765 | ```typescript 766 | async function loadMfe(remoteApp: string, exposedFile: string, options?: LoadMfeOptions): Promise> {} 767 | ``` 768 | - `remoteApp` - is the name of the remote app as specified in the webpack.config.js file in the ModuleFederationPlugin in the __name__ property; 769 | - `exposedFile` - is the key (or name) of the exposed file specified in the webpack.config.js file in the ModuleFederationPlugin in the __exposes__ property; 770 | 771 | #### MfeOutlet directive: 772 | - Since the `Mfe string` has been removed from the library, the API of `[mfeOutlet]` directive has changed: 773 | 1. `mfeOutletLoader` and `mfeOutletFallback` now accept only `TemplateRef`, more details below. 774 | 2. 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 specify `mfeOutletModule` with the name of the open module file in which the component is declared for the ModuleFederationPlugin in webpack.config. 775 | 3. 776 | - `@Input('mfeOutletOptions')' options` changed type from `MfeComponentFactoryResolverOptions` to `LoadMfeOptions`; 777 | - `@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: 778 | 779 | ```html 780 | 781 | 787 | 788 | 789 | 790 | 791 | 792 | 799 | 800 | 801 | 802 | 803 | 809 | 810 | 811 | 812 | 813 |
loading...
814 |
815 | ``` 816 | 817 | #### MfeComponentFactoryResolver: 818 | - The `MfeComponentFactoryResolver` has been replaced with `MfeService` and the API has been changed; 819 | - The `MfeComponentFactoryResolverOptions` type has been removed; 820 | 821 | #### MfeComponentCache 822 | - Now the `MfeComponentCache` not only saves `ComponentFactory` but also `Type`; 823 | - In version 2.1.0 `ComponentFactory` was replaced to `ComponentWithNgModuleRef`; 824 | 825 | #### DynamicComponentBinding 826 | - The `bindInputs()` and `bindOutputs()` methods now require `ComponentRef` in the first argument, `MfeOutletInputs`/`MfeOutletOutputs` are method dependent in the second, and the third argument has been removed; 827 | - The `DynamicComponentInputs` and `DynamicComponentOutputs` types have been removed because these types are replaced in `bindInputs()` and `bindOutputs()` respectively by the `ComponentRef` type; 828 | - The `validateInputs()` method has been removed; 829 | - The `validateOutputs()` method is now private; 830 | 831 | --------------- 832 | 833 | ### Changes in __v1.1.0__: 834 | 835 | - Deleted the `loadMfeComponent` helper function; 836 | - Deleted the `parseMfeString` helper function; 837 | - Renamed the `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; 838 | - Renamed the `MfeService` to `MfeComponentFactoryResolver`; 839 | - `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. 840 | - Added new Input prop to the `MfeOutletDirective` - `options: MfeComponentFactoryResolver`, this parameter provided to `resolveComponentFactory` method of the `MfeComponentFactoryResolver` when resolving the component factory of MFE. 841 | - 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. 842 | 843 | 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. 844 | 845 | 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' }`; 846 | 847 | --------------- 848 | 849 | ### Changes in __v1.0.8__: 850 | 851 | - `IMfeModuleRootOptions` interface renamed to `NgxMfeOptions`; 852 | - Property `delay` in the `NgxMfeOptions` renamed to `loaderDelay`; 853 | - `OPTIONS` injection token renamed to `NGX_MFE_OPTIONS`; 854 | 855 | --------------- 856 | -------------------------------------------------------------------------------- /projects/ngx-mfe/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, '../../coverage/ngx-mfe'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /projects/ngx-mfe/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-mfe", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/ngx-mfe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-mfe", 3 | "version": "19.0.0", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "GitHub", 7 | "url": "https://github.com/dkhrunov/ngx-mfe" 8 | }, 9 | "keywords": [ 10 | "microfrontend", 11 | "mfe", 12 | "angular-microfrontend", 13 | "angular-mfe", 14 | "microcomponent", 15 | "angular" 16 | ], 17 | "author": { 18 | "name": "Denis Khrunov", 19 | "email": "therealpanda98@gmail.com" 20 | }, 21 | "peerDependencies": { 22 | "@angular/common": "^19.0.0", 23 | "@angular/core": "^19.0.0", 24 | "@angular-architects/module-federation": "^19.0.0", 25 | "rxjs": "^7.0.0" 26 | }, 27 | "dependencies": { 28 | "tslib": "^2.3.0" 29 | } 30 | } -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './track-changes.decorator'; 2 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/decorators/track-changes.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SimpleChanges } from '@angular/core'; 2 | 3 | /** 4 | * Strategy for changing the component's @Input() variable. 5 | */ 6 | export enum EChangesStrategy { 7 | /** 8 | * Called on every change. 9 | */ 10 | Each, 11 | /** 12 | * Called only on the first change. 13 | */ 14 | First, 15 | /** 16 | * Called on every change except the first. 17 | */ 18 | NonFirst, 19 | } 20 | 21 | export interface TrackChangesOptions { 22 | /** 23 | * Change strategy. 24 | * @default EChangesStrategy.Each 25 | */ 26 | strategy?: EChangesStrategy; 27 | /** 28 | * Compare the previous value with the current one 29 | * and execute the method only if the values differ. 30 | * 31 | * Values must be immutable, as values are compared by reference. 32 | * @default false 33 | */ 34 | compare?: boolean; 35 | } 36 | 37 | const defaultOptions: TrackChangesOptions = { 38 | strategy: EChangesStrategy.Each, 39 | compare: false, 40 | }; 41 | 42 | /** 43 | * Decorator of lifecycle hook ngOnChanges, that call specified method when changes prop (@Input) value. 44 | * ------- 45 | * 46 | * Method decorator. 47 | * 48 | * @param prop Variable name of Input, that will be call method when changes. 49 | * @param methodName The name of the method that will be called when the variable changes. 50 | * @param options Options. 51 | */ 52 | export function TrackChanges( 53 | prop: string, 54 | methodName: string, 55 | options?: TrackChangesOptions 56 | ): MethodDecorator { 57 | return function ( 58 | target: any, 59 | _propertyKey: string | symbol, 60 | descriptor: PropertyDescriptor 61 | ): TypedPropertyDescriptor { 62 | const _options = { ...defaultOptions, ...options }; 63 | const originalMethod = descriptor.value as (changes: SimpleChanges) => void; 64 | 65 | descriptor.value = function (changes: SimpleChanges): void { 66 | if (changes && changes[prop] && changes[prop].currentValue !== undefined) { 67 | const isFirstChange = changes[prop].firstChange; 68 | const shouldCompareValues = _options.compare; 69 | const isValuesDifference = changes[prop].previousValue !== changes[prop].currentValue; 70 | 71 | if ( 72 | _options.strategy === EChangesStrategy.Each || 73 | (_options.strategy === EChangesStrategy.First && isFirstChange) || 74 | (_options.strategy === EChangesStrategy.NonFirst && !isFirstChange) 75 | ) { 76 | if (!shouldCompareValues) { 77 | target[methodName].call(this, changes[prop].currentValue as T); 78 | } else if (isValuesDifference) { 79 | target[methodName].call(this, changes[prop].currentValue as T); 80 | } 81 | } 82 | } 83 | 84 | originalMethod.call(this, changes); 85 | }; 86 | 87 | return descriptor; 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/directives/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mfe-outlet.directive'; 2 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/directives/mfe-outlet.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { MfeOutletDirective } from '.'; 2 | 3 | describe('MfeOutletDirective', () => { 4 | it('should create an instance', () => { 5 | const directive = new MfeOutletDirective(); 6 | expect(directive).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/directives/mfe-outlet.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | ChangeDetectorRef, 4 | ComponentRef, 5 | Directive, 6 | EmbeddedViewRef, 7 | Inject, 8 | Injector, 9 | Input, 10 | OnChanges, 11 | OnDestroy, 12 | TemplateRef, 13 | ViewContainerRef, 14 | } from '@angular/core'; 15 | 16 | import { EChangesStrategy, TrackChanges } from '../decorators'; 17 | import { delay, LoadMfeOptions } from '../helpers'; 18 | import { NGX_MFE_OPTIONS } from '../injection-tokens'; 19 | import { 20 | isRemoteComponentWithModule, 21 | isStandaloneRemoteComponent, 22 | NgxMfeOptions, 23 | RemoteComponent, 24 | RemoteComponentWithModule, 25 | StandaloneRemoteComponent, 26 | } from '../interfaces'; 27 | import { 28 | DynamicComponentBinding, 29 | RemoteComponentLoader, 30 | RemoteComponentsCache, 31 | } from '../services'; 32 | import { MfeOutletInputs, MfeOutletOutputs } from '../types'; 33 | 34 | /** 35 | * Micro-frontend directive for plugin-based approach. 36 | * ------------- 37 | * 38 | * This directive give you to load micro-frontend inside in HTML template. 39 | * 40 | * @example Loads remote component and show as embed view or as a plugin. 41 | * ```html 42 | * 43 | * 48 | * 49 | * 50 | * 51 | * 56 | * 57 | * ``` 58 | * 59 | * @example Loads standalone remote component. Standalone component - it is a component that does not depend on anything and does not need dependencies from other modules. 60 | * ```html 61 | * 62 | * 66 | * 67 | * ``` 68 | * 69 | * @example You can sets Inputs and sets handlers for Output events of the Remote component. 70 | * ```html 71 | * 78 | * 79 | *``` 80 | * 81 | * @example Loads remote component and sets custom loader, same approach for fallback view. 82 | * ```html 83 | * 90 | * 91 | * 92 | * 93 | * 94 | * 95 | * 102 | * 103 | * 104 | * 105 | * 106 | * 107 | *
loading...
108 | *
109 | * ``` 110 | */ 111 | @Directive({ 112 | // eslint-disable-next-line @angular-eslint/directive-selector 113 | selector: '[mfeOutlet]', 114 | exportAs: 'mfeOutlet', 115 | providers: [DynamicComponentBinding], 116 | standalone: false, 117 | }) 118 | export class MfeOutletDirective implements OnChanges, AfterViewInit, OnDestroy { 119 | /** 120 | * Sets the Remote app name. 121 | */ 122 | @Input('mfeOutlet') 123 | public mfeApp?: string; 124 | 125 | /** 126 | * Sets the Remote compoennt. 127 | */ 128 | // eslint-disable-next-line @angular-eslint/no-input-rename 129 | @Input('mfeOutletComponent') 130 | public mfeComponent?: string; 131 | 132 | /** 133 | * Sets the Remote module where declared Remote component (```mfeOutletComponent```) 134 | */ 135 | // eslint-disable-next-line @angular-eslint/no-input-rename 136 | @Input('mfeOutletModule') 137 | public mfeModule?: string; 138 | 139 | /** 140 | * A map of Inputs for a micro-frontend component. 141 | */ 142 | @Input('mfeOutletInputs') 143 | public inputs?: MfeOutletInputs; 144 | 145 | /** 146 | * A map of Outputs for a micro-frontend component. 147 | */ 148 | @Input('mfeOutletOutputs') 149 | public outputs?: MfeOutletOutputs; 150 | 151 | /** 152 | * Custom injector for micro-frontend component. 153 | * @default current injector 154 | */ 155 | @Input('mfeOutletInjector') 156 | public injector?: Injector = this._injector; 157 | 158 | /** 159 | * MFE RemoteComponent or TemplateRef. 160 | * Displayed when loading the micro-frontend. 161 | * 162 | * **Overrides the loader specified in the global library settings.** 163 | * @default options.loader 164 | */ 165 | @Input('mfeOutletLoader') 166 | public set loader(value: TemplateRef | undefined) { 167 | this._loader = value; 168 | } 169 | 170 | /** 171 | * The delay between displaying the contents of the bootloader and the micro-frontend . 172 | * 173 | * This is to avoid flickering when the micro-frontend loads very quickly. 174 | * 175 | * @default options.delay, if not set, then 0 176 | */ 177 | @Input('mfeOutletLoaderDelay') 178 | public loaderDelay = this._options.loaderDelay ?? 0; 179 | 180 | /** 181 | * MFE RemoteComponent or TemplateRef. 182 | * Displayed when loaded or compiled a micro-frontend with an error. 183 | * 184 | * **Overrides fallback the specified in the global library settings.** 185 | * @default options.fallback 186 | */ 187 | @Input('mfeOutletFallback') 188 | public set fallback(value: TemplateRef | undefined) { 189 | this._fallback = value; 190 | } 191 | 192 | /** 193 | * Custom options for loading Mfe. 194 | */ 195 | @Input('mfeOutletOptions') 196 | public options?: LoadMfeOptions; 197 | 198 | private _loader?: RemoteComponent | TemplateRef = 199 | this._options.loader; 200 | private _fallback?: RemoteComponent | TemplateRef = 201 | this._options.fallback; 202 | 203 | private _mfeComponentRef?: ComponentRef; 204 | private _loaderComponentRef?: 205 | | ComponentRef 206 | | EmbeddedViewRef; 207 | private _fallbackComponentRef?: 208 | | ComponentRef 209 | | EmbeddedViewRef; 210 | 211 | /** 212 | * Remote component object. 213 | */ 214 | private get _remoteComponent(): RemoteComponent { 215 | if (this.mfeModule) { 216 | return { 217 | app: this.mfeApp, 218 | component: this.mfeComponent, 219 | module: this.mfeModule, 220 | } as RemoteComponentWithModule; 221 | } 222 | 223 | return { 224 | app: this.mfeApp, 225 | component: this.mfeComponent, 226 | } as StandaloneRemoteComponent; 227 | } 228 | 229 | constructor( 230 | private readonly _vcr: ViewContainerRef, 231 | // INSTEAD OF USE THIS REF TO INJECTOR USE `this.injector` 232 | private readonly _injector: Injector, 233 | private readonly _remoteComponentLoader: RemoteComponentLoader, 234 | private readonly _remoteComponentCache: RemoteComponentsCache, 235 | private readonly _dynamicBinding: DynamicComponentBinding, 236 | @Inject(NGX_MFE_OPTIONS) private readonly _options: NgxMfeOptions 237 | ) {} 238 | 239 | @TrackChanges('mfeRemote', 'renderMfe', { 240 | compare: true, 241 | strategy: EChangesStrategy.NonFirst, 242 | }) 243 | @TrackChanges('mfeComponent', 'renderMfe', { 244 | compare: true, 245 | strategy: EChangesStrategy.NonFirst, 246 | }) 247 | @TrackChanges('mfeModule', 'renderMfe', { 248 | compare: true, 249 | strategy: EChangesStrategy.NonFirst, 250 | }) 251 | @TrackChanges('inputs', 'transferInputs', { 252 | strategy: EChangesStrategy.NonFirst, 253 | compare: true, 254 | }) 255 | public ngOnChanges(): void { 256 | return; 257 | } 258 | 259 | public ngAfterViewInit(): void { 260 | this.renderMfe(); 261 | } 262 | 263 | public ngOnDestroy(): void { 264 | this._clearView(); 265 | } 266 | 267 | /** 268 | * Transfer MfeOutletInputs to micro-frontend component. 269 | * 270 | * Used when changing input "inputs" of this directive. 271 | * @internal 272 | */ 273 | protected transferInputs(): void { 274 | if (!this._mfeComponentRef) return; 275 | 276 | this._dynamicBinding.bindInputs(this._mfeComponentRef, this.inputs ?? {}); 277 | 278 | // Workaround for bug related to Angular and dynamic components. 279 | // Link - https://github.com/angular/angular/issues/36667#issuecomment-926526405 280 | this._mfeComponentRef?.injector.get(ChangeDetectorRef).detectChanges(); 281 | } 282 | 283 | /** 284 | * Render micro-frontend component. 285 | * 286 | * While loading bundle of micro-frontend showing loader. 287 | * If error occur then showing fallback. 288 | * 289 | * Used when changing input "mfe" of this directive. 290 | * @internal 291 | */ 292 | protected async renderMfe(): Promise { 293 | try { 294 | // If some component already rendered then need to unbind outputs 295 | if (this._mfeComponentRef) this._dynamicBinding.unbindOutputs(); 296 | 297 | if (this._remoteComponentCache.isRegistered(this._remoteComponent)) { 298 | this._showMfe(); 299 | } else { 300 | await this._showLoader(); 301 | await delay(this.loaderDelay); 302 | this._showMfe(); 303 | } 304 | } catch (error) { 305 | console.error(error); 306 | this._showFallback(); 307 | } 308 | } 309 | 310 | /** 311 | * Shows micro-frontend component. 312 | * @internal 313 | */ 314 | private async _showMfe(): Promise { 315 | try { 316 | if (this.mfeApp) { 317 | this._mfeComponentRef = await this._createView( 318 | this._remoteComponent, 319 | this.options 320 | ); 321 | this._bindMfeData(); 322 | } 323 | } catch (error) { 324 | console.group(`Error in Microfronted "${this._remoteComponent.app}"`); 325 | if (isRemoteComponentWithModule(this._remoteComponent)) { 326 | console.log('module :>> ', this._remoteComponent.module); 327 | } 328 | console.log('component :>> ', this._remoteComponent.component); 329 | console.log( 330 | 'is standalone :>> ', 331 | isStandaloneRemoteComponent(this._remoteComponent) 332 | ); 333 | console.error(error); 334 | console.groupEnd(); 335 | this._showFallback(); 336 | } 337 | } 338 | 339 | /** 340 | * Shows loader content. 341 | * @internal 342 | */ 343 | private async _showLoader(): Promise { 344 | try { 345 | if (this._loader) { 346 | this._loaderComponentRef = await this._createView(this._loader); 347 | } 348 | } catch (error) { 349 | console.error(error); 350 | this._showFallback(); 351 | } 352 | } 353 | 354 | /** 355 | * Shows fallback content. 356 | * @internal 357 | */ 358 | private async _showFallback(): Promise { 359 | if (this._fallback) { 360 | try { 361 | this._fallbackComponentRef = await this._createView(this._fallback); 362 | } catch (error) { 363 | console.error(error); 364 | this._clearView(); 365 | } 366 | } else { 367 | this._clearView(); 368 | } 369 | } 370 | 371 | /** 372 | * Shows MFE Component or TemlateRef. 373 | * @param content MFE (Remote component) or TemlateRef. 374 | * @param options Custom options for MfeComponentFactoryResolver. 375 | * @internal 376 | */ 377 | private async _createView( 378 | templateRef: TemplateRef 379 | ): Promise>; 380 | 381 | private async _createView( 382 | remoteComponent: RemoteComponent, 383 | options?: LoadMfeOptions 384 | ): Promise>; 385 | 386 | private async _createView( 387 | remoteComponentOrTemplateRef: RemoteComponent | TemplateRef, 388 | options?: LoadMfeOptions 389 | ): Promise | ComponentRef>; 390 | 391 | private async _createView( 392 | content: RemoteComponent | TemplateRef, 393 | options?: LoadMfeOptions 394 | ): Promise | ComponentRef> { 395 | // TemplateRef 396 | if (content instanceof TemplateRef) { 397 | this._clearView(); 398 | return this._vcr.createEmbeddedView(content); 399 | } 400 | // MFE (Remote Component) 401 | else { 402 | const componentRef: ComponentRef = isRemoteComponentWithModule(content) 403 | ? // for modular Angular (any version) components 404 | await this._createRemoteComponent(content, options) 405 | : // for standalone Angular v13+ components 406 | await this._createStandaloneRemoteComponent(content, options); 407 | 408 | componentRef.changeDetectorRef.detectChanges(); 409 | return componentRef; 410 | } 411 | } 412 | 413 | // TODO pattern strategy 1 414 | /** 415 | * Create view for modular remote component. 416 | * @param remoteComponent MFE remote component 417 | * @param options (Optional) object of options. 418 | */ 419 | private async _createRemoteComponent( 420 | remoteComponent: RemoteComponentWithModule, 421 | options?: LoadMfeOptions 422 | ): Promise> { 423 | const { component, ngModuleRef } = 424 | await this._remoteComponentLoader.loadComponentWithModule< 425 | TComponent, 426 | unknown 427 | >(remoteComponent, this.injector, options); 428 | 429 | this._clearView(); 430 | 431 | const componentRef = this._vcr.createComponent(component, { 432 | ngModuleRef, 433 | injector: this.injector, 434 | }); 435 | 436 | return componentRef; 437 | } 438 | 439 | // TODO pattern strategy 2 440 | /** 441 | * Create view for standalone remote component. 442 | * @param remoteComponent MFE remote component 443 | * @param options (Optional) object of options. 444 | */ 445 | private async _createStandaloneRemoteComponent( 446 | remoteComponent: StandaloneRemoteComponent, 447 | options?: LoadMfeOptions 448 | ): Promise> { 449 | const component = 450 | await this._remoteComponentLoader.loadStandaloneComponent( 451 | remoteComponent, 452 | options 453 | ); 454 | 455 | this._clearView(); 456 | 457 | const componentRef = this._vcr.createComponent(component, { 458 | injector: this.injector, 459 | }); 460 | 461 | return componentRef; 462 | } 463 | 464 | // TODO работает и без этого метода, но не работает output 465 | /** 466 | * Binding the initial data of the micro-frontend. 467 | * @internal 468 | */ 469 | private _bindMfeData(): void { 470 | if (!this._mfeComponentRef) { 471 | throw new Error( 472 | `_bindMfeData method must be called after micro-frontend component "${this.mfeApp}" has been initialized.` 473 | ); 474 | } 475 | 476 | this._dynamicBinding.bindInputs(this._mfeComponentRef, this.inputs ?? {}); 477 | this._dynamicBinding.bindOutputs(this._mfeComponentRef, this.outputs ?? {}); 478 | 479 | // TODO похоже что не актуально больше работает все и без этой штуки все 480 | 481 | // Workaround for bug related to Angular and dynamic components. 482 | // Link - https://github.com/angular/angular/issues/36667#issuecomment-926526405 483 | this._mfeComponentRef?.injector.get(ChangeDetectorRef).detectChanges(); 484 | } 485 | 486 | /** 487 | * Destroy all displayed components and clear view container ref. 488 | * @internal 489 | */ 490 | private _clearView() { 491 | this._loaderComponentRef?.destroy(); 492 | this._fallbackComponentRef?.destroy(); 493 | this._mfeComponentRef?.destroy(); 494 | this._vcr.clear(); 495 | } 496 | } 497 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/helpers/delay.ts: -------------------------------------------------------------------------------- 1 | export const delay = (time: number) => new Promise((resolve) => setTimeout(resolve, time)); -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './delay'; 2 | export * from './load-mfe'; 3 | 4 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/helpers/load-mfe.ts: -------------------------------------------------------------------------------- 1 | import { loadRemoteModule, LoadRemoteModuleOptions } from '@angular-architects/module-federation'; 2 | import { Type } from '@angular/core'; 3 | 4 | import { firstValueFrom } from 'rxjs'; 5 | import { MfeRegistry } from '../registry'; 6 | 7 | /** 8 | * Options for ```loadMfe``` function. 9 | */ 10 | export type LoadMfeOptions = { 11 | /** 12 | * Set custom exposed module name, by default module name = exposedItem + 'Module'. 13 | */ 14 | moduleName?: string, 15 | /** 16 | * Type of loaded module as a ```script``` or as a ```module```. 17 | */ 18 | type?: LoadRemoteModuleOptions['type']; 19 | }; 20 | 21 | const loadMfeDefaultOptions: LoadMfeOptions = { type: 'module' }; 22 | 23 | /** 24 | * Loads remote bundle. 25 | * 26 | * @param remoteApp The name of the micro-frontend app decalred in ModuleFederationPlugin. 27 | * @param exposedModule The key of the exposed module decalred in ModuleFederationPlugin. 28 | * @param options (Optional) object of options. 29 | */ 30 | export async function loadMfe( 31 | remoteApp: string, 32 | exposedModule: string, 33 | options: LoadMfeOptions = loadMfeDefaultOptions 34 | ): Promise> { 35 | const _options: LoadMfeOptions = { ...loadMfeDefaultOptions, ...options }; 36 | const remoteEntry = await firstValueFrom(MfeRegistry.instance.getMfeRemoteEntry(remoteApp)); 37 | const loadRemoteModuleOptions: LoadRemoteModuleOptions = 38 | _options.type === 'module' 39 | ? { type: _options.type, remoteEntry, exposedModule } 40 | : { type: _options.type, remoteEntry, exposedModule, remoteName: remoteApp }; 41 | const bundle = await loadRemoteModule(loadRemoteModuleOptions); 42 | const moduleName = _options.moduleName ?? exposedModule; 43 | const module = bundle[moduleName] 44 | 45 | if (!module) { 46 | throw new Error(`Module with name "${moduleName}" does not exist in the exposed file. Key of exposed file must match with class name in this file (Key of exposed file it is key of 'exposes' object in webpack config inside ModuleFederationPlugin).`); 47 | } 48 | 49 | return module; 50 | } 51 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/injection-tokens/index.ts: -------------------------------------------------------------------------------- 1 | export * from './options.token'; 2 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/injection-tokens/options.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { NgxMfeOptions } from '../interfaces'; 3 | 4 | /** 5 | * InjectionToken of options. 6 | */ 7 | export const NGX_MFE_OPTIONS = new InjectionToken('ngx-mfe/options'); 8 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mfe-config.interface'; 2 | export * from './ngx-mfe-options.interface'; 3 | export * from './remote-component.interface'; 4 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/interfaces/mfe-config.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MFE app options. 3 | */ 4 | export interface MfeConfig { 5 | [mfeName: string]: string; 6 | } 7 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/interfaces/ngx-mfe-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { MfeConfig } from './mfe-config.interface'; 3 | import { RemoteComponent } from './remote-component.interface'; 4 | 5 | /** 6 | * Sync list of available micro-frontends. 7 | */ 8 | export type NgxMfeSyncConfig = MfeConfig; 9 | 10 | /** 11 | * Async list of available micro-frontends. 12 | */ 13 | export type NgxMfeAsyncConfig = { 14 | /** 15 | * A function to invoke to load a `MfeConfig`. The function is invoked with 16 | * resolved values of `token`s in the `deps` field. 17 | */ 18 | useLoader: (...deps: any[]) => Observable | Promise; 19 | /** 20 | * A list of `token`s to be resolved by the injector. The list of values is then 21 | * used as arguments to the `useLoader` function. 22 | */ 23 | deps?: any[]; 24 | }; 25 | 26 | /** 27 | * Type of sync / async list of available micro-frontends. 28 | */ 29 | export type NgxMfeConfigOption = NgxMfeSyncConfig | NgxMfeAsyncConfig; 30 | 31 | /** 32 | * Type guard check that NgxMfeConfig is async list of available micro-frontends. 33 | */ 34 | export const isNgxMfeConfigAsync = (config: NgxMfeConfigOption): config is NgxMfeAsyncConfig => { 35 | return Object.prototype.hasOwnProperty.call(config, 'useLoader'); 36 | } 37 | 38 | /** 39 | * Global options. 40 | */ 41 | export interface NgxMfeOptions { 42 | /** 43 | * List of names of remote appls, declared apps will be downloaded immediately and stored in the cache. 44 | */ 45 | preload?: string[]; 46 | /** 47 | * Loader remote component. 48 | * 49 | * Shows when load bundle of the micro-frontend. 50 | * 51 | * For better UX, add this micro-frontend to {@link preload} array. 52 | */ 53 | loader?: RemoteComponent; 54 | /** 55 | * The delay between displaying the contents of the bootloader and the micro-frontend. 56 | * 57 | * This is to avoid flickering when the micro-frontend loads very quickly. 58 | */ 59 | loaderDelay?: number; 60 | /** 61 | * Fallback remote component. 62 | * 63 | * Showing when an error occurs while loading bundle 64 | * or when trying to display the contents of the micro-frontend. 65 | * 66 | * For better UX, add this micro-frontend to {@link preload} array. 67 | */ 68 | fallback?: RemoteComponent; 69 | } 70 | 71 | /** 72 | * Options forRoot configuration of `NgxMfeModule` 73 | */ 74 | export type NgxMfeForRootOptions = NgxMfeOptions & { 75 | /** 76 | * List of available micro-frontends. 77 | */ 78 | mfeConfig: NgxMfeConfigOption; 79 | } 80 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/interfaces/remote-component.interface.ts: -------------------------------------------------------------------------------- 1 | export interface StandaloneRemoteComponent { 2 | /** 3 | * Remote app name, specified in ModuleFederationPlugin in name config. 4 | */ 5 | app: string; 6 | /** 7 | * The key of the exposed component. 8 | */ 9 | component: string; 10 | } 11 | 12 | export interface RemoteComponentWithModule { 13 | /** 14 | * Remote app name, specified in ModuleFederationPlugin in name config. 15 | */ 16 | app: string; 17 | /** 18 | * The key of the exposed component. 19 | */ 20 | component: string; 21 | /** 22 | * The key of the exposed module in which the component is declared. 23 | */ 24 | module: string; 25 | } 26 | 27 | export type RemoteComponent = StandaloneRemoteComponent | RemoteComponentWithModule; 28 | 29 | /** 30 | * Type Guard for RemoteComponent, checks if RemoteComponent is Standalone 31 | * @param remoteComponent Mfe Remote Component 32 | * @returns 33 | */ 34 | export function isStandaloneRemoteComponent( 35 | remoteComponent: RemoteComponent 36 | ): remoteComponent is StandaloneRemoteComponent { 37 | return !Object.prototype.hasOwnProperty.call(remoteComponent, 'module'); 38 | } 39 | 40 | /** 41 | * Type Guard for RemoteComponent, checks if RemoteComponent is Modular 42 | * @param remoteComponent Mfe Remote Component 43 | * @returns 44 | */ 45 | export function isRemoteComponentWithModule( 46 | remoteComponent: RemoteComponent 47 | ): remoteComponent is RemoteComponentWithModule { 48 | return Object.prototype.hasOwnProperty.call(remoteComponent, 'module'); 49 | } 50 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/mfe.module.ts: -------------------------------------------------------------------------------- 1 | import { loadRemoteEntry } from '@angular-architects/module-federation'; 2 | import { APP_INITIALIZER, ModuleWithProviders, NgModule, Provider } from '@angular/core'; 3 | import { firstValueFrom, from, Observable, tap } from 'rxjs'; 4 | import { MfeOutletDirective } from './directives'; 5 | import { NGX_MFE_OPTIONS } from './injection-tokens'; 6 | import { isNgxMfeConfigAsync, MfeConfig, NgxMfeForRootOptions } from './interfaces'; 7 | import { MfeRegistry } from './registry'; 8 | 9 | /** 10 | * Core lib of micro-frontend architecture. 11 | * --------------- 12 | * 13 | * For core module provide MfeModule.forRoot(options).
14 | * 15 | * For feature modules provide MfeModule. 16 | */ 17 | @NgModule({ 18 | declarations: [MfeOutletDirective], 19 | exports: [MfeOutletDirective], 20 | }) 21 | export class MfeModule { 22 | /** 23 | * Sets global configuration of Mfe lib. 24 | * @param options Object of options. 25 | */ 26 | public static forRoot(options: NgxMfeForRootOptions): ModuleWithProviders { 27 | const { preload, mfeConfig } = options; 28 | const providers: Provider[] = [ 29 | { 30 | provide: NGX_MFE_OPTIONS, 31 | useValue: options, 32 | }, 33 | ]; 34 | 35 | if (isNgxMfeConfigAsync(mfeConfig)) { 36 | providers.push({ 37 | provide: APP_INITIALIZER, 38 | useFactory: (): (() => Observable) => { 39 | return () => { 40 | return from(mfeConfig.useLoader(...(mfeConfig.deps ?? []))).pipe( 41 | tap((config) => initializeMfeRegistry(config, preload)) 42 | ); 43 | }; 44 | }, 45 | multi: true, 46 | }); 47 | } else { 48 | initializeMfeRegistry(mfeConfig, preload); 49 | } 50 | 51 | return { ngModule: MfeModule, providers }; 52 | } 53 | } 54 | 55 | function initializeMfeRegistry(config: MfeConfig, preload?: string[]): MfeRegistry { 56 | const mfeRegistry = MfeRegistry.instance; 57 | mfeRegistry.setMfeConfig(config); 58 | const loadMfeBundle = loadMfeBundleWithMfeRegistry(mfeRegistry); 59 | 60 | if (preload) { 61 | preload.map((mfe) => loadMfeBundle(mfe)); 62 | } 63 | 64 | return mfeRegistry; 65 | } 66 | 67 | /** 68 | * Loads micro-frontend app bundle (HOF - High Order Function). 69 | * ------ 70 | * 71 | * Returns function that can load micro-frontend app by provided name. 72 | * @param mfeRegistry Registry of micro-frontends apps. 73 | */ 74 | function loadMfeBundleWithMfeRegistry(mfeRegistry: MfeRegistry): (mfe: string) => Promise { 75 | return async (mfeString: string): Promise => { 76 | const remoteEntry = await firstValueFrom(mfeRegistry.getMfeRemoteEntry(mfeString)); 77 | 78 | return loadRemoteEntry({ type: 'module', remoteEntry }); 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/registry/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mfe-registry'; 2 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/registry/mfe-registry.ts: -------------------------------------------------------------------------------- 1 | import { map, Observable, ReplaySubject, take } from 'rxjs'; 2 | import { MfeConfig } from '../interfaces'; 3 | 4 | /** 5 | * Registry of micro-frontends apps. 6 | */ 7 | export class MfeRegistry { 8 | private static _instance: MfeRegistry; 9 | 10 | private readonly _mfeConfig$ = new ReplaySubject(1); 11 | 12 | /** 13 | * Get instance of the MfeRegistry 14 | */ 15 | public static get instance(): MfeRegistry { 16 | if (!MfeRegistry._instance) { 17 | MfeRegistry._instance = new MfeRegistry(); 18 | } 19 | 20 | return MfeRegistry._instance; 21 | } 22 | 23 | private constructor() {} 24 | 25 | /** 26 | * Set config. 27 | * @param config Micro-frontends config 28 | */ 29 | public setMfeConfig(config: MfeConfig): void { 30 | this._mfeConfig$.next(config); 31 | } 32 | 33 | /** 34 | * Get the remote entry URL the micro-frontend app 35 | * @param mfeApp Micro-frontend app name 36 | */ 37 | public getMfeRemoteEntry(mfeApp: string): Observable { 38 | return this._mfeConfig$.pipe( 39 | take(1), 40 | map((config) => { 41 | const remoteEntry = config[mfeApp]; 42 | 43 | if (!remoteEntry) { 44 | throw new Error( 45 | `'${mfeApp}' micro-frontend is not registered in the MfeRegistery using MfeModule.forRoot({ mfeConfig })` 46 | ); 47 | } 48 | 49 | return remoteEntry; 50 | }) 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/services/dynamic-component-binding.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { DynamicComponentBinding } from './dynamic-component-binding'; 4 | 5 | describe(DynamicComponentBinding, () => { 6 | let service: DynamicComponentBinding; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(DynamicComponentBinding); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/services/dynamic-component-binding.ts: -------------------------------------------------------------------------------- 1 | import { ComponentRef, EventEmitter, Injectable, OnDestroy } from '@angular/core'; 2 | import { Subject, takeUntil } from 'rxjs'; 3 | 4 | import { MfeOutletInputs, MfeOutletOutputs } from '../types'; 5 | 6 | /** 7 | * The service that binds the dynamic component. 8 | */ 9 | @Injectable() 10 | export class DynamicComponentBinding implements OnDestroy { 11 | private readonly _destroy$ = new Subject(); 12 | 13 | public ngOnDestroy(): void { 14 | this._destroy$.next(); 15 | this._destroy$.complete(); 16 | } 17 | 18 | /** 19 | * Bind provided MfeOutletInputs to dynamic component. 20 | * @param componentRef Reference of component. 21 | * @param inputs Provided MfeOutletInputs. 22 | */ 23 | public bindInputs(componentRef: ComponentRef, inputs: MfeOutletInputs): void { 24 | for (const key in inputs) { 25 | if (Object.prototype.hasOwnProperty.call(inputs, key)) { 26 | (componentRef.instance as any)[key] = inputs[key]; 27 | } 28 | } 29 | } 30 | 31 | /** 32 | * Bind provided MfeOutletOutputs to dynamic component. 33 | * @param componentRef Reference of component. 34 | * @param outputs Provided MfeOutletOutputs. 35 | */ 36 | public bindOutputs( 37 | componentRef: ComponentRef, 38 | outputs: MfeOutletOutputs 39 | ): void { 40 | this._validateOutputs(componentRef, outputs); 41 | 42 | for (const key in outputs) { 43 | if (Object.prototype.hasOwnProperty.call(outputs, key)) { 44 | ((componentRef.instance as any)[key] as EventEmitter) 45 | .pipe(takeUntil(this._destroy$)) 46 | .subscribe((event) => { 47 | const handler = outputs[key]; 48 | if (handler) { 49 | // in case the output has not been provided at all 50 | handler(event); 51 | } 52 | }); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Unbind all outputs. 59 | */ 60 | public unbindOutputs(): void { 61 | this._destroy$.next(); 62 | } 63 | 64 | /** 65 | * Validate MfeOutletOutputs of dynamic component. 66 | * @param componentRef Reference of component. 67 | * @param outputs Provided MfeOutletOutputs. 68 | */ 69 | private _validateOutputs( 70 | componentRef: ComponentRef, 71 | outputs: MfeOutletOutputs 72 | ): void { 73 | Object.keys(outputs).forEach((key) => { 74 | const isComponentHaveOutput = Object.prototype.hasOwnProperty.call( 75 | componentRef.instance, 76 | key 77 | ); 78 | 79 | if (!isComponentHaveOutput) { 80 | throw new Error( 81 | `Dynamically bound Output "${key}" is not declared in target component ${componentRef.componentType.constructor.name}.` 82 | ); 83 | } 84 | 85 | if (!((componentRef.instance as any)[key] instanceof EventEmitter)) { 86 | throw new Error( 87 | `Dynamically bound Output "${key}" must be an instance of EventEmitter.` 88 | ); 89 | } 90 | 91 | if (!(outputs[key] instanceof Function)) { 92 | throw new Error(`Dynamically bound Output "${key}" must be a function.`); 93 | } 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dynamic-component-binding'; 2 | export * from './remote-component-loader'; 3 | export * from './remote-components-cache'; 4 | 5 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/services/remote-component-loader.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RemoteComponentLoader } from './remote-component-loader'; 3 | 4 | describe('RemoteComponentLoader', () => { 5 | let service: RemoteComponentLoader; 6 | 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({}); 9 | service = TestBed.inject(RemoteComponentLoader); 10 | }); 11 | 12 | it('should be created', () => { 13 | expect(service).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/services/remote-component-loader.ts: -------------------------------------------------------------------------------- 1 | import { createNgModuleRef, Injectable, Injector, NgZone, Type } from '@angular/core'; 2 | 3 | import { loadMfe, LoadMfeOptions } from '../helpers'; 4 | import { RemoteComponentWithModule, StandaloneRemoteComponent } from '../interfaces'; 5 | import { ComponentWithNgModuleRef } from '../types'; 6 | import { RemoteComponentsCache } from './remote-components-cache'; 7 | 8 | /** 9 | * A low-level service for loading a remote micro-frontend component. 10 | */ 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class RemoteComponentLoader { 15 | constructor( 16 | private readonly _ngZone: NgZone, 17 | private readonly _injector: Injector, 18 | private readonly _cache: RemoteComponentsCache 19 | ) {} 20 | 21 | /** 22 | * Loads a remote component with module where was declared this component. 23 | * @param remoteComponent Remote component. 24 | * @param injector (Optional) Injector, use root injector by default. 25 | * @param options (Optional) object of options. 26 | */ 27 | public async loadComponentWithModule( 28 | remoteComponent: RemoteComponentWithModule, 29 | injector: Injector = this._injector, 30 | options?: LoadMfeOptions 31 | ): Promise> { 32 | try { 33 | if (this._cache.isRegistered(remoteComponent)) { 34 | return this._cache.getValue(remoteComponent); 35 | } 36 | 37 | this._cache.register(remoteComponent); 38 | 39 | const { component, module } = await this._ngZone.runOutsideAngular(async () => { 40 | const component = await loadMfe( 41 | remoteComponent.app, 42 | remoteComponent.component, 43 | options 44 | ); 45 | const module = await loadMfe( 46 | remoteComponent.app, 47 | remoteComponent.module, 48 | options 49 | ); 50 | 51 | return { component, module }; 52 | }); 53 | 54 | const ngModuleRef = createNgModuleRef(module, injector); 55 | 56 | const componentWithNgModuleRef: ComponentWithNgModuleRef = { component, ngModuleRef }; 57 | this._cache.setValue(remoteComponent, componentWithNgModuleRef); 58 | return componentWithNgModuleRef; 59 | } catch (error: unknown) { 60 | this._cache.setError(remoteComponent, error); 61 | throw error; 62 | } 63 | } 64 | 65 | /** 66 | * Loads a standalone remote component. 67 | * @param remoteComponent Remote component 68 | * @param options (Optional) object of options. 69 | */ 70 | public async loadStandaloneComponent( 71 | remoteComponent: StandaloneRemoteComponent, 72 | options?: LoadMfeOptions 73 | ): Promise> { 74 | try { 75 | if (this._cache.isRegistered(remoteComponent)) { 76 | return this._cache.getValue(remoteComponent); 77 | } 78 | 79 | this._cache.register(remoteComponent); 80 | 81 | 82 | const componentType = await this._ngZone.runOutsideAngular(() => 83 | loadMfe(remoteComponent.app, remoteComponent.component, options) 84 | ); 85 | 86 | this._cache.setValue(remoteComponent, componentType); 87 | 88 | return componentType; 89 | } catch (error: unknown) { 90 | this._cache.setError(remoteComponent, error); 91 | throw error; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/services/remote-components-cache.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { RemoteComponentsCache } from '.'; 4 | 5 | describe('RemoteComponentsCache', () => { 6 | let service: RemoteComponentsCache; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(RemoteComponentsCache); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/services/remote-components-cache.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Type } from '@angular/core'; 2 | import { AsyncSubject, lastValueFrom } from 'rxjs'; 3 | 4 | import { 5 | isRemoteComponentWithModule, 6 | RemoteComponentWithModule, 7 | RemoteComponent, 8 | StandaloneRemoteComponent, 9 | isStandaloneRemoteComponent, 10 | } from '../interfaces'; 11 | import { ComponentWithNgModuleRef } from '../types'; 12 | 13 | /** 14 | * Cache of the loaded micro-frontend apps. 15 | * 16 | * Main reasons to create cache: 17 | * 1) Avoid race condition, when same micro-frontend are requested twice or more times at the same time. 18 | * 2) Cache already loaded MFE component and dont make same request twice. 19 | */ 20 | @Injectable({ 21 | providedIn: 'root', 22 | }) 23 | export class RemoteComponentsCache { 24 | private readonly _map = new Map | Type>>(); 25 | 26 | /** 27 | * Register a new micro-frontend cache. 28 | * @param remoteComponent Mfe Remote Component 29 | */ 30 | public register(remoteComponent: RemoteComponent): void { 31 | if (this.isRegistered(remoteComponent)) return; 32 | 33 | const key = this.generateKey(remoteComponent); 34 | this._map.set(key, new AsyncSubject | Type>()); 35 | } 36 | 37 | /** 38 | * Unregister a micro-frontend cache. 39 | * @param remoteComponent Mfe Remote Component 40 | */ 41 | public unregister(remoteComponent: RemoteComponent): void { 42 | if (!this.isRegistered(remoteComponent)) return; 43 | 44 | const key = this.generateKey(remoteComponent); 45 | this._map.delete(key); 46 | } 47 | 48 | /** 49 | * Checks that specified micro-frontend app already registered. 50 | * @param remoteComponent Mfe Remote Component 51 | */ 52 | public isRegistered(remoteComponent: RemoteComponent): boolean { 53 | const key = this.generateKey(remoteComponent); 54 | return this._map.has(key); 55 | } 56 | 57 | /** 58 | * Set to cache ComponentWithNgModuleRef of micro-frontend. 59 | * @param remoteComponent Mfe Remote Component 60 | * @param value ComponentWithNgModuleRef for that micro-frontend 61 | */ 62 | public setValue( 63 | remoteComponent: RemoteComponentWithModule, 64 | value: ComponentWithNgModuleRef 65 | ): void; 66 | 67 | public setValue( 68 | remoteComponent: StandaloneRemoteComponent, 69 | value: Type 70 | ): void; 71 | 72 | public setValue( 73 | remoteComponent: RemoteComponent, 74 | value: ComponentWithNgModuleRef | Type 75 | ): void { 76 | if (!this.isRegistered(remoteComponent)) { 77 | throw new Error( 78 | `Error while trying to set value into MFE cache, this key - "${JSON.stringify( 79 | remoteComponent 80 | )}" does not exist in cache` 81 | ); 82 | } 83 | 84 | if (isStandaloneRemoteComponent(remoteComponent)) { 85 | const cache = this.getCache(remoteComponent); 86 | cache.next(value as Type); 87 | cache.complete(); 88 | return; 89 | } 90 | 91 | const cache = this.getCache(remoteComponent); 92 | cache.next(value as ComponentWithNgModuleRef); 93 | cache.complete(); 94 | } 95 | 96 | /** 97 | * Sets the error that occurs in the loading and compiling micro-frontend. 98 | * @param remoteComponent Mfe Remote Component 99 | * @param error Error 100 | */ 101 | public setError(remoteComponent: RemoteComponent, error: any): void { 102 | if (!this.isRegistered(remoteComponent)) { 103 | throw new Error( 104 | `Error while trying to set error into MFE cache, this key - "${JSON.stringify( 105 | remoteComponent 106 | )}" does not exist in cache` 107 | ); 108 | } 109 | 110 | const cache = this.getCache(remoteComponent); 111 | cache.error(error); 112 | cache.complete(); 113 | } 114 | 115 | /** 116 | * Gets ComponentWithNgModuleRef or Component Class of the micro-frontend. 117 | * 118 | * --------------------- 119 | * Returns ComponentWithNgModuleRef of MFE for component with module, 120 | * or returns Component Class of MFE for standalone component. 121 | * @param remoteComponent Mfe Remote Component 122 | */ 123 | public getValue( 124 | remoteComponent: RemoteComponentWithModule 125 | ): Promise>; 126 | 127 | public getValue( 128 | remoteComponent: StandaloneRemoteComponent 129 | ): Promise>; 130 | 131 | public getValue( 132 | remoteComponent: RemoteComponent 133 | ): Promise> | Promise> { 134 | if (isStandaloneRemoteComponent(remoteComponent)) { 135 | const cache = this.getCache(remoteComponent); 136 | return lastValueFrom(cache); 137 | } 138 | 139 | const cache = this.getCache(remoteComponent); 140 | return lastValueFrom(cache); 141 | } 142 | 143 | /** 144 | * Gets the AsyncSubject cache value from Map 145 | * @param remoteComponent Mfe Remote Component 146 | */ 147 | protected getCache( 148 | remoteComponent: RemoteComponentWithModule 149 | ): AsyncSubject>; 150 | 151 | protected getCache( 152 | remoteComponent: StandaloneRemoteComponent 153 | ): AsyncSubject>; 154 | 155 | protected getCache( 156 | remoteComponent: RemoteComponent 157 | ): AsyncSubject> | AsyncSubject> { 158 | const key = this.generateKey(remoteComponent); 159 | const value = this._map.get(key); 160 | 161 | if (!value) 162 | throw new Error( 163 | `Error MFE "${JSON.stringify(remoteComponent)}" does not exist in cache` 164 | ); 165 | 166 | if (isStandaloneRemoteComponent(remoteComponent)) { 167 | return value as AsyncSubject>; 168 | } 169 | 170 | return value as AsyncSubject>; 171 | } 172 | 173 | /** 174 | * Generates a cache key based on RemoteComponent 175 | * @param remoteComponent Mfe Remote Component 176 | */ 177 | protected generateKey(remoteComponent: RemoteComponent): string { 178 | if (isRemoteComponentWithModule(remoteComponent)) { 179 | return `${remoteComponent.app}/${remoteComponent.component}/${remoteComponent.module}`; 180 | } 181 | 182 | return `${remoteComponent.app}/${remoteComponent.component}`; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/types/component-with-ng-module-ref.ts: -------------------------------------------------------------------------------- 1 | import { Type, NgModuleRef } from "@angular/core"; 2 | 3 | export type ComponentWithNgModuleRef = { 4 | component: Type; 5 | ngModuleRef: NgModuleRef; 6 | } -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './component-with-ng-module-ref' 2 | export * from './mfe-outlet-inputs'; 3 | export * from './mfe-outlet-outputs'; -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/types/mfe-outlet-inputs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Inputs that projects to micro-frontend component. 3 | */ 4 | export type MfeOutletInputs = Record; 5 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/lib/types/mfe-outlet-outputs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Outputs that projects to micro-frontend component. 3 | */ 4 | export type MfeOutletOutputs = Record void>; 5 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-mfe 3 | */ 4 | 5 | export * from './lib/directives'; 6 | export * from './lib/helpers'; 7 | export * from './lib/injection-tokens'; 8 | export * from './lib/interfaces'; 9 | export * from './lib/mfe.module'; 10 | export * from './lib/registry'; 11 | export * from './lib/services'; 12 | 13 | -------------------------------------------------------------------------------- /projects/ngx-mfe/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | // First, initialize the Angular testing environment. 12 | getTestBed().initTestEnvironment( 13 | BrowserDynamicTestingModule, 14 | platformBrowserDynamicTesting(), 15 | { teardown: { destroyAfterEach: true }}, 16 | ); 17 | -------------------------------------------------------------------------------- /projects/ngx-mfe/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": [ 11 | "dom", 12 | "es2018" 13 | ] 14 | }, 15 | "exclude": [ 16 | "src/test.ts", 17 | "**/*.spec.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /projects/ngx-mfe/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false, 6 | "removeComments": true, 7 | "sourceMap": true, 8 | }, 9 | "angularCompilerOptions": { 10 | "compilationMode": "partial" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /projects/ngx-mfe/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "esModuleInterop": true, 9 | "strict": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "sourceMap": true, 13 | "declaration": false, 14 | "paths": { 15 | "ngx-mfe": [ 16 | "dist/ngx-mfe/ngx-mfe", 17 | "dist/ngx-mfe" 18 | ] 19 | }, 20 | "experimentalDecorators": true, 21 | "moduleResolution": "node", 22 | "importHelpers": true, 23 | "target": "ES2022", 24 | "module": "es2020", 25 | "lib": [ 26 | "es2018", 27 | "dom" 28 | ], 29 | "useDefineForClassFields": false 30 | }, 31 | "angularCompilerOptions": { 32 | "enableI18nLegacyMessageIdFormat": false, 33 | "strictInjectionParameters": true, 34 | "strictInputAccessModifiers": true, 35 | "strictTemplates": true 36 | } 37 | } 38 | --------------------------------------------------------------------------------