Suche
Kolumne

Die Angular-Abenteuer: Microservices ins Frontend hineintragen? Micro Apps mit Angular Elements & Web Components

Manfred Steyer

©SuS_Media

In der Kolumne Die Angular-Abenteuer stürzt sich Manfred Steyer mitten ins Getümmel der dynamischen Welt der Webentwicklung. Tipps und Tricks für Angular-Entwickler kommen dabei ebenso zur Sprache wie aktuelle Entwicklungen im JavaScript-Ökosystem.

Dank Angular Elements können Angular-Komponenten als Web Components exportiert werden. Da sie sich separat verteilen und dynamisch in eine Shell laden lassen, bieten sie sich zur Realisierung von Micro Apps an. Der Monolith hat vielerorts ausgedient. Kleine wartbare Microservices liegen im Trend. Aber wie nutzt man diese Idee in der Welt von Single Page Applications? Von Frameworks unabhängige Web Components, die sich dynamisch in eine App-Shell laden lassen, sind hierfür eine moderne und attraktive Lösung, und genau die lassen sich nun mit Angular Elements erstellen. Dazu nutze ich ein Beispiel, das dynamisch Bundles mit Web Components lädt (Abb. 1).

Wie Abbildung 1 zeigt, kommen diese Web Components auch mit einem Router, der einen Wechsel zwischen den Seiten der Micro App erlaubt. Den Quellcode dazu gibt es wie immer in meinem GitHub-Account.

Angular Abenteuer 1

Abb. 1: So einfach kann eine Shell-Anwendung aussehen, die Web Components lädt.

UX Design im Videotutorial

Im entwickler.tutorial UX Design mit Angular, HTML & CSS bietet Timo Korinth, freiberuflicher UX-Entwickler, Berater und Trainer, einen Überblick über UX, Design, Konzeption und Entwicklung, sowie praktischer Tipps und Tricks aus der jahrelangen Erfahrung als UI-Entwickler.

Angular Elements

Seit Version 6 ist Angular Elements im Lieferumfang des beliebten SPA-Frameworks von Google enthalten. Damit lassen sich Angular-Komponenten als standardkonforme Web Components darstellen. Genaugenommen müsste man von Custom Elements sprechen, weil Web Components ein Sammelbegriff für verschiedene Standards darstellt. Alles, was man hierfür benötigt, ist das npm-Paket @angular/elements und die darin enthaltene createCustomElement-Funktion (Listing 1).

import { createCustomElement } from '@angular/elements';

[...]

@NgModule({
  [...]
  bootstrap: [],
  entryComponents: [
    AppComponent,
    ClientAWidgetComponent
  ]
})
export class AppModule { 
  constructor(private injector: Injector) {
  }

  ngDoBootstrap() {
    const appElement = createCustomElement(AppComponent, { injector: this.injector})
    customElements.define('client-a', appElement);
  }
}

Jede Komponente, die als Angular Element dargestellt werden soll, ist beim Modul der Wahl unter entryComponents einzutragen. Dadurch wird der Compiler auf die Komponente, die zur Laufzeit erst dynamisch instanziiert wird, aufmerksam. Verwendet man keine klassische Angular-basierte Bootstrap-Komponente, muss das Modul eine Methode ngDoBootstrap erhalten. Sie bietet sich an, um mit createCustomElement einen Wrapper für die Komponente zu erzeugen. Dieser lässt sie nach außen hin als Custom Element erscheinen. Bei der Gelegenheit bekommt das Custom Element auch den aktuellen Injector, um es am Dependency-Injection-Mechanismus von Angular anzuschließen. Der so geschaffene Wrapper lässt sich dann mit customElements.define beim Browser registrieren.

Da auch die gesamte Anwendung nur aus Komponenten besteht, bietet es sich an, Micro Apps auf diesem Weg zu exportieren. Das zuletzt betrachtete Beispiel macht das, indem es die AppComponent als client-a bereitstellt:

<client-a [state]="someState" (message)="handleMessage($event)"><client-a>

Der Einsatz von eckigen und runden Klammern lässt schon vermuten, dass das Custom Element innerhalb einer weiteren Angular-Anwendung zum Einsatz kommt. Diese Rahmenanwendung, die sich um das Laden einzelner Micro Apps kümmert, wird nachfolgend auch Shell genannt. Wichtig ist hier, dass die Shell mit jedem beliebigen Framework und sogar mit VanillaJS umgesetzt werden kann, da sich Custom Elements wie alle anderen HTML-Elemente verhalten.

