Nachladen von Angular-Modulen: Eigene Lösung mit Web Components Custom Elements – Teil 3

Diese Artikelserie beschäftigt sich mit dem dynamischen Nachladen von Angular-Modulen. Der letzte Artikel der Serie beschreibt einen Weg des Nachladens mit der Berücksichtigung eines Berechtigungskonzeptes. Die Kommunikation zwischen den dynamischen Modulen und der Hauptanwendung ist nicht mehr Teil dieses Artikels. Aus Gründen der Vereinfachung werde ich in den Codebeispielen eine geteilte Referenz auf ein State-Objekt verwenden. Die im Artikel referenzierte Demo-Anwendung finden Sie hier.

In diesem Artikel:

Nachladen von Angular-Modulen: Eigene Lösung mit Web Components Custom Elements – Teil 3
Konstantin Denerz ist Software Architekt und Consultant bei Thinktecture mit Fokus auf Angular und der Verbesserung von User Experience.

Artikelserie

  1. Einleitung
  2. Lazy Modules und Routen
  3. Eigene Lösung mit Web Components Custom Elements ⬅

Idee des Nachladens mit Berechtigungskonzept

Ein nicht unerheblicher Teil von vielen Angular-Anwendungen enthält Funktionalität, die dem potentiellen Nutzer nur in Abhängigkeit seiner Benutzerrechte zur Verfügung gestellt werden soll. Während die Anwendung die gesamte Feature-Bandbreite enthält, soll nun erst zur Laufzeit geprüft werden, welche Berechtigungen der jeweilige Nutzer hat. Diese entscheiden dann über den abrufbaren Funktionsumfang.

Im vorliegenden Beispiel soll das Reporting Feature nur in Abhängigkeit von den Benutzerrechten angezeigt werden. Das im ersten Anwendungsszenario gezeigte Verhalten der Angular-CLI, die einen ladefähigen Chunk erstellt, der in Folge von der Route aktiviert wird, muss nun teilweise selbst implementiert werden. Hierzu wird das Reporting-Modul als Web Component Bundle verpackt und kann dann dynamisch geladen und aktiviert werden.

Für das vorliegende Beispiel gehen wir davon aus, dass eine gemockte API die Information liefert, dass das Reporting Modul vorhanden ist (siehe oben).

Was sind Web Components?

Web Components sind Zusammenstellungen von Technologien, die die Erstellung wiederverwendbarer und individueller (HTML) Elemente (Custom Elements) ermöglichen. Web Components können so abgegrenzte Funktionalität bündeln.

Vorgehensweise

Ein Custom Element kann erzeugt werden, wenn es zuvor mit Hilfe der Custom Elements Registry API beim Browser registriert wurde. Diese API stellt eine define() Methode bereit, die zwei Parameter erwartet: den Selektor eines Elementes (hier foo-bar) und eine Klasse, die das Element repräsentiert (hier FoobarElement).

				
					class FoobarElement extends HTMLElement {
	constructor(){
		super();
		const div = document.createElement('div');
		div.innerText = '42';
		this.attachShadow({mode: 'open'}).appendChild(div);
	}
}
customElements.define('foo-bar', FoobarElement)
				
			

Bei der Implementierung ist zu beachten, dass ein Selektor mindestens einen Bindestrich (Minus) als Trennzeichen enthalten muss: eine valide Namensgebung wäre foo-bar, wohingegen foobar invalid ist. Mit dem Selektor kann nun ein Element vom Typ FoobarElement erzeugt werden:

				
					const foobar = document.createElement('foo-bar');
document.body.appendChild(foobar);
				
			

Die im Angular Framework (via @angular/elements) bereitgestellte Funktion createCustomElement erwartet neben dem Selektor eine Component (hier foobarComponent) und eine Referenz auf den Injector der App (hier injector). Dieser zweite Parameter ist ein Konfigurationsobjekt, das eine Referenz auf den Injector der App (Web Component Bundle) hält. Die createCustomElement-Funktion erzeugt so einen dynamischen Wrapper um die Angular Component. Dieser Wrapper kann später vom Browser als Factory für unsere Component verwenden werden.

				
					const factory = createCustomElement(FoobarComponent, { injector })
customElements.define('foo-bar', factory);
				
			

Nachladen und Aktivierung der Angular-Module

Die nachfolgende Animation zeigt den Ablauf des Nachladens und der Aktivierung des Reporting-Moduls im Zusammenspiel mit der gemockten Server API.

Nachladen

				
					import {Component, ElementRef, HostListener, OnDetsroy, OnInit, ViewContainerRef} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {combineLatest, Subscription} from 'rxjs';
import {Context} from '../../services/context';
import {ModuleService} from '../../services/module.service';

@Component({
  selector: 'labs-dynamic-layout',
  template: '',
  styleUrls: ['./dynamic-layout.component.scss'],
})
export class DynamicLayoutComponent implements OnInit, OnDestroy {
  private subscription = Subscription.EMPTY;

  constructor(
    readonly moduleService: ModuleService,
    readonly elementRef: ElementRef,
    readonly activatedRoute: ActivatedRoute,
    readonly context: Context,
    readonly viewContainerRef: ViewContainerRef,
  ) {
  }

  public ngOnInit(): void {
    this.subscription = combineLatest(this.moduleService.modules, this.activatedRoute.params).subscribe(([modules, { id }]) => {
      this.viewContainerRef.clear();
      this.moduleService.load(modules.find(current => current.id === id), this.elementRef.nativeElement);
    });
  }

  @HostListener('document:activate-module', ['$event.detail'])
  public bootstrap(module: { init: (parentContext: any) => void }) {
    module.init(this.context);
  }

  public ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}
				
			

Die Route für die Funktionalität Reporting bekommt module ID als Parameter mit. Navigiert der Nutzer auf diese Route, wird eine DynamicLayoutComponent aktiviert.

				
					// app-routing.module.ts
{ path: 'module/:id', component: DynamicLayoutComponent},
				
			

In ngOnInit findet eine Subscription auf modules und die aktuelle module ID als Parameter der URL statt. Wurde das Module über den ModuleService geladen und die aktuelle Module ID breitgestellt, so kann

  • der aktuelle ViewContainer mit viewContainerRef.clear() bereinigt werden.
  • Sowie ein Element mit dem labs-reporting Selektor in der load()-Methode erstellt werden.
  • Und zuletzt, mittels der load()-Methode, die JavaScript-Dateien eines Modules über HTTP geladen werden. Dazu werden script-Elemente für die Bundle Files erstellt (vgl. unten). Danach führt sich das geladene Bundle selbst aus und initialisiert das ReportingModule.
				
					// module-service.ts
public load({ files, url, selector }: Module, element: HTMLElement): void {
	const entryComponent = document.createElement(selector);
	element.appendChild(entryComponent);

files.forEach(file => {
	const fileUrl = `http://localhost:4201${url}/${file}`; 
	const script = document.createElement('script');
	script.type = 'module';
	script.src = fileUrl;
	element.appendChild(script);
  },
);
}
				
			

Aktivierung mit Custom Events

Das Reporting-Modul wirft über die activate-Methode ein Custom Event mit dem Namen activate-module:

				
					// reporting.module.ts
ngDoBootstrap() {
	ModuleService.activate(components, this.injector, Context);
}
				
			

Die DynamicLayoutComponent lauscht auf das Aktivierungs-Event und startet die Modulinitialisierung mit dem geteilten State als Parameter:

				
					// dynamic-layout.component.ts
@HostListener('document:activate-module', ['$event.detail'])
public bootstrap(module: { init: (parentContext: any) => void }) {
module.init(this.context);
}
				
			

Nun kommt die oben angesprochene define()-Methode der Custom Elements Registry API wieder ins Spiel. Die Modulinitialisierung registriert die ReportingComponent in der CustomElementRegistry. Dazu wird der mitgegebene labs-reporting-Selektor verwendet:

				
					// module.service.ts
static activate(components: { [selector: string]: Type<any> }, parentInjector: Injector, contextType: Type<any>): void {
document.dispatchEvent(new CustomEvent('activate-module', {
    detail: {
      init: (context) => {
        const injector = Injector.create({ providers: [{ provide: contextType, useFactory: () => context }], parent: parentInjector });
        Object.keys(components).forEach(selector => {
          customElements.define(selector, createCustomElement(components[selector], { injector }));
        });
      },
    },
  }),
);
}
				
			

Da vor der Initialisierung der ReportingComponent bereits ein Element mit dem Selektor ‚labs-reporting‘ erstellt wurde, wird nun ReportingComponent direkt zum Leben erweckt – das Reporting-Modul läuft.

Größenvergleich

Die Nutzung von Web Component Bundles bietet die Möglichkeit, Funktionalität in die Anwendung einzubringen, deren Feature Bundle zunächst nicht bekannt sein muß. Die jeweiligen Modul-Files werden über den Module-Service durch den Server bereitgestellt. Dadurch kann hier ein separater Build verwendet werden. Das Web Component Bundle ist jedoch größer, da es eine Angular Runtime enthält.

Zum Vergleich eine Gegenüberstellung der App samt dem Lazy-Modul für Settings und dem Reporting-Modul als Web Component Bundle:

Die App mit dem Lazy-Modul für Settings ist in Summe nur ~356 KB (gzipped 105 KB) gross. Davon belegt der Settings-Modul-Chunk nur ~2 KB.

Im Gegensatz dazu ist das Reporting-Modul, das als Web Component Bundle implementiert wurde, allein ~165 KB (gzipped 53 KB) gross – die Gesamtgröße erreicht knappe 521 KB (gzipped 158 KB).

Resümee

Zusammenfassend lassen sich folgende Vor- und Nachteile der beiden Wege zum dynamischen Nachladen von Angular-Modulen zur Laufzeiten aufzeigen:

Lazy Modules (Router)
➕ Lassen sich einfach definieren und verwenden
➕ Sind – was die Dateigröße angeht – sehr kompakt
➕ Injector und somit State lässt sich einfach teilen
➖ Host Anwendung muss die Module zur Buildzeit der Anwendung kennen
➖ Lazy Module (Chunk) muss auf dem gleichen Server liegen

Web Component Bundles
➕ Anwendung muss die Feature Bundles nicht kennen
➕ Bundle kann von einem anderen Server geladen werden
➕ Separater Build
➖ Bundle enthält Angular Runtime und ist dadurch größer

Hierdurch ergibt sich schon ein kleiner Leitfaden für die eigenen Projekte:

Lazy Modules (Router) dürften in Projekten ausreichen, in denen eine kompakte Anwendung von einem Team entwickelt wird. Entwickeln mehrere Teams in einem Projekt verschiedene große Module eventuell auch mit unterschiedlichen Frameworks wie Angular, React oder Vue, die nur leichtgewichtig miteinander gekoppelt sind, so wären die Web Component Bundles ein möglicher Weg zur Optimierung.

Kostenloser
Newsletter

Aktuelle Artikel, Screencasts, Webinare und Interviews unserer Experten für Sie

Verpassen Sie keine Inhalte zu Angular, .NET Core, Blazor, Azure und Kubernetes und melden Sie sich zu unserem kostenlosen monatlichen Dev-Newsletter an.

Diese Artikel könnten Sie interessieren
Angular
Configuring Lazy Loaded Angular Modules

Configuring Lazy Loaded Angular Modules

Making our Angular modules configurable is an important step in building a reusable architecture. Having used Angular for a while you might be familiar with the commonly used forRoot() and forChild() functions, that some modules provide you with. But what is the best way to provide configuration in these cases?
16.06.2022
Angular
Master Web Component Forms Integration – with Lit and Angular

Master Web Component Forms Integration – with Lit and Angular

When a company has cross-framework teams, it is a good choice to use Web Components to build a unified and framework-independent component library. However, some pitfalls are to consider when integrating these components into web forms. Therefore, for a better understanding, we will look at two possible approaches and try to integrate them into an Angular form as an example.

Notice: All code samples are available on Github!
09.06.2022
.NET
Asynchrone Operationen: Blazor WebAssembly für Angular-Entwickler – Teil 5 [Screencast]

Asynchrone Operationen: Blazor WebAssembly für Angular-Entwickler – Teil 5 [Screencast]

Eine Webanwendung will natürlich auch mit Daten gefüttert werden. Doch diese müssen irgendwo her kommen. Nichts liegt näher als diese von einer Web API zu laden. Dieser Screencast zeigt, wie asynchrone Operationen in Blazor funktionieren und welche gravierenden Unterschiede es zu Angular gibt.
26.05.2022
.NET
Typings: Blazor WebAssembly für Angular-Entwickler – Teil 4 [Screencast]

Typings: Blazor WebAssembly für Angular-Entwickler – Teil 4 [Screencast]

C# und TypeScript entstammen der Feder der selben Person. Doch sind sie deshalb auch gleich? In diesem Teil der Screencast-Serie erfahren Sie, wie mit Typen in den beiden Programmiersprachen verfahren wird und welche Unterschiede es gibt.
19.05.2022
.NET
Bindings: Blazor WebAssembly für Angular-Entwickler – Teil 3 [Screencast]

Bindings: Blazor WebAssembly für Angular-Entwickler – Teil 3 [Screencast]

Wer Komponenten einsetzt, steht früher oder später vor der Fragestellung, wie man Daten an die Komponente übergibt oder auf Ereignisse einer Komponente reagiert. In diesem Screencast wird gezeigt wie Bindings bei Komponenten funktionieren, also wie eine Komponente Daten von außerhalb benutzen und Rückmeldung bei Aktionen geben kann.
12.05.2022
.NET
Komponenten: Blazor WebAssembly für Angular-Entwickler – Teil 2 [Screencast]

Komponenten: Blazor WebAssembly für Angular-Entwickler – Teil 2 [Screencast]

Bei der Entwicklung einer Webapplikation kommt es ständig vor, dass UI-Teile immer und immer wieder verwendet werden. Damit nicht immer Copy & Paste verwendet werden muss, können diese Teile in Komponenten zusammengefasst werden.
05.05.2022