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.

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.

“Mocked API”

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.

“App - Webpack Bundle Analyzer”

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).

“Reporting - Webpack Bundle Analyzer”

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.

Don't miss out on news about Angular & more

Subscribe to our free monthly newsletter for our experts' latest technical articles about Angular, .NET, Blazor, Azure, and Kubernetes.

Please enter a valid email address.

Related Articles

angular
Nachladen von Angular-Modulen: Lazy Modules und Routen - Teil 2
Diese Artikelserie beschäftigt sich mit dem dynamischen Nachladen von Angular-Modulen. Im zweiten Teil der Serie erfahren Sie wie die initiale Lade- und Start-Performance durch das Nachladen mit Angular Router optimiert werden kann. Die im Artikel referenzierte Demo-Anwendung…
Konstantin Denerz
angular
Nachladen von Angular-Modulen: Einführung & Use Cases - Teil 1
Eine hohe Performance und die Sicherheit von Webapplikationen ist für jeden Entwickler ein Dauerthema. Unter JavaScript ist es möglich, für eine hohe Performance nur die gerade benötigten oder wegen der Sicherheit nur die erlaubten Teile der Applikation zu laden. Diese…
Konstantin Denerz
blazor
Re-Using Angular components in a Blazor WebAssembly application using Angular Elements - Web Components custom elements, FTW!
Microsoft's Blazor WebAssembly toolkit is one of the new kids on the block in WebAssembly land. For many developers WebAssembly (or WASM) is the origin for a new revolution in the web. We can finally(?) use other languages and frameworks than JavaScript to run applications in a…
Christian Weyer
angular
Eine Angular-Anwendung offline nehmen: Herausforderung von "online" zu "offline"
"Bitte prüfen Sie, ob eine Verbindung zum Internet vorhanden ist" - ein Satz, den wir alle kennen, wenn wir eine Anwendung nutzen wollen, die nicht offline verfügbar ist. In diesem Artikel zeigen wir, wie man eine bestehende (Angular-) Anwendung und ihre Daten offline nehmen kann…
Thomas Hilzendegen