Bietet die dahinterstehende Angular-Komponente Eigenschaften und Ereignisse an, können diese wie herkömmliche DOM-Attribute und -Ereignisse genutzt werden, um mit der Komponente zu kommunizieren. Genau das ist im betrachteten Beispiel der Fall: Über diese Mechanismen lässt sich ein Anwendungszustand (state) an die Micro App übergeben und es lassen sich Nachrichten von der Micro App an die Shell übertragen (message). Somit ergibt sich ein standardkonformer Weg, über den die Shell mit der Micro App kommunizieren kann.

Was noch fehlt, ist ein Build-Prozess, der ein einziges unabhängiges Bundle erzeugt, das sich bei Bedarf laden lässt. Dies lässt sich derzeit mit dem Angular CLI noch nicht bewerkstelligen, denn es erzeugt mehrere Bundles, die untereinander interagieren. Das führt zu einem Problem, wenn mehrere Micro Apps geladen werden. Daher greife ich auf einen einfachen webpack-Build zurück. Die Konfigurationsdatei dazu findet sich im Quellcode.

Shell

Auch bei der Shell handelt es sich um eine Angular-Anwendung. Um Custom Elements wie oben gezeigt nutzen zu können, muss in ihre Module das CUSTOM_ELEMENTS_SCHEMA aufgenommen werden (Listing 2).

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

[...]

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Dieses Schema stellt sicher, dass der Angular-Compiler keinen Fehler auswirft, wenn er auf Komponenten stößt, die er nicht kennt. Das ist notwendig, weil beim Kompilieren der Shell die einzelnen zur Laufzeit geladenen Custom Elements nicht zwangsweise bekannt sind.

Alternativ zum oben gezeigten direkten Aufruf eines Custom Elements lässt sich dieses mit ein wenig DOM-Programmierung auch dynamisch hinzufügen. Dazu müssen lediglich ein Script-Element, das auf das Bundle mit dem Custom Element verweist, sowie ein weiteres HTML-Element, das das Custom Element repräsentiert, erzeugt werden. (Listing 3).

const script = document.createElement('script');
script.src = 'client-a.bundle.js';
content.appendChild(script);

const element: HTMLElement = document.createElement('client-a');
content.appendChild(element);

element.addEventListener('message', msg => { […] });
element.setAttribute('state', 'init');

Routing

Die geladenen Micro Apps müssen wissen, wann sie sich am Bildschirm zu zeigen haben und welche ihrer Routen sie dann präsentieren sollen. Dazu reicht es, allen Routen einer Micro App ein einheitliches Präfix zu spendieren. Sieht man sich zum Beispiel die Adresszeile in Abbildung 1 an, fällt auf, dass der hier genutzte Präfix client-a lautet. Damit sich die Micro App versteckt, wenn ein anderer Präfix zum Einsatz kommt, hat jede Micro App auch eine Fallback-Route. Diese verweist auf eine Komponente mit leerem Template (Listing 4).

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      { path: 'client-a', component: CoreComponent, children: [
        { path: 'page1', component: Page1Component },
        { path: 'page2', component: Page2Component },
      ]},
      { path: '**', component: EmptyComponent }
    ], { useHash: true }),
    ReactiveFormsModule
  ],
  […]
})
export class AppModule { }

Damit die Router der einzelnen Micro Apps auch auf URL-Änderungen anderer Micro Apps reagieren, muss auf Routing im Hash-Fragment umgestellt werden (useHash: true). Außerdem sind die Router manuell zu aktivieren, da sie sich nun nicht mehr in einer Angular-basierten Bootstrap-Komponente befinden (Listing 5).

@Component([...])
export class ClientAComponent {
  constructor(private router: Router) {
    router.initialNavigation(); 
    // Manually triggering initial navigation
  }
}

Polyfills

Damit Custom Elements in jedem Browser funktionieren, müssen Polyfills eingebunden werden. Ich habe mich für das per npm verfügbare Polyfill @webcomponents/custom-elements entschieden, da es selbst dem Internet Explorer 11 auf die Sprünge hilft. Es kann zum Beispiel am Ende der vom CLI eingerichteten polyfills.ts referenziert werden:

import '@webcomponents/custom-elements/custom-elements.min';

