Kolumne: Die Angular-Abenteuer

Flexible Clientarchitekturen mit Web Components und Micro Apps: Angular, Vue.js und Co. friedlich vereint

Manfred Steyer

Wir haben mittlerweile Milchprodukte, die länger halten als so manches JavaScript-Framework. Diese Aussage ist natürlich nicht ganz ernst gemeint, aber wie so oft bewahrheitet sich, dass in jedem Scherz auch ein Körnchen Wahrheit steckt. Es ist nämlich tatsächlich so, dass die Release- und Innovationszyklen im Web-Umfeld so kurz wie noch nie sind.

Auf der einen Seite ist das natürlich schön, weil es zeigt, dass wir ständig dazulernen. Teams, die große Unternehmensanwendungen bauen, stellt das jedoch zunehmend vor Herausforderungen. Solche Anwendungen müssen zehn bis zwanzig Jahre lang erweitert und gewartet werden. Gleichzeitig möchte man gerade in dieser schnelllebigen Welt nicht zwei Dekaden lang mit den Architektur- und Technologieentscheidungen von heute leben müssen – selbst wenn sie noch so sorgfältig getroffen werden.

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.

Micro Apps als Lösung?

Wenn man sich in so einer Sackgasse wiederfindet, kann es auch gut sein, dass man das falsche Ziel vor Augen hat. Vielleicht ist es gar nicht das Ziel, riesengroße Unternehmensanwendungen zu entwickeln. Vielleicht sollte man stattdessen mehrere kleine Anwendungen schaffen. Das erlaubt das Skalieren auf mehrere Teams, die eine jeweils passende Architektur und einen zeitgemäßen Technologiestack einsetzen.

Genaue diese Idee verfolgen Microservice-basierte Architekturen. Überträgt man dies auf die UI-Schicht, spricht man von Micro Frontends oder Micro Apps. Wie schon in einer der letzten Ausgaben berichtet, bieten sich Web Components zur Realisierung dieses Ansatzes an. Diese frameworkunabhängigen Komponenten lassen sich sehr einfach dynamisch in eine Shellanwendung laden. Außerdem basieren sie auf aktuellen Web-Standards, die sich dank Polyfills auch in Evergreen-Browsern wie IE 11 nutzen lassen (Kasten: „Polyfills“).

Polyfills

Damit Web Components auch in Evergreenbrowsern wie IE 11 laufen, ist ein Polyfill einzubinden. Die hier gezeigte Lösung nutzt das Paket @webcomponents/custom-elements, das sich per npm installieren lässt. Es beinhaltet die folgende Datei, welche die benötigte Browserunterstützung nachrüstet: @webcomponents/custom-elements/custom-elements.min.Alternativ dazu findet sich in diesem Paket auch ein Loader, der gezielt nur jene Polyfills nachlädt, die für den jeweiligen Browser benötigt werden.

Da Custom Elements für ECMAScript 2015+ definiert sind, heutzutage jedoch in der Regel noch das alte ECMAScript 5 als Kompilierungsziel dient, benötigen auch Browser, die Custom Elements unterstützen, ein Polyfill. Ein solches befindet sich im genannten Paket in der Datei @webcomponents/custom-elements/src/native-shim.js.

Die gezeigte Lösung bindet das eine Polyfill über die Datei angular.json ein und das andere über die Datei polyfills.ts. Dadurch wird gewährleistet, dass beide Polyfills in unterschiedlichen Bundles landen. Das ist notwendig, da je nach Browser eines der beiden fehlschlagen kann und das die weitere Ausführung des jeweiligen Bundles verhindert.

In dieser Ausgabe greife ich diese Idee erneut auf und zeige, wie sich damit unterschiedliche Technologien mixen lassen. Das macht man natürlich nicht aus Jux und Tollerei, sondern weil sich über mehrere Jahre hinweg Änderungen ergeben oder weil es für unterschiedliche Anwendungsfälle jeweils unterschiedliche Technologien zu benutzen gilt.

Dazu nutze ich eine Rahmenanwendung, die einzelne Micro Apps lädt. Diese Micro Apps basieren auf Web Components und verwenden weitere Web Components, die mit unterschiedlichen Technologien geschrieben wurden.

