Skip to content

Commit

Permalink
refactor(platform): remove gateway iframe in client-broker communication
Browse files Browse the repository at this point in the history
closes #14
  • Loading branch information
danielwiehl committed Dec 9, 2021
1 parent 61b7de5 commit 74f9037
Show file tree
Hide file tree
Showing 18 changed files with 421 additions and 693 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class DevToolsManifestService {
new QualifierMatcher(intention.qualifier, {evalAsterisk: true, evalOptional: true}).matches(capability.qualifier) ||
new QualifierMatcher(capability.qualifier, {evalAsterisk: true, evalOptional: true}).matches(intention.qualifier)
)),
filterArray(intention => isCapabilityVisibleToApplication(capability, intention.metadata.appSymbolicName)),
filterArray(intention => this.isCapabilityVisibleToApplication(capability, intention.metadata.appSymbolicName)),
);
}

Expand All @@ -118,13 +118,13 @@ export class DevToolsManifestService {
new QualifierMatcher(intention.qualifier, {evalAsterisk: true, evalOptional: true}).matches(capability.qualifier) ||
new QualifierMatcher(capability.qualifier, {evalAsterisk: true, evalOptional: true}).matches(intention.qualifier)
)),
filterArray(capability => isCapabilityVisibleToApplication(capability, intention.metadata.appSymbolicName)),
filterArray(capability => this.isCapabilityVisibleToApplication(capability, intention.metadata.appSymbolicName)),
);
}
}

function isCapabilityVisibleToApplication(capability: Capability, appSymbolicName: string): boolean {
return !capability.private || this._appsBySymbolicName.get(appSymbolicName).scopeCheckDisabled || capability.metadata.appSymbolicName === appSymbolicName;
private isCapabilityVisibleToApplication(capability: Capability, appSymbolicName: string): boolean {
return !capability.private || this._appsBySymbolicName.get(appSymbolicName).scopeCheckDisabled || capability.metadata.appSymbolicName === appSymbolicName;
}
}

const byType = (a: ManifestObject, b: ManifestObject): number => a.type.localeCompare(b.type);
Expand Down
22 changes: 11 additions & 11 deletions apps/microfrontend-platform-devtools/src/app/ng-zone-decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,28 @@ export class NgZoneMessageClientDecorator implements BeanDecorator<MessageClient
constructor(private _zone: NgZone) {
}

public decorate(messageClient: MessageClient): MessageClient {
public decorate(delegate: MessageClient): MessageClient {
const zone = this._zone;
return new class implements MessageClient {

public publish<T = any>(topic: string, message?: T, options?: PublishOptions): Promise<void> {
return messageClient.publish(topic, message, options);
return delegate.publish(topic, message, options);
}

public request$<T>(topic: string, request?: any, options?: RequestOptions): Observable<TopicMessage<T>> {
return messageClient.request$<T>(topic, request, options).pipe(synchronizeWithAngular(zone));
return delegate.request$<T>(topic, request, options).pipe(synchronizeWithAngular(zone));
}

public observe$<T>(topic: string): Observable<TopicMessage<T>> {
return messageClient.observe$<T>(topic).pipe(synchronizeWithAngular(zone));
return delegate.observe$<T>(topic).pipe(synchronizeWithAngular(zone));
}

public onMessage<IN = any, OUT = any>(topic: string, callback: (message: TopicMessage<IN>) => Observable<OUT> | Promise<OUT> | OUT | void): Subscription {
return messageClient.onMessage(topic, callback);
return delegate.onMessage(topic, callback);
}

public subscriberCount$(topic: string): Observable<number> {
return messageClient.subscriberCount$(topic).pipe(synchronizeWithAngular(zone));
return delegate.subscriberCount$(topic).pipe(synchronizeWithAngular(zone));
}
};
}
Expand All @@ -59,24 +59,24 @@ export class NgZoneIntentClientDecorator implements BeanDecorator<IntentClient>
constructor(private _zone: NgZone) {
}

public decorate(intentClient: IntentClient): IntentClient {
public decorate(delegate: IntentClient): IntentClient {
const zone = this._zone;
return new class implements IntentClient {

public publish<T = any>(intent: Intent, body?: T, options?: IntentOptions): Promise<void> {
return intentClient.publish(intent, body, options);
return delegate.publish(intent, body, options);
}

public request$<T>(intent: Intent, body?: any, options?: IntentOptions): Observable<TopicMessage<T>> {
return intentClient.request$<T>(intent, body, options).pipe(synchronizeWithAngular(zone));
return delegate.request$<T>(intent, body, options).pipe(synchronizeWithAngular(zone));
}

public observe$<T>(selector?: Intent): Observable<IntentMessage<T>> {
return intentClient.observe$<T>(selector).pipe(synchronizeWithAngular(zone));
return delegate.observe$<T>(selector).pipe(synchronizeWithAngular(zone));
}

public onIntent<IN = any, OUT = any>(selector: IntentSelector, callback: (intentMessage: IntentMessage<IN>) => Observable<OUT> | Promise<OUT> | OUT | void): Subscription {
return intentClient.onIntent(selector, callback);
return delegate.onIntent(selector, callback);
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ This part of the documentation is for developers who want to integrate the SCION
[.chapter-title]
In this Chapter
- <<chapter:angular-integration-guide:configuring-hash-based-routing>>
- <<chapter:angular-integration-guide:starting-platform-in-app-initializer>>
- <<chapter:angular-integration-guide:connecting-to-host-in-app-initializer>>
- <<chapter:angular-integration-guide:using-route-resolver-instead-app-initializer>>
- <<chapter:angular-integration-guide:configuring-hash-based-routing>>
- <<chapter:angular-integration-guide:activate-custom-elements-schema>>
- <<chapter:angular-integration-guide:providing-activator-services>>
- <<chapter:angular-integration-guide:providing-platform-beans-for-dependency-injection>>
Expand All @@ -22,27 +22,15 @@ In this Chapter
****
'''

[[chapter:angular-integration-guide:configuring-hash-based-routing]]
[discrete]
=== Configuring Hash-Based Routing
We recommend using hash-based routing over HTML 5 push-state routing in micro applications. To enable hash-based routing in an Angular application, pass `{useHash: true}` when configuring the router in `AppRoutingModule`, as follows.

[source,typescript]
----
include::angular-integration-guide.snippets.ts[tags=configure-hash-based-routing]
----

====
TIP: Read chapter <<chapter:miscellaneous:routing-in-micro-applications>> to learn more about why to prefer hash-based routing.
====

[[chapter:angular-integration-guide:starting-platform-in-app-initializer]]
[discrete]
=== Starting the Platform Host in an Angular App Initializer

The platform should be started during the bootstrapping of the Angular application, that is, before displaying content to the user. Hence, we recommend starting the platform in an app initializer. See chapter <<chapter:angular-integration-guide:using-route-resolver-instead-app-initializer>> for an alternative approach.

NOTE: Angular allows hooking into the process of initialization by providing an initializer to the `APP_INITIALIZER` injection token. Angular will wait until all initializers resolved to start the application, making it the ideal place for starting the SCION Microfrontend Platform.
Angular allows hooking into the process of initialization by providing an initializer to the Angular's `APP_INITIALIZER` injection token. Angular will wait until all initializers resolved to start the application, making it the ideal place for starting the SCION Microfrontend Platform.

NOTE: We recommend starting the platform outside of the Angular zone in order to avoid excessive change detection cycles of platform-internal subscriptions to global DOM events.

The following code snippet configures an Angular `APP_INITIALIZER` to start the platform. In the provider definition, we reference a higher order factory function and instruct Angular to inject the `PlatformInitializer` as function argument.

Expand All @@ -64,7 +52,7 @@ include::start-platform-via-initializer.snippets.ts[tags=host-app:initializer]
----
<1> Injects the Angular `NgZone`.
<2> Declares or loads the platform config, e.g., using `HttpClient`.
<3> Starts the platform host. It is super important to start the platform outside the Angular zone to avoid excessive change detection cycles.
<3> Starts the platform host. We recommend starting it outside of the Angular zone in order to avoid excessive change detection cycles.


====
Expand All @@ -76,20 +64,22 @@ TIP: Refer to chapter <<chapter:configuration:starting-the-platform-in-host-appl
=== Connecting to the Host in an Angular App Initializer
A micro application should connect to the platform host during the bootstrapping of the Angular application, that is, before displaying content to the user. Hence, we recommend connecting to the platform host in an app initializer. See chapter <<chapter:angular-integration-guide:using-route-resolver-instead-app-initializer>> for an alternative approach.

NOTE: We recommend connecting to the platform host outside of the Angular zone in order to avoid excessive change detection cycles of platform-internal subscriptions to global DOM events.

[source,typescript]
----
include::start-platform-via-initializer.snippets.ts[tags=micro-app:initializer]
----
<1> Provides an initializer to the `APP_INITIALIZER` injection token.
<2> Instructs Angular to pass the `NgZone` to the higher order function.
<3> Returns the initializer function, which connects to the host. It is super important to start the platform outside the Angular zone to avoid excessive change detection cycles.
<3> Returns the initializer function, which connects to the host. We recommend connecting to the host outside of the Angular zone in order to avoid excessive change detection cycles.

[[chapter:angular-integration-guide:using-route-resolver-instead-app-initializer]]
[discrete]
=== Using a Route Resolver instead of an App Initializer
If you cannot use an app initializer for starting the platform or connecting to the platform host, an alternative would be to use a route resolver.
If you cannot use an app initializer for starting the platform or connecting to the platform host, an alternative would be to use a route resolver. Angular allows installing resolvers on a route, allowing data to be resolved asynchronously before the route is finally activated.

NOTE: Angular allows installing resolvers on a route, allowing data to be resolved asynchronously before the route is finally activated.
NOTE: We recommend connecting to the platform host outside of the Angular zone in order to avoid excessive change detection cycles of platform-internal subscriptions to global DOM events.

The following code snippet illustrates how to connect to the platform host using an Angular resolver. Similarly, you could start the platform in the host application.

Expand All @@ -100,7 +90,7 @@ For a micro application, the resolver implementation could look as following:
include::start-platform-via-resolver.snippets.ts[tags=resolver]
----
<1> Injects the Angular `NgZone`.
<2> Connects to the platform host. It is super important to start the platform outside the Angular zone to avoid excessive change detection cycles.
<2> Connects to the platform host. We recommend connecting to the host outside of the Angular zone in order to avoid excessive change detection cycles.

Ensure that a micro application instance connects to the host only once. Therefore, it is recommended to install the resolver in a parent route common to all microfrontend routes. When loading a microfrontend for the first time, Angular will wait activating the child route until the platform finished starting. When navigating to another microfrontend of the micro application, the resolver would not resolve anew.

Expand All @@ -111,6 +101,20 @@ include::start-platform-via-resolver.snippets.ts[tags=resolver-registration]
<1> Installs the resolver on a component-less, empty-path route, which is parent to the microfrontend routes.
<2> Registers microfrontend routes as child routes.

[[chapter:angular-integration-guide:configuring-hash-based-routing]]
[discrete]
=== Configuring Hash-Based Routing
We recommend using hash-based routing over HTML 5 push-state routing in micro applications. To enable hash-based routing in an Angular application, pass `{useHash: true}` when configuring the router in `AppRoutingModule`, as follows.

[source,typescript]
----
include::angular-integration-guide.snippets.ts[tags=configure-hash-based-routing]
----

====
TIP: Read chapter <<chapter:miscellaneous:routing-in-micro-applications>> to learn more about why to prefer hash-based routing.
====

[[chapter:angular-integration-guide:activate-custom-elements-schema]]
[discrete]
=== Instruct Angular to allow Custom Elements in Templates
Expand Down Expand Up @@ -213,9 +217,28 @@ include::lazy-loaded-modules.snippets.ts[tags=microfrontend-1-module]

[[chapter:angular-integration-guide:preparing-messaging-for-use-with-angular]]
[discrete]
=== Preparing the MessageClient and IntentClient for use with Angular
=== Synchronizing MessageClient and IntentClient with the Angular Zone

If you start or connect to the platform outside the Angular zone, which we strongly recommend, messages and intents will be received outside of the Angular zone. In consequence, Angular does not trigger a change detection cycle, which may not update the UI as expected.

Consequently, when receiving messages or intents, you have to synchronize with the Angular zone. For example, as follows.

[source,typescript]
----
include::angular-integration-guide.snippets.ts[tags=synchronize-with-angular-zone-subscription]
----
<1> Runs the passed lambda inside the Angular zone.

You can also use the `observeInside` RxJS operator of `@scion/toolkit` to run downstream operators and the subscription handler inside of the Angular zone.

[source,typescript]
----
include::angular-integration-guide.snippets.ts[tags=synchronize-with-angular-zone-observeInside-operator]
----
<1> Runs downstream operators and the subscription handler inside of the Angular zone.


Messages and intents are published and received via a separate browsing context, preventing Angular (or more precisely zone.js) from triggering a change detection cycle, causing the UI not to update as expected. Therefore, we recommend to decorate the `MessageClient` and `IntentClient` with a bean decorator and pipe its Observables to emit inside the Angular zone.
Alternatively, to not have to synchronize each subscription, you can decorate the two beans `MessageClient` and `IntentClient` to emit directly inside the Angular zone. This is probably the most practical approach.

TIP: Decorators allow intercepting bean method invocations. For more information about decorators, refer to the link:https://github.com/SchweizerischeBundesbahnen/scion-toolkit/blob/master/docs/site/tools/bean-manager.md[documentation of the bean manager, window=\"_blank\"].

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ContextService, IntentClient, ManifestService, MessageClient, OutletRouter } from '@scion/microfrontend-platform';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Beans } from '@scion/toolkit/bean-manager';
import {ContextService, IntentClient, ManifestService, MessageClient, OutletRouter} from '@scion/microfrontend-platform';
import {NgZone} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {Beans} from '@scion/toolkit/bean-manager';
import {observeInside} from '@scion/toolkit/operators';

// tag::provide-platform-beans-for-dependency-injection[]
@NgModule({
Expand Down Expand Up @@ -35,3 +36,24 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
export class AppModule {
}
// end::add-custom-elements-schema[]

const zone: NgZone = undefined;

// tag::synchronize-with-angular-zone-subscription[]
Beans.get(MessageClient).observe$('topic').subscribe(message => {
console.log(NgZone.isInAngularZone()); // Prints `false`

zone.run(() => { // <1>
console.log(NgZone.isInAngularZone()); // Prints `true`
});
});
// end::synchronize-with-angular-zone-subscription[]

// tag::synchronize-with-angular-zone-observeInside-operator[]
Beans.get(MessageClient).observe$('topic')
.pipe(observeInside(continueFn => zone.run(continueFn))) // <1>
.subscribe(message => {
console.log(NgZone.isInAngularZone()); // Prints `true`
});
// end::synchronize-with-angular-zone-observeInside-operator[]

Loading

0 comments on commit 74f9037

Please sign in to comment.