Paradoxerweise benötigen auch Browser, die Custom Elements unterstützen, ein Polyfill. Der Grund dafür ist, dass Custom Elements für EcmaScript 2015 und aufwärts definiert sind, während als Kompilierungsziel aktuell meist noch das gute alte EcmaScript 5 zum Einsatz kommt. Ein solches Polyfill befindet sich auch im genannten npm-Paket. Ich habe es in der Datei angular.json unter scripts referenziert:

"scripts": [
  "node_modules/@webcomponents/custom-elements/src/native-shim.js"
]

Damit wird sichergestellt, dass dieses Polyfill in einem eigenen Bundle landet und sich Fehler bei der Ausführung, die sich in älteren Browsern ergeben, nicht auf andere Bundles auswirken.

Zusammenfassung und Bewertung

Mit Web Components lassen sich flexible Micro Apps schaffen, die sich in eine Shell geladen werden können. Dank Polyfills funktioniert dieser Ansatz auch in Evergreen-Browsern, wie Internet Explorer 11. Zusätzlich baut man hier auf einen zukunftsträchtigen Standard, der nicht an ein bestimmtes Framework gebunden ist. Genau diese Frameworkunabhängigkeit ist eines der Ziele hinter Microservices-Architekturen: Man möchte für jeden Microservice den jeweils besten Technologiestack einsetzen. Das ist insbesondere wichtig, wenn man bedenkt, dass Unternehmensanwendungen häufig zehn bis fünfzehn Jahre zu warten sind und niemand vorhersagen kann, wie sich Frameworks in dieser Zeit entwickeln. Außerdem können einzelnen Custom Elements separat entwickelt und separat veröffentlicht werden. Somit können einzelne Teams unabhängig(er) vorgehen.

Einen Kompromiss muss man bei den Bundle-Größen eingehen: Möchte man wirklich autarke und somit voneinander strikt isolierte Micro Apps haben, ist das jeweils verwendete Framework in die Bundles der einzelnen Micro Apps aufnehmen. Das führt im schlimmsten Fall zu einer Duplizierung des Frameworkcodes. Natürlich kann man zur Optimierung der Bundle-Größen die Frameworks außen vorlassen, sodass sie nur ein einziges Mal in die Shell zu laden sind. In diesem Fall ergibt sich jedoch eine Abhängigkeit zur Shell und der dort verwendeten Frameworkversion.

For free: The iJS React Cheat Sheet

For free: The iJS React Cheat Sheet

You want to get started with React? Our Cheat Sheet includes all the most important snippets. Get the sheet for free!

 

API Summit 2018
Christian Schwendtner

GraphQL – A query language for your API

mit Christian Schwendtner (PROGRAMMIERFABRIK)

Abstriche muss man im Punkt Isolation machen. Der mit Web Components assoziierte und von Angular unterstützte Shadow DOM verhindert zwar, dass sich Stylesheets der einen Micro App auf das Layout einer anderen auswirken, aber gegen konkurrierende JavaScript-Frameworks kann er nichts unternehmen. Probleme können sich insbesondere ergeben, wenn Bibliotheken globale Variablen nutzen. Ein Beispiel dafür ist jQuery und die hier häufig genutzte Variable $. Da solche Frameworks jedoch mehr oder weniger der Vergangenheit angehören und jüngere Entwicklungen Gebrauch von Modulsystemen wie CommonJS oder EcmaScript-Modulen machen, sollte sich dieses Problem heutzutage wenig auswirken.

Generell sollte man sich überlegen, ob man überhaupt eine Shell-Anwendung benötigt. Gibt es nämlich wenig Navigation und Kommunikation zwischen den einzelnen Micro Apps, kann es sich zur Vereinfachung lohnen, diese als separate Anwendungen bereitzustellen. Zur Integration können dann lediglich einfache Hyperlinks verwendet werden. Ein Beispiel dafür ist Office 365. Kommt man jedoch zu dem Schluss, dass eine Shell-Anwendung von Vorteil ist, ist man mit Custom Elements gut beraten.

Geschrieben von
Manfred Steyer
Manfred Steyer
Manfred Steyer ist selbstständiger Trainer und Berater mit Fokus auf Angular 2. In seinem aktuellen Buch "AngularJS: Moderne Webanwendungen und Single Page Applications mit JavaScript" behandelt er die vielen Seiten des populären JavaScript-Frameworks aus der Feder von Google. Web: www.softwarearchitekt.at
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: