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:

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

Newsletter Anmeldung
Diese Artikel könnten Sie interessieren
Angular
SL-rund

View Transition API Integration in Angular—a brave new world (Part 1)

If you previously wanted to integrate view transitions into your Angular application, this was only possible in a very cumbersome way that needed a lot of detailed knowledge about Angular internals. Now, Angular 17 introduced a feature to integrate the View Transition API with the router. In this two-part series, we will look at how to leverage the feature for route transitions and how we could use it for single-page animations.
15.04.2024
Low-angle photography of metal structure
AI
cl-neu

AI-Funktionen zu Angular-Apps hinzufügen: lokal und offlinefähig

Künstliche Intelligenz (KI) ist spätestens seit der Veröffentlichung von ChatGPT in aller Munde. Wit WebLLM können Sie einen KI-Chatbot in Ihre eigenen Angular-Anwendungen integrieren. Wie das funktioniert und welche Vor- und Nachteile WebLLM hat, lesen Sie hier.
26.02.2024
Angular
SL-rund

Konfiguration von Lazy Loaded Angular Modulen

Die Konfigurierbarkeit unserer Angular-Module ist für den Aufbau einer wiederverwendbaren Architektur unerlässlich. Aber in der jüngsten Vergangenheit hat uns Angular seine neue modullose Zukunft präsentiert. Wie sieht das Ganze jetzt aus? Wie konfigurieren wir jetzt unsere Lazy-Komponenten? Lasst uns gemeinsam einen Blick darauf werfen.
03.08.2023
Angular
yb

Using EntityAdapter with ComponentStore: @ngrx/entity Series – Part 3

As someone who enjoys the ComponentStore on an average level, I have written simple reactive CRUD logic several times. While storing a vast number of entities in the component state might not be a frequent use case, I will briefly illustrate the usage of the EntityAdapter with the @ngrx/component-store.
14.02.2023
Angular
yb

Multiple Entity Collections in the Same Feature State: @ngrx/entity-Series – Part 2

After introducing the @ngrx/entity package, I am often asked how to manage multiple entity types in the same feature state. While I hope that the previous part of this article series has made this more apparent, I will further focus on this question in the following.
07.02.2023
Angular
yb

Managing Your Collections With the EntityAdapter: @ngrx/entity-Series – Part 1

This three-part series of blogposts is targeted at developers who have already gained experience with NgRx but still manage their collections themselves. In the first part I introduce the Entity Adapter, in the second part I show you how to connect it to NgRx and in the third part how to do it with the Component Store as well.
31.01.2023