Angular Abenteuer: Abb. 1: Shellanwendung, die Web Components lädt

Abb. 1: Shellanwendung, die Web Components lädt

Wie die Icons in Abbildung 1 vermuten lassen, kombiniere ich auf diese Weise Angular mit Vue.js und Vanilla JS, also Komponenten, die ohne Frameworkunterstützung geschrieben wurden. Zugegeben, der Mix ist hier auf einer Seite ein wenig heftig. Generell wird man eine so starke Verwebung einzelner Micro Apps verhindern wollen: Das schafft eine höhere Kopplung und schränkt die Flexibilität ein. Da es hier jedoch in erster Linie um die Veranschaulichung der Möglichkeiten und der dazu nötigen Vorgehensweise geht, möge man mir das verzeihen.

Das gesamte Beispiel findet man in meinem GitHub-Repo.

International JavaScript Conference
Hans-Christian Otto

Testing React Applications

by Hans-Christian Otto (Suora GmbH)

Sebastian Witalec

Building a Robo-Army with Angular

by Sebastian Witalec (Progress)

API Summit 2018
Christian Schwendtner

GraphQL – A query language for your API

mit Christian Schwendtner (PROGRAMMIERFABRIK)

Web Components mit Vanilla JS

Jenseits von Frameworks ergibt es Sinn, sich mit der Entwicklung von Web Components mit Vanilla JS, also blankem ECMAScript, zu beschäftigen. Wer die dazu nötigen Browser-APIs kennt, kann auch Frameworks wrappen, die noch keine native Unterstützung für Web Components mit sich bringen. Um ohne Umwege eine Web Component zu bauen, leitet man zunächst von HTMLElement ab (Listing 1). Das zeigt schon, dass Web Components für ECMAScript 2015 und jünger definiert sind.

class MilesCard extends HTMLElement {

  get miles() {
    return parseInt(this.getAttribute('miles'));
  }

  set miles(value) {
    this.setAttribute('miles', '' + value);
  }

  static get observedAttributes() {
    return ['miles'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    this.miles = newValue;
    this.render();
  }

  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.render();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <div style="border: darkkhaki 2px dashed; padding: 20px">
        <p>
          <img src="assets/js.png" height="50">
        </p>
        <b>Miles: ${this.miles}</b>
      </div>
    `;
  }

}

Die einzelnen Eigenschaften kann die Anwendung über JavaScript setzen. Frameworks vereinfachen das häufig mittels Datenbindung. Zu unterscheiden sind solche Eigenschaften auf Objektebene von Attributen, die beim Aufruf der Komponente in der HTML-Datei gesetzt werden. Während erstere auch Objekte oder Arrays aufnehmen können, beschränken sich Attribute auf primitive Werte, also auf Strings, Zahlen und Booleans. Um die Quelle für Verwirrungen gering zu halten, ist es üblich, Eigenschaften mit primitiven Werten mit gleichnamigen Attributen zu synchronisieren. Darum kümmern sich hier zum Beispiel die Getter und Setter der Eigenschaft miles.

Die Komponente kann sich auch informieren lassen, wenn sich ein Attribut ändert. Aus performancegründen hat sie dazu zunächst den Getter observedAttributes zu implementieren. Dieser liefert die Namen der zu überwachenden Attribute zurück. Ändert sich in weiterer Folge eines dieser Attribute, ruft der Browser die Methode attributeChangedCallback auf. Die hier betrachtete einfache Implementierung aktualisiert daraufhin die gleichnamige Eigenschaft und rendert die Komponente neu.

Um das initiale Rendering kümmert sich der Konstruktor. Zuvor erzeugt er jedoch einen sogenannten Shadow Root. Dieser stellt sicher, dass der Browser das Layout der Komponente losgelöst vom Rest der Seite darstellt. Styles, die also außerhalb der Komponente importiert werden, wirken sich nicht auf die Komponente aus. Ausnahmen wie die Hintergrundfarbe oder die Schriftart bestätigen die Regel. Die so geschaffene Isolation verhindert, dass sich externe CSS-Angaben mit komponenteninternen vermengen und sich so ein ungewünschtes Layout ergibt. Der Standard hinter diesem Konzept nennt sich Shadow DOM.

Um die Komponente zu nutzen, ist sie noch beim Browser zu registrieren. Dabei bekommt sie einen ElementNamen zugewiesen:

customElements.define('miles-card', MilesCard);

Per Definition muss der vergebene Name einen Bindestrich beinhalten. Somit lassen sich eigene Elemente von Standardelementen unterscheiden. Um die Komponente aufzurufen, ist lediglich ein entsprechendes Element in die Seite aufzunehmen:

<miles-card miles="35700"></miles-card>

Web Component mit Vue.js

Vue.js erlaubt es seit einiger Zeit, herkömmliche Komponenten als Web Components zu exportieren. Das CLI automatisiert das sogar, sodass Entwickler keine einzige zusätzliche Codezeile schreiben müssen. Das Ergebnis ist ein Bundle, das alle Komponenten des Projektes oder wahlweise auch nur eine bestimmte als Web Component enthält. Dazu erstellt die CLI lediglich Code, der dem in Listing 1 ähnlich ist und die Komponenten wrappt.

Allerdings schränkt die Nutzung dieses einfachen Automatismus auch ziemlich ein: Beispielsweise kommt immer Shadow DOM zum Einsatz und es ergeben sich Probleme mit älteren Browsern. Daneben lassen sich Plug-ins wie der Router auch nur begrenzt nutzen.

Lesen Sie auch: Vue.js Tutorial – Einführung in das JavaScript-Framework

Um diesen Einschränkungen zu entfliehen, lässt sich zur inoffiziellen Alternative greifen, oder die Vue.js-Anwendung manuell wrappen. Ich habe mich für letztere Variante, die etwas mehr Arbeit aber dafür auch mehr Freiheiten mit sich bringt, entschieden (Listing 2).

export default class FlightBasket extends HTMLElement {

  get flights() {
    return this._flights;
  }

  set flights(value) {
    this._flights = value;
    this.vue.$data.products = this.flights;
  }

  static get observedAttributes() {
    return ['flights'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    this.flights = JSON.parse(newValue);
  }

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.render();
  }

  [...]
}

Zunächst einmal gleicht der Code, hier jenem aus Listing 1. Allerdings findet kein Abgleich zwischen der Eigenschaft flight und einem eventuellen gleichnamigen Attribut statt. Es handelt sich nämlich um ein Array von Objekten und sowas lässt sich nicht ohne weiters in einer HTML-Datei mit einem Attribut darstellen. Das bedeutet aber auch, dass diese Eigenschaft entweder direkt mit JavaScript oder mit dem Datenbindungsmechanismus eines Frameworks zu setzen ist. Spannend wird es bei der render-Methode, die die Verbindung zu Vue.js aufbaut (Listing 3).

render() {

  const cssBase = require('!to-string-loader!css-loader!../assets/css/bootstrap.min.css');
  const cssTheme = require('!to-string-loader!css-loader!../assets/css/paper-dashboard.css')

  this.shadowRoot.innerHTML = `
    <style>${cssBase}</style>
    <style>${cssTheme}</style>

    <div id="component"></div>
  `;

  const comp = this.shadowRoot.getElementById('component');

  this.vue = new Vue({
    data: {
      products: []
    },
    render(r) {
      return r(Basket, { props: { products: this.products } });
    }
  })

  this.vue.$mount(comp);

}

Die Funktion require lädt die im Shadow DOM der Web Component zu nutzenden CSS-Layouts. Dies veranlasst das zugrundeliegende Build-Werkzeug webpack, die Dateien auch ins Bundle aufzunehmen. Damit webpack den Brückenschlag zwischen den CSS-Dateien und dem JavaScript-Bundle schaffen kann, stützt es sich auf den css-loader und den to-string-loader. Sie lassen sich per npm install zum Projekt hinzufügen und werden beim Aufruf von require neben dem Dateinamen angegeben.

Der Inhalt der Komponente, besteht lediglich aus Style-Elementen mit den CSS-Layouts und einem Platzhalterelement. Die Methode $mount des Vue-Objekts platziert darin die gewrappte Komponente.

Außerdem definiert das Vue-Objekt ein Array products, das es an die Komponente bindet. Das bedeutet, dass sich bei jeder Änderung dieses Arrays die Komponente selbstständig aktualisiert. Somit schlägt sich eine Aktualisierung durch den Setter flights (Listing 2) sofort auf die Darstellung durch.

Etwas gewöhnungsbedürftig ist die Implementierung der Render-Funktion des Vue-Objekts. Ihr Inhalt entspricht in etwa der Funktionsweise des folgenden Vue.js-Templates:

<basket :flights="flights"></basket>

Es gilt demnach, die Basket-Komponente zu erzeugen und ihre Eigenschaft flights an die gleichnamige Variable aus dem data-Objekt zu binden. Man könnte anstatt der render-Funktion sogar dieses Template nutzen, aber dann ist auch der Vue.js-Compiler ins Bundle aufzunehmen. Allerdings ist das nicht erstrebenswert, weil es das Bundle unnötig aufbläht und zur Steigerung der Performance die Kompilierung bereits im Build erfolgen sollte. Deswegen kommt hier die etwas unhandliche render-Funktion zum Einsatz. Die Templates der einzelnen Vue-Komponenten kompiliert die CLI ohne weiteres Zutun.

customElements.define('flight-basket', FlightBasketCE);

Web Components mit Angular Elements

Dank Angular Elements, das sich im Paket @angular/elements befindet, können Angular-Komponenten sehr einfach als Web Components veröffentlicht werden. Dazu ist lediglich die gewünschte Komponente in die entryComponents eines Modules aufzunehmen und mit der Methode createCustomElement in eine Web Component zu überführen (Listing 4).

@NgModule({
  [...],
  declarations: [
    AppComponent,
    [...]
  ],
  bootstrap: [],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  entryComponents: [
    AppComponent,
    [...]
  ]
})
export class AppModule { 

  constructor(private injector: Injector) {
  }

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

  }
}

Möchte man im Modul externe Web Components nutzen, ist auch noch das Schema CUSTOM_ELEMENTS_SCHEMA aufzunehmen. Das verhindert, dass der Angular-Compiler, der die externen Komponenten nicht kennt, einen Fehler auslöst, wenn er auf deren Aufrufe stößt.

Lesen Sie auch: Microservices ins Frontend hineintragen? Micro Apps mit Angular Elements & Web Components

Leider unterstützt die CLI in der aktuell vorliegenden Version 6.x nicht das Erzeugen eines eigenständigen Bundles für Web Components. Abhilfe schafft hier das Community-Projekt ngx-build-plus, das ein Plug-in für die CLI beinhaltet.

Laden von Web Components

Liegen die Web Components erst mal in Form von Bundles vor, sind sie noch in die Shell zu laden. Dazu reichen einfache DOM-Manipulationen, die beispielsweise ein Script-Element zum Einbinden des Bundles oder ein Element zum Aufrufen der Komponenten zur Seite hinzufügen:

const content = document.getElementById('content');
const script = document.createElement('script');
script.src = configItem.path;
content.appendChild(script);

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

Neben dem Betrieb innerhalb der Shell, lassen sich Micro Apps, die auf Web Components basieren, auch direkt laden (Abb. 2).

Abb. 2: Micro Apps laufen auch eigenständig

Das ist auch notwendig, denn schließlich möchte man ja eine Entkoppelung vom Gesamtsystem erreichen. Diese ermöglicht zum Beispiel, separat zu entwickeln, zu testen und bereitzustellen.

Fazit

Web Components sind eine moderne Lösung zur Schaffung von Micro Apps und zum Teilen von UI-Fragmenten. Sie lassen sich bei Bedarf in den Browser laden und erlauben die Kombination unterschiedlicher Technologien. Diese Option ist besonders bei Geschäftsanwendungen, die über eine lange Zeit hinweg entwickelt und gepflegt werden, interessant, zumal sich am Markt im Laufe mehrerer Jahre einiges ändern kann.

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: