From b0a9ec578d3369be2872bc6442853bc3dc2f6fd4 Mon Sep 17 00:00:00 2001 From: Manfred Steyer Date: Sun, 13 Mar 2022 00:08:58 +0100 Subject: [PATCH] v14.2 --- libs/mf-runtime/package.json | 2 +- libs/mf-tools/README.md | 155 ++++++++---------- libs/mf-tools/package.json | 4 +- .../src/lib/web-components/bootstrap-utils.ts | 40 ++++- libs/mf/package.json | 4 +- 5 files changed, 115 insertions(+), 90 deletions(-) diff --git a/libs/mf-runtime/package.json b/libs/mf-runtime/package.json index 3dc65142..ff6aee2c 100644 --- a/libs/mf-runtime/package.json +++ b/libs/mf-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@angular-architects/module-federation-runtime", "license": "MIT", - "version": "14.1.1", + "version": "14.2.0", "peerDependencies": { "@angular/common": ">=12.0.0", "@angular/core": ">=12.0.0" diff --git a/libs/mf-tools/README.md b/libs/mf-tools/README.md index 2419f3c7..cb065864 100644 --- a/libs/mf-tools/README.md +++ b/libs/mf-tools/README.md @@ -23,6 +23,7 @@ This can help to **balance the trade-off** between bundle size and isolation of - [Source Code for Micro Frontend](https://github.com/manfredsteyer/angular-app1) - [Source Code for Micro Frontend with Routing](https://github.com/manfredsteyer/angular3-app) - [Source Code for Micro Frontend with Vue](https://github.com/manfredsteyer/vue-js) +- [Source Code for Micro Frontend with React](https://github.com/manfredsteyer/react-app) - [Source Code for Micro Frontend with AngularJS](https://github.com/manfredsteyer/angularjs-app) ## Tutorial @@ -31,7 +32,17 @@ Please find our [tutorial here](https://github.com/angular-architects/module-fed ## Providing a Web Component with Module Federation -Expose your Angular components via Angular Elements: +This helper packages assumes that Micro Frontends are exposed as Web Components. + +### Exposing Micro Frontend as Web Component in Angular + +To do this in Angular, install `@angular/elements`: + +```bash +npm i @angular/elements +``` + +Then you can directly convert your AppComponent to a Web Component: ```typescript import { createCustomElement } from '@angular/elements'; @@ -56,6 +67,12 @@ export class AppModule { } ``` +### Exposing Web Component with other Frameworks like Ract + +If you framework doesn't directly support exposing your application as a web component, you can easily write a simple Wrapper around it. Basically, a web component -- to be more precise: a custom element -- is just an EcmaScript class extending ``HtmlElement`` and registered via ``customElements.register``. Please find an [example for React here](https://github.com/manfredsteyer/react-app/blob/main/app.js). + +### Exposting Web Component-based Micro Frontend via Module Federation + Add ``@angular-architects/module-federation`` to your micro frontend: ``` @@ -74,9 +91,12 @@ exposes: { }, ``` -## Bootstrapping +If the file that bootstraps your applications is called differently, adjust these settings accordingly. + +## Helper for Angular + +For enabling Angular for a multi version/ multi framework scenario, we need some helper functions. The easiest way to use them, is to bootstrap your Angular app with our bootstrap helper: -Our ``bootstrap`` helper function bootstraps your shell and your micro frontend and takes care of some details needed in a multi-framework/ multi-version scenario (like sharing the ``platform`` used). ```typescript // main.ts @@ -85,71 +105,15 @@ import { environment } from './environments/environment'; import { bootstrap } from '@angular-architects/module-federation-tools'; bootstrap(AppModule, { - production: environment.production + production: environment.production, + appType: 'shell', + // appType: 'microfrontend' }); ``` > Use this bootstrap helper for **both**, your shell and your micro frontends! -## Sharing Zone.js - -In order to share zone.js, call our ``shareNgZone`` helper when starting the shell. - -```typescript -import { Component, NgZone } from '@angular/core'; -import { shareNgZone } from '@angular-architects/module-federation-tools'; - -@Component({ - selector: 'app-root', - templateUrl: './app.component.html', -}) -export class AppComponent { - title = 'shell'; - - constructor(private ngZone: NgZone) { - shareNgZone(ngZone); - } - -} -``` - -The micro frontends will pick it up, if they are bootstrapped with the ``bootstrap`` helper (see above). - -## Details on ngZone and Platform sharing - -> In a multi version micro frontend strategy, it is important to load the zone.js bundle to the window object only once. Also, one need to make sure that only one instance of the ngZone is used by all the micro frontends. - -If you share `@angular/core` and therefore also have one technical reference to the BrowserPlatform, that is used by more than one micro frondend, Angular's default setup is, to support only one platform instance per shared version. Be aware that you **need** to create multi platform instances in case of different versions, but also in case the version is the same, but `@angular/core` is not shared, but packed into the micro frontend's bundles directly (like in Angular's default way w/o module federation). - -Naturally, such technical details are hard to get into. Therefore the `bootstrap()` function of this package helps to implement your multi version strategy w/o the need of implementing those low-level aspects on your own. - -Some optional flags are offered to provide options for custom behavior of the `bootstrap()` function: - -- `ngZoneSharing: false`: Deactivate ngZone sharing in the window object (not recommended): - ```typescript - bootstrap(AppModule, { - production: environment.production, - ngZoneSharing: false // defaults to true - }); - ``` -- `platformSharing: false`: Deactivate Platform sharing in the window object (not recommended): - ```typescript - bootstrap(AppModule, { - production: environment.production, - platformSharing: false // defaults to true - }); - ``` - - Possible, if dependencies are not shared or each bootstrapped remote app uses a different version. -- `activeLegacyMode: false`: Deactivates the legacy mode that provides backwards compatibility for Platform sharing: - ```typescript - bootstrap(AppModule, { - production: environment.production, - activeLegacyMode: false // defaults to true - }); - ``` - - If all your micro frontends use `@angular-architects/module-federation-tools` in version `^12.6.0`, `^13.1.0` or any newer major version you can switch off the legacy mode manually. - - Those versions introduced new features on how to share the Platform in the window object. - - This allows to use the `bootstrap()` function even in such cases, where the same version is packed into different micro frontend bundles. +Please make sure to set the ``appType`` to ``shell`` for your shell application and to ``microfrontend`` for your Micro Frontends. ## Routing to Web Components @@ -218,28 +182,15 @@ export const APP_ROUTES: Routes = [ ```typescript // Micro Frontend -export const APP_ROUTES: Routes = [ - [...] - { matcher: endsWith('a'), component: AComponent}, - { matcher: endsWith('b'), component: BComponent}, - [...] -} -``` - -In order to prevent issues with the "inner" router, use our helper function ``connectRouter``. - -```typescript -// AppComponent in Micro Frontend -@Component({ ... }) -export class AppComponent { - - constructor(private router: Router) { } +RouterModule.forRoot([ + { path: 'angular3/a', component: AComponent }, + { path: 'angular3/b', component: BComponent }, - ngOnInit(): void { - connectRouter(this.router); - } + // To prevent issues when routing to other micro frontends + // a catch-all route should be defined + { path: '**', component: EmptyComponent }, -} +]); ``` ## Directly Loading a Web Component via Module Federation @@ -277,6 +228,44 @@ events = { ``` +## Some Additional Details + +> In a multi version micro frontend strategy, it is important to load the zone.js bundle to the window object only once. Also, one need to make sure that only one instance of the ngZone is used by all the micro frontends. + +If you share `@angular/core` and therefore also have one technical reference to the BrowserPlatform, that is used by more than one micro frondend, Angular's default setup is, to support only one platform instance per shared version. Be aware that you **need** to create multi platform instances in case of different versions, but also in case the version is the same, but `@angular/core` is not shared, but packed into the micro frontend's bundles directly (like in Angular's default way w/o module federation). + +Naturally, such technical details are hard to get into. Therefore the `bootstrap()` function of this package helps to implement your multi version strategy w/o the need of implementing those low-level aspects on your own. + +Some optional flags are offered to provide options for custom behavior of the `bootstrap()` function: + +- `ngZoneSharing: false`: Deactivate ngZone sharing in the window object (not recommended): + ```typescript + bootstrap(AppModule, { + production: environment.production, + ngZoneSharing: false // defaults to true + }); + ``` +- `platformSharing: false`: Deactivate Platform sharing in the window object (not recommended): + ```typescript + bootstrap(AppModule, { + production: environment.production, + platformSharing: false // defaults to true + }); + ``` + - Possible, if dependencies are not shared or each bootstrapped remote app uses a different version. +- `activeLegacyMode: false`: Deactivates the legacy mode that provides backwards compatibility for Platform sharing: + ```typescript + bootstrap(AppModule, { + production: environment.production, + activeLegacyMode: false // defaults to true + }); + ``` + - If all your micro frontends use `@angular-architects/module-federation-tools` in version `^12.6.0`, `^13.1.0` or any newer major version you can switch off the legacy mode manually. + - Those versions introduced new features on how to share the Platform in the window object. + - This allows to use the `bootstrap()` function even in such cases, where the same version is packed into different micro frontend bundles. + + + ## More about the underlying ideas Please find more information on the underlying ideas in this [blog article](https://www.angulararchitects.io/aktuelles/multi-framework-and-version-micro-frontends-with-module-federation-the-good-the-bad-the-ugly). diff --git a/libs/mf-tools/package.json b/libs/mf-tools/package.json index 766ca52a..163ec68b 100644 --- a/libs/mf-tools/package.json +++ b/libs/mf-tools/package.json @@ -1,12 +1,12 @@ { "name": "@angular-architects/module-federation-tools", - "version": "14.1.1", + "version": "14.2.0", "license": "MIT", "peerDependencies": { "@angular/common": ">=11.0.0", "@angular/core": ">=11.0.0", "@angular/router": ">=11.0.0", - "@angular-architects/module-federation": "^14.1.1", + "@angular-architects/module-federation": "^14.2.0", "@angular/platform-browser": ">=11.0.0", "rxjs": ">= 6.0.0" }, diff --git a/libs/mf-tools/src/lib/web-components/bootstrap-utils.ts b/libs/mf-tools/src/lib/web-components/bootstrap-utils.ts index dabbf9a0..f50e27d8 100644 --- a/libs/mf-tools/src/lib/web-components/bootstrap-utils.ts +++ b/libs/mf-tools/src/lib/web-components/bootstrap-utils.ts @@ -1,13 +1,18 @@ -import { CompilerOptions, enableProdMode, NgModuleRef, NgZone, PlatformRef, Type, Version } from "@angular/core"; +import { CompilerOptions, enableProdMode, Injector, NgModuleRef, NgZone, PlatformRef, Type, Version } from "@angular/core"; import { platformBrowser } from "@angular/platform-browser"; import { VERSION } from '@angular/core'; import { getGlobalStateSlice, setGlobalStateSlice } from "../utils/global-state"; +import { Router } from "@angular/router"; +import { connectRouter } from "./router-utils"; + +export type AppType = 'shell' | 'microfrontend'; export type Options = { production: boolean, platformFactory?: () => PlatformRef, compilerOptions?: CompilerOptions & BootstrapOptions, version?: () => string | Version, + appType?: AppType; /** * Opt-out of ngZone sharing. * Not recommanded. @@ -161,5 +166,36 @@ export function bootstrap(module: Type, options: Options): Promise { + + if (options.appType === 'shell') { + shareShellZone(ref.injector); + } + else if (options.appType === 'microfrontend') { + connectMicroFrontendRouter(ref.injector); + } + + return ref; + }); +} + +function shareShellZone(injector: Injector) { + const ngZone = injector.get(NgZone, null); + if (!ngZone) { + console.warn('No NgZone to share found'); + return; + } + shareNgZone(ngZone); +} + +function connectMicroFrontendRouter(injector: Injector) { + const router = injector.get(Router); + + if (!router) { + console.warn('No router to connect found'); + return; + } + + connectRouter(router); } + diff --git a/libs/mf/package.json b/libs/mf/package.json index ee7a7692..5c116d82 100644 --- a/libs/mf/package.json +++ b/libs/mf/package.json @@ -1,6 +1,6 @@ { "name": "@angular-architects/module-federation", - "version": "14.1.1", + "version": "14.2.0", "license": "MIT", "repository": { "type": "GitHub", @@ -17,7 +17,7 @@ "schematics": "./collection.json", "builders": "./builders.json", "dependencies": { - "@angular-architects/module-federation-runtime": "14.1.1", + "@angular-architects/module-federation-runtime": "14.2.0", "word-wrap": "^1.2.3", "callsite": "^1.0.0", "node-fetch": "^2.6.7",