Blog mit neuen Fachartikeln, Interviews und Event-News https://javascript-days.de/blog/ JavaScript & Angular Days Mon, 23 Feb 2026 13:02:37 +0000 en-US hourly 1 https://wordpress.org/?v=6.7.2 Angular 21: Die Evolution zum AI-First-Framework https://javascript-days.de/blog/angular/angular-21-zoneless-change-detection-ai-integration/ Mon, 23 Feb 2026 12:28:50 +0000 https://javascript-days.de/?p=107958 Mit Angular 21 vollzieht das Framework einen strategischen Wandel zum AI-First-Framework. Die Einführung von Zoneless Change Detection als Standard, die Ablösung von Zone.js, der Einsatz von Vitest im Testing sowie experimentelle Signal Forms markieren eine neue Phase in der Angular-Architektur.

The post Angular 21: Die Evolution zum AI-First-Framework appeared first on JavaScript & Angular Days.

]]>
Mit Angular 21 verschiebt sich der Schwerpunkt des Frameworks sichtbar – architektonisch, organisatorisch und mit Blick auf den Einfluss KI-gestützter Entwicklung.

Karsten Sitterberg, einer der ausgewiesenen Angular-Experten im deutschsprachigen Raum, hat die Neuerungen detailliert analysiert. Sein Fachartikel liefert den technischen Deep Dive, inklusive Migrationspfaden, API-Details und praktischer Beispiele:

 

Zoneless als neuer Standard

Mit Angular 21 wird „Zoneless Change Detection” zum Default für neue Anwendungen. Damit verabschiedet sich Angular schrittweise von Zone.js – jener Bibliothek, die bislang im Hintergrund Browser-APIs patchte, um Change Detection automatisch auszulösen.

Der Schritt ist technisch konsequent. Explizite Steuerung ersetzt implizite „Magie”. Anwendungen werden transparenter, Debugging nachvollziehbarer, Performance kalkulierbarer. Gleichzeitig entsteht Reibung dort, wo Drittbibliotheken noch auf Zone.js setzen. Wer größere Ökosysteme betreibt, wird diese Übergangsphase aktiv gestalten müssen.

NEU! Generative AI für Angular-Entwickler:innen

Erlebe das neue Bootcamp vom 19. – 20. März 2026

Testing: Vitest statt Karma

Auch im Testing vollzieht Angular einen deutlichen Schnitt. Vitest wird für neue Projekte zum Standard. Das passt zur esbuild-basierten Toolchain und reduziert Ausführungszeiten spürbar.

Für bestehende Projekte bleibt die Migration optional und vorerst experimentell. Dennoch ist die Richtung klar: Angular modernisiert seine Infrastruktur beherzt, statt historische Entscheidungen dauerhaft mitzuschleppen. Die Details zu Syntax-Anpassungen und Migration beschreibt Sitterberg in seinem Beitrag.

 

Signal Forms: Architektur in Bewegung

Signals prägen Angular bereits seit einigen Versionen. Mit den experimentellen Signal Forms wird dieses Architekturprinzip nun auf ein traditionell komplexes Feld übertragen: Formulare.

Ziel ist eine klarere, typsichere und deklarative Modellierung, die perspektivisch die Trennung zwischen template-driven und reactive forms überwindet. Noch trägt die API den Status „experimentell”, doch die Stoßrichtung ist eindeutig. Wer Angular strategisch einsetzt, sollte diese Entwicklung aufmerksam verfolgen.

 

AI-Integration: Infrastruktur statt Schlagwort

Angular 21 versteht AI nicht als Marketing-Add-on, sondern als Werkzeug in der Entwicklungsumgebung.

Dazu gehört die Integration eines MCP-Servers in die CLI, der es ermöglicht, Angular-Befehle aus AI-gestützten Editoren heraus auszuführen. Hinzu kommen LLM-optimierte Dokumentationsdateien sowie ein Codegen-Scorer, der automatisch generierten Code bewertet.

Das sind keine visionären Ankündigungen, sondern konkrete Werkzeuge. Wer mit AI-gestütztem Coding arbeitet, erhält damit eine strukturierte Anbindung an bestehende Entwicklungsprozesse.

Router und weitere Anpassungen

Der Router kehrt zu einem konsistent asynchronen Verhalten auf Basis von async/await zurück. Das vereinfacht Server-Side-Rendering und verändert insbesondere das Verhalten in Tests.

Daneben bringt Angular 21 zahlreiche Detailverbesserungen: automatische Bereitstellung des HttpClient, präzisere Typisierung bei Lifecycle-Hooks, Fortschritte im Accessibility-Bereich sowie aktualisierte CLI-Migrationen. Gleichzeitig hebt das Framework die Mindestanforderung auf TypeScript 5.9 an und entfernt weitere Altlasten.

Angular entwickelt sich weiter – nicht spektakulär, sondern strukturell. Und vor allem passt es sich modernen Anforderungen in Sachen AI an. Die Entscheidung für Zoneless, die konsequente Signal-Architektur und die Integration von KI-Werkzeugen zeigen ein Framework, das seine Grundlagen überprüft und anpasst.

Sitterbergs vollständiger Artikel liefert die technische Substanz hinter diesen Entwicklungen. Er spiegelt damit direkt die technische Substanz der vielen Workshops auf den JavaScript Days wider. Wer darüber hinaus diskutieren, ausprobieren und vor allem vertiefen möchte, findet bei den JavaScript Days den passenden Rahmen. Dort treffen sich Expert:innen, die Framework-Entwicklungen nicht nur verfolgen, sondern einordnen – und Angular 21 ist nur eines der Themen, die dort mit fachlicher Tiefe und praktischer Konsequenz behandelt werden.

 

Links und Quellen

Sitterberg, Karsten: AI-ngular: Im Zeichen der AI mit Angular 21. Verfügbar unter: https://app.entwickler.de/Qarkq8q1rYb

 

🔍 Frequently Asked Questions (FAQ)

1. Was ist Angular 21?

Angular 21 ist die neueste Version des Angular-Frameworks und markiert den strategischen Wandel hin zu einem AI-First-Framework. Mit Zoneless Change Detection, moderner Toolchain und integrierter AI-Unterstützung in der CLI modernisiert Angular seine Architektur grundlegend.

2. Was bedeutet „Zoneless Change Detection“ in Angular 21?

Zoneless Change Detection ersetzt die bisherige Abhängigkeit von Zone.js. Statt automatischer, impliziter Aktualisierung setzt Angular 21 auf explizite Steuerung. Das macht Anwendungen transparenter, verbessert Debugging-Prozesse und sorgt für kalkulierbarere Performance – besonders in komplexen Enterprise-Projekten.

3. Was ändert sich beim Testing mit Angular 21?

Angular 21 setzt bei neuen Projekten auf Vitest statt Karma. Die esbuild-basierte Toolchain sorgt für schnellere Testläufe und eine modernere Infrastruktur. Bestehende Projekte können optional migrieren, profitieren aber langfristig von einer performanteren Testing-Umgebung.

4. Was sind Signal Forms in Angular 21?

Signal Forms sind ein experimentelles API-Konzept, das auf Angular Signals basiert. Ziel ist eine typsichere, deklarative und klar strukturierte Formulararchitektur. Langfristig könnten Signal Forms die Trennung zwischen template-driven und reactive forms auflösen.

5. Wie integriert Angular 21 künstliche Intelligenz (AI)?

Angular 21 integriert AI direkt in die Entwicklungsumgebung. Über einen MCP-Server in der Angular CLI können Befehle aus AI-gestützten Editoren ausgeführt werden. Zusätzlich gibt es LLM-optimierte Dokumentation und einen Codegen-Scorer zur Bewertung von KI-generiertem Code.

6. Wer sollte Angular 21 einsetzen?

Angular 21 richtet sich an Entwickler:innen und Unternehmen, die moderne Webanwendungen mit klarer Architektur, hoher Performance und AI-gestützten Workflows entwickeln möchten. Besonders für größere Projekte mit langfristiger Wartung und Skalierung bietet das Update strategische Vorteile.

7. Was bedeutet das Angular-21-Update für bestehende Projekte?

Bestehende Projekte können schrittweise migrieren. Während Zoneless Change Detection für neue Anwendungen Standard ist, bleibt die Nutzung von Zone.js zunächst möglich. Auch der Umstieg auf Vitest ist optional. Gleichzeitig erfordert Angular 21 mindestens TypeScript 5.9 und bringt mehrere Infrastruktur-Anpassungen mit sich.

The post Angular 21: Die Evolution zum AI-First-Framework appeared first on JavaScript & Angular Days.

]]>
Web-Crawling mit JavaScript leicht gemacht https://javascript-days.de/blog/javascript-web-scraping-mit-llms/ Fri, 13 Feb 2026 20:43:15 +0000 https://javascript-days.de/?p=107935 Web-Crawling mit JavaScript ist eine zentrale Technik, um Inhalte aus Webseiten automatisiert auszulesen. In diesem Artikel zeigen wir, wie sich dynamische Webseiten mit Node.js, Puppeteer und LangChain verarbeiten lassen, um Inhalte gezielt an Large Language Models (LLMs) zu übergeben. Schritt für Schritt entsteht ein JavaScript-basiertes CLI-Tool, das Web Scraping und KI-Analyse elegant miteinander verbindet.

The post Web-Crawling mit JavaScript leicht gemacht appeared first on JavaScript & Angular Days.

]]>
KI löst heutzutage ja allerhand Probleme für uns. Also dachte ich mir, es könne ja nicht so schwer sein, einer LLM Fragen zu einer Webseite zu stellen und eine passende Antwort zu erhalten. Meine konkrete Fragestellung lautete: „Welche Workshops werden auf den JavaScript Days in Berlin angeboten und welche Speaker halten die Workshops?“ Die Aufgabe lässt sich auf verschiedene Arten lösen.

Auf den ersten Blick scheint eine der populären, gehosteten GUIs mit einem extrem leistungsfähigen LLM im Hintergrund die Lösung für das Problem zu sein. Beispiele hierfür sind ChatGPT, Gemini oder Claude. Ein Vorteil dieser Plattformen ist, dass die Modelle standardmäßig auf das Internet zugreifen können und somit selbstständig recherchieren können. Doch das ernüchternde Ergebnis liegt schnell vor. Weder ChatGPT noch Claude oder Gemini beantworten die Frage korrekt. Vielmehr ist das Ergebnis entweder nur ein Bruchteil (bei ChatGPT) oder schlichtweg falsch (bei Gemini), wobei Gemini ungefragt ein fotorealistisches Bild der Konferenzatmosphäre generiert.

Möglicherweise können die Modelle die Informationen der Webseite nicht richtig abrufen. Dieses Problem ließe sich jedoch lösen, indem man dem Modell die entsprechende Seite zur Verfügung stellt. Doch wenn der Code der Seite einfach in das Eingabefeld des Prompts kopiert wird, quittieren die Web-UIs der Modelle das mit einer Fehlermeldung, da die Eingabe zu lang ist. Also bleibt nur, den HTML-Code in Form einer Datei hochzuladen. Dann werden die Ergebnisse schon deutlich besser.

Von Nachteil ist dabei nur, dass wir den Quellcode der Seite jedes Mal lokal zwischenspeichern und dem Modell zur Verfügung stellen müssten. Mit ein wenig Code lässt sich dieser Prozess jedoch deutlich vereinfachen.

 

 

 

Die lokale Lösung

Die Basis unserer eleganteren Lösung bildet Node.js. Als Schnittstelle soll die Kommandozeile des Systems dienen. Die Applikation benötigt zwei Informationen: die Webseite, für die wir uns interessieren, und unseren Prompt, also die Fragestellung. Die Applikation besteht aus drei Teilen: einer CLI-Schnittstelle für die Ein- und Ausgabe, einer Funktion zum Herunterladen des Webseiteninhalts und einer weiteren Funktion zum Senden der Informationen an ein LLM.

Eine solche Applikation kann auf verschiedene Weise aufgebaut werden. Die einfachste Methode besteht darin, die drei Kernelemente jeweils in einer Funktion zu kapseln und die Funktionen nacheinander aufzurufen. Eine etwas elegantere Variante ist der Einsatz von LangChain als LLM-Framework. LangChain wurde genau für diese Art von Applikation entwickelt. Die einzelnen Teile bilden dabei eine Kette, an deren Ende die Antwort des LLMs steht.

Als Erstes wird die Applikation mit npm init -y initialisiert und in der erzeugten package.json-Datei der type auf module umgestellt, um das ECMAScript-Modulsystem zu aktivieren. Anschließend können die erforderlichen LangChain-Module initialisiert werden:

npm install @langchain/core @langchain/ollama

Die Eingabe

Für die Interaktion auf der Kommandozeile stellt Node.js das Readline-Modul zur Verfügung. Die createInterface-Funktion erzeugt eine neue Schnittstelle für zwei Streams. Normalerweise sind das die Standardeingabe und die Standardausgabe. Über dieses Schnittstellenobjekt können wir dann mit der question-Methode Eingaben auf der Kommandozeile abfragen. Node.js bietet für dieses Modul mittlerweile auch eine Promise-basierte Variante, die in Kombination mit async/await zu gut lesbarem und kompaktem Code führt. Die beiden Eingaben kapseln wir in ein RunnableLambda, eine Abstraktion von LangChain für Funktionen, die es ermöglicht, diese Funktionen in eine LangChain-Kette zu integrieren.

Der Rückgabewert der Funktion ist ein Objekt, das den eingegebenen URL und die Frage zur Webseite, also den Prompt, enthält. Dieses Objekt stellt LangChain dem nächsten Element in der Kette zur Verfügung. Testen können wir diese Funktion, indem wir die invoke-Methode des Objekts aufrufen. In Listing 1 ist das mit dem Code des ersten Kettenglieds zu sehen.

Listing 1: Eingaben über die Kommandozeile

import readline from 'node:readline/promises';
import { RunnableLambda } from '@langchain/core/runnables';

export const cliInput = new RunnableLambda({
  async func() {
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout,
    });

    const url = await rl.question('Gib die URL ein: ');
    const question = await rl.question('Welche Frage möchtest du stellen? ');
    rl.close();

    return { url, question };
  },
});

// test:
// const result = await cliInput.invoke();
// console.log(result);

Download der Webseite

Für den Download einer einfachen Webseite kann die fetch-Funktion von Node.js verwendet werden. Alternativ gibt es Pakete wie crawler, die über den einfachen Download von HTML-Dokumenten hinaus noch zusätzliche Komfortfeatures bieten, beispielsweise die Verarbeitung mehrerer Seiten oder die Unterstützung von Rate Limits, Timeouts und Retries. Für unser Beispiel, die Programmseite der JavaScript Days, hilft uns jedoch weder die fetch-Funktion noch das crawler-Paket, da die Webseite die wichtigsten Inhaltselemente asynchron mit JavaScript nachlädt. Das Problem der meisten Web-Crawler ist, dass sie kein JavaScript unterstützen und nur die initiale HTML-Struktur zur Verfügung stellen. Diese hilft jedoch nicht bei der Beantwortung unserer Frage. Hier müssen wir also etwas tiefer in die Trickkiste greifen.

Eine mögliche Lösung heißt Puppeteer. Im Kern ist dies ein Headless-Browser, also ein Browser ohne grafische Oberfläche. Er ist in der Lage, JavaScript auszuführen, sodass auch dynamische Inhalte konsumiert werden können.

Puppeteer wird mit dem Kommando npm install puppeteer installiert. Da mit dem Paket auch ein vollwertiger Chrome-Browser heruntergeladen wird, kann der Installationsprozess je nach System und Internetanbindung etwas länger dauern.

Die Verarbeitung von Webseiten mit Puppeteer erfolgt in mehreren Schritten: Zunächst starten wir den Browser im Headless-Modus mit der launch-Methode von Puppeteer. Anschließend erzeugen wir mit der newPage-Methode ein Objekt, das eine Seite repräsentiert, und wechseln mit der goto-Methode zum gewünschten URL. Im letzten Schritt erhalten wir den HTML-Code der Seite mit der evaluate-Methode des Page-Objekts.

Der Workflow mit Puppeteer wird für die Ausführung in der LangChain-Kette in ein RunnableLambda gekapselt. Der Rückgabewert enthält den Inhalt der Seite sowie die ursprünglich übergebene Frage, die hier nur durchgereicht wird. Den Code können wir wiederum mit einem Aufruf der invoke-Methode des RunnableLambda-Objekts testen (Listing 2).

Listing 2: Download einer dynamischen Webseite mit Puppeteer

import { RunnableLambda } from '@langchain/core/runnables';
import puppeteer from 'puppeteer';

export const fetchPage = new RunnableLambda({
  async func({ url, question }) {
    const browser = await puppeteer.launch({ headless: true });
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: 'networkidle2' });

    const html = await page.evaluate(() => document.body.innerText);
    await browser.close();
    return { content: html, question };
  },
});

// test:
// const result = await fetchPage.invoke({
//   url: 'https://de.wikipedia.org/wiki/Kuh',
//   question: 'Nenne mir eine interessante Tatsache über Kühe.',
// });
// console.log(result);

Setze JavaScript Trends um

16. - 20. März 2026

>

 

Die einzelnen Elemente zusammenfügen

Im letzten Schritt fehlt nun noch die Interaktion mit dem Modell sowie das Zusammenfügen der einzelnen Teile zu einer vollwertigen Kette. Diese besteht aus mehreren Elementen:

  • Konsoleneingabe: Am Anfang der Interaktion steht die Konsoleneingabe, die wir in der cliInput-Funktion gekapselt haben. Als erstes Element in der Kette benötigt diese Funktion keine Argumente und liefert als Ausgaben den URL und die ursprüngliche Frage des Users.

  • Download der Webseite: Der URL aus der Eingabe ist der wichtigste Input für den Download mit Puppeteer. Die entsprechende Logik haben wir in der fetchPage-Funktion gekapselt und in eine eigene Datei ausgelagert. Die Userfrage wird an das nächste Element der Kette weitergereicht.

  • Prompt-Template: Mit dem Prompt-Template können wir die Informationen, die wir an das Modell übergeben, strukturieren. Das verbessert die Ergebnisse deutlich. Neben dem verwendeten Modell ist dieses Template eine der wichtigsten Stellschrauben. Experimentieren Sie mit der Formulierung und der Struktur, um die Ergebnisse zu beeinflussen.

  • Modellinteraktion: Das Herzstück der Applikation ist die Kommunikation mit dem Modell. Die Wahl des passenden Modells ist entscheidend für die Qualität des Ergebnisses. Im Beispiel in Listing 3 kommt mit Llama 3.2:1b ein vergleichsweise kleines Modell zum Einsatz. Der Vorteil ist, dass dieses Modell auf einem einigermaßen gut ausgestatteten Rechner problemlos ausgeführt werden kann. Das Ergebnis ist jedoch in der Regel nicht optimal. Wenn wir ein größeres Modell wie das Granite4-Small-Modell mit seinen 32 Milliarden Parametern nutzen, werden die Ergebnisse deutlich besser. Eine weitere Optimierung besteht in der Verwendung eines kommerziellen Modells, wie beispielsweise GPT-5 von OpenAI. Um dieses zu nutzen, ersetzen Sie das Paket @langchain/ollama durch @langchain/openai, wählen das entsprechende Modell aus und hinterlegen Ihren API-Key. Dabei ist jedoch zu beachten, dass durch die Kommunikation Kosten auf Basis der Input- und Output-Tokens entstehen. Zu den Inputtokens zählt in diesem Fall der gesamte Quellcode, den unser Skript im Zuge der Kette heruntergeladen hat. Zwar sprechen wir hier zunächst über verschwindend geringe Centbeträge. Wird das Skript jedoch sehr häufig ausgeführt, können durchaus nennenswerte Kosten entstehen.

  • Parsen der Antwort: Der StringOutputParser von LangChain stellt sicher, dass die Antwort des Modells ohne zusätzliche Metainformationen zur Verfügung steht, sodass sie direkt ausgegeben werden kann.

Listing 3 zeigt den Quellcode der Applikation.

Listing 3: Integration der einzelnen Bestandteile der LangChain-Kette

import { RunnableSequence, RunnableLambda } from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts';
import { Ollama } from '@langchain/ollama';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { fetchPage } from './fetchPage.js';
import { cliInput } from './cliInput.js';

const prompt = new PromptTemplate({
  template: `
You are a helpful assistant.
Here is the content of a web page:
---
{content}
---

Answer this question based on the page content:
{question}
  `,
  inputVariables: ['content', 'question'],
});

const model = new Ollama({
  model: 'llama3.2:1b',
});
const parser = new StringOutputParser();

const chain = RunnableSequence.from([
  cliInput,
  fetchPage,
  prompt,
  model,
  parser,
]);

const answer = await chain.invoke();
console.log(answer);

Die Applikation können wir nun mit Node.js auf der Kommandozeile ausführen und erhalten dann die Antworten auf unsere Frage zu einer Webseite.

Bleib informiert

Brancheninsights im Newsletter erhalten:

[mc4wp-simple-turnstile]

 

Und was lernen wir daraus?

In diesem Beispiel stellen wir eine konkrete Frage zu einer Webseite und servieren dem LLM die Antwort praktisch auf einem Silbertablett. Doch es bereitet gerade kleineren Modellen erhebliche Schwierigkeiten, die gewünschte Information zu extrahieren und zu präsentieren.

Allerdings gibt es in diesem Prozess eine Reihe von Stellschrauben, an denen wir drehen können. Zunächst wäre da das Prompt-Template. Je nachdem, wie es formuliert ist, kann es das Ergebnis erheblich beeinflussen. Auch die verwendete Sprache kann Bedeutung haben. Die meisten Modelle arbeiten beispielsweise mit Englisch deutlich besser als mit Deutsch. Zudem können wir, wie schon erwähnt, das Modell gegen ein leistungsfähigeres ersetzen und damit die Ergebnisqualität nochmals verbessern.

Aktuell müssen wir bei sehr spezifischen Problemstellungen also noch immer kreativ sein, um eine gute Lösung zu finden. Die technischen Möglichkeiten entwickeln sich in diesem Bereich jedoch rasant weiter.

 

🔍 Frequently Asked Questions (FAQ)

1. Was ist JavaScript Web-Crawling in diesem Artikel?

JavaScript Web-Crawling beschreibt das automatische Laden und Auslesen von Webseiten mit Node.js. Im Artikel liegt der Fokus auf dynamischen Webseiten, die Inhalte per JavaScript nachladen, um diese Inhalte anschließend an Large Language Models (LLMs) weiterzugeben.

2. Welches Problem löst diese JavaScript-Web-Crawling-Lösung?

LLMs können Webseiteninhalte oft nicht zuverlässig selbst abrufen oder verstehen. Durch das lokale Herunterladen und Vorverarbeiten der Inhalte kann das Modell die relevanten Informationen direkt erhalten, was präzisere und vollständigere Antworten ermöglicht.

3. Welche Technologien werden in diesem Beispiel verwendet?

Verwendet werden Node.js als Laufzeitumgebung, Puppeteer als Headless-Browser für die Ausführung von JavaScript auf Webseiten und LangChain, um die Interaktion zwischen Eingaben, Webseiteninhalt und LLM zu strukturieren.

4. Was kann Puppeteer, was einfache Web-Crawler nicht können?

Puppeteer führt JavaScript in einem Headless-Browser aus. Dadurch können dynamische Inhalte, die erst clientseitig geladen werden, abgerufen werden – etwas, das klassische Web-Crawler, die nur statisches HTML herunterladen, nicht leisten können.

5. Wozu wird LangChain in dieser Anwendung eingesetzt?

LangChain verbindet mehrere Schritte – CLI-Eingabe, Seiten-Download, Prompt-Erstellung, Modellinteraktion und Ausgabe – zu einer strukturierten Kette. Das macht die Anwendung modular, gut lesbar und leicht erweiterbar.

6. Wer kann von diesem JavaScript-Web-Crawling profitieren?

Die Methode ist besonders nützlich für JavaScript-Entwickler, Data Engineers und alle, die Webseiteninhalte mit KI analysieren oder abfragen möchten. Besonders relevant ist sie bei dynamischen Webseiten und automatisierten LLM-Prozessen.

7. Wozu kann diese Web-Crawling-Lösung eingesetzt werden?

Die Lösung dient dazu, konkrete Fragen zu Webseiten zu beantworten, strukturierte Informationen zu extrahieren, Forschungsaufgaben zu automatisieren oder Webseiteninhalte für KI-Analysen mit LLMs vorzubereiten.

The post Web-Crawling mit JavaScript leicht gemacht appeared first on JavaScript & Angular Days.

]]>
Mit Angular Signals das Web-Frontend verändern https://javascript-days.de/blog/angular/signals-vs-rxjs-angular-updates/ Tue, 02 Dec 2025 13:31:07 +0000 https://javascript-days.de/?p=107868 Angular Signals bringen erstmals tief integrierte Reaktivität direkt ins Framework und verändern damit nachhaltig, wie moderne Web-Frontends entwickelt werden. Mit writable(), computed(), effect() und den neuen Resource-APIs entsteht ein performantes, leichtgewichtiges Reaktivitätsmodell, das Change Detection (Synchronization), Input-Bindings und State Management deutlich vereinfacht. Gleichzeitig bleibt Angular dank RxJS-Interop, Abwärtskompatibilität und Code-Generatoren gut migrierbar – ein zentraler Vorteil für Enterprise-Projekte.

The post Mit Angular Signals das Web-Frontend verändern appeared first on JavaScript & Angular Days.

]]>
Die Angular APIs bringen die lang ersehnte Reaktivität inklusive Input-Bindings direkt ins Framework. Dadurch verändern sich viele Bereiche der Enterprise Web Frontend Plattform. Die Synchronisation zwischen JavaScript Zustand und gerendertem Browser-Inhalt wird performanter und das Framework im gesamten einfacher zu erlernen und auch leichtgewichtiger in der Verarbeitung. Dieser neue Stil verlangt allerdings weitgehendst manuelle Refactorings, die in großen Codebases auch entsprechend eingeplant werden müssen. Das Angular Team stellt allerdings auch Abwärtskompatibilität bereit und liefert unterstützende Code-Generatoren.

 

Die wichtigsten Angular Signal APIs sind nun für den produktiven Einsatz freigegeben (stable) und daher sollte jedes Entwicklerteam spätestens ab jetzt neue Features mit Signals ausstatten. Das Angular-Team hat in einem Evaluierungsprozess mehrere Konzepte bewertet – auch RxJS und JavaScript Proxies wurden für deren Eignung als neues „Reactive Primitive“ des Frameworks analysiert – und das Signals Konzept hat die größte Übereinstimmung mit deren Zielsetzungen gezeigt. Was können Signals also so besonders gut, sodass sie sich sogar gegen RxJS durchsetzen konnten, das seit Angular Version 2 als Drittanbieter Bibliothek verpflichtend in jedem Angular Projekt mitinstalliert wird? Signals sind mit Sicherheit nicht das Universalwerkzeug wie RxJS samt all seiner Operatoren, aber sie glänzen in Teilbereichen, die speziell für den Synchronisationsprozess zwischen Browser-DOM und JavaScript-Zustand verantwortlich sind. Zudem ist das (reaktive) Abrufen von Werten im Vergleich zu RxJS deutlich komfortabler und vor allem wird der Wert immer synchron – also sofort – zurückgegeben. Kein subscribe/unsubscribe, kein async/await – einfach ein Funktionsaufruf und der im Signal verwaltete Wert steht schon typkonform bereit. Ein wichtiges Kriterium ist auch, dass man das Framework einfacher machen will. Einfacher nicht im Sinne von weniger bereitgestellten Features, aber hinsichtlich der Erlernbarkeit für neue Developer, die vor der Entscheidung stehen, auf welches Frontend-Framework sie sich in Ihrer künftigen Karriere fokussieren wollen. Dies ist für jeden und jede von uns wichtig – denn nur wenn ausreichend ausgebildete Angular Entwickler am Markt verfügbar sind, werden sich Unternehmen weiterhin für diese Plattform entscheiden. Bliebe Angular für alle Ewigkeit bei den bestehenden APIs, während andere Frameworks mutig neue Konzepte implementieren und etablieren, würde sich jeder Entscheidungsträger irgendwann die Frage stellen, ob Angular noch immer die beste Wahl für die eigenen Projekte ist. Das Angular-Team geht eben diesen mutigen Weg, allerdings auch einen, der es ermöglicht, all diese Innovation möglichst sanft in die eigene Codebase einbauen zu können und uns damit vor vergleichsweise sehr wenige Breaking Changes stellt. Für jeden Breaking Change wird – wenn technisch möglich – ein Angular CLI Schematics Generator angeboten, der die notwendigen Refactorings entweder vollautomatisch umsetzen kann oder bei der Migration zumindest Unterstützung anbietet. Im Fall von Signals gibt es mehrere unterstützende Generatoren, die beim manuellen Refactoring helfen [1], [2], [3]. Ein alternatives Beispiel für Update Schematics, die fast zu 100 Prozent automatisch umbauen können, sind jene für den neuen Control Flow (@if, @for, @switch) [4].

 

„Zahlreiche Vertreter der Angular Community wünschen sich schon sehr lange eine tiefere reaktive Framework-Unterstützung.“

 

Warum Signals?

Die neue reaktive Reise hat mit Angular 16 begonnen und ist nun, mit Version 21, schon sehr weit fortgeschritten. Signals sind allerdings kein Konzept das vom Angular Team erfunden wurde, sondern sie wurden vom „CEO of Signals“, dem Entwickler von SolidJS, Ryan Carniato, 2016 in die moderne Single-Page-Application-Welt gebracht [5]. Die früheren Wurzeln gehen zu Xerox und Libraries wie Knockout zurück. Heute verwenden zahlreiche Frontend Bibliotheken und Frameworks, wie z. B. Vue, Preact und Qwik Signal Konzepte. Mittlerweile ist auch ein offizielles TC39 Proposal [6] veröffentlicht worden, mit dem das Ziel verfolgt wird, Signals zu einem EcmaScript Sprachstandard zu machen. Das Angular Team war hier beteiligt und hat auch die Referenzimplementierung bereitgestellt [7]. 

Zahlreiche Vertreter der Angular Community wünschen sich schon sehr lange eine tiefere reaktive Framework-Unterstützung. Ein Teil dieser Initiative war die Etablierung des NgRx-Namespaces, der genau dieses Ziel verfolgt und anfangs vor allem für seine Event-basierte, reaktive State Management Implementierung nach dem Redux-Pattern bekannt wurde. NgRx und auch andere Drittanbieter Bibliotheken stoßen allerdings spätestens dann an Ihre Grenzen, wenn es um tiefere Framework-Integration wie Change Detection (wird mittlerweile „Synchronization“ genannt) oder Data Bindings geht. Letzteres erfordert auch Compilerintegration, damit die Rendering Engine Ivy korrekt angebunden werden kann. Aus diesen Gründen war es notwendig, dass das Angular Team selbst die relevanten reaktiven APIs direkt in den Framework Code einbindet.

Aus dem internen Evaluierungsprozess ist das Signals Konzept als Sieger hervor gegangen, weil es die gesetzten Ziele am besten umsetzen konnte:

  • Werte werden per Funktionsaufruf sofort – also synchron – zurückgegeben
  • Signals können in bestimmen Fällen abhängigen Quellcode benachrichtigen und als Konsequenz kann dieser erneut ausgeführt werden
    • um abgeleiteten Zustand zu ermitteln
    • Side-Effects auszulösen
    • das UI zu aktualisieren
  • Werteupdates, die zum selben Resultat führen, lösen keine Benachrichtigung aus
    • bei Objekten wird die Referenz verglichen
    • Immutable Updates sind hier also wichtig: wird eine Property eines Objekts verändert, muss auch eine neue Objektreferenz erzeugt werden

 

Als die ersten Signal APIs mit Version 16 veröffentlicht wurden, waren auch noch mutable Updates für schreibbare Signals erlaubt. Dies wurde mit Version 17 – sinnvollerweise – korrigiert. Einen Wert zunächst durch Objektmutation zu verändern hat einen Preis, den man später durch einen aufwendigen Vergleich, potenziell auf mehreren verschachtelten Objektebenen, hinsichtlich beeinträchtigter Runtime-Perfomance bezahlen muss. Zudem hat die Version 17 grundlegende APIs wie signal() und computed() sowie andere relevante Signal APIs und Typen aus dem Core Paket als stabil definiert. Andere wichtige Features wie effect() und toSignal(), toObservable() aus dem RxJS-Interop API wurden jedoch im Developer Preview Status belassen, da die interne Einplanung der Effects mehrmals angepasst werden musste. Auch diese APIs wurden nun mit Version 20 in den stabilen Zustand befördert. Dazwischen wurden mit Feature-Releases der Version 17 – endlich – Signal-basierte, reaktive Bindings Realität: input() für readonly Property-Bindings und model() für schreibbares Two-Way-Binding. Ergänzend wurde mit output() auch noch ein dekoratorloses Event-Binding API veröffentlicht, das aber keine Signals für die eventbasierte Kommunikation verwendet, sondern weiterhin einen EventEmitter. Alle diese APIs wurden mit Version 19 stabil. Dieses Major Release brachte uns auch noch das linkedSignal() und die Familie der Resource APIs, um Signals als Trigger für asynchrone Prozesse zu verwenden, die nicht zwingend mit RxJS implementiert sein müssen, aber erneut damit kompatibel sind. Das linkedSignal() wurde bereits mit Version 20 stabil, während die Resource APIs weiterhin experimentell sind. All diese Features werden im Rahmen dieses Artikels näher betrachtet.

 

„Das Signal kann einfach per Funktionsaufruf gelesen werden – ohne AsyncPipe.“

Erlebe Angular Trends live

16. - 20. März 2026

>

 

Grundlegende APIs

Starten wir mit jenem Signal API, mit dem man üblicherweise zuerst in Kontakt kommt – das Writable Signal, das mit der Funktion signal() erstellt wird. Diese und viele andere Features können direkt aus dem @angular/core Paket importiert werden. Das Angular Team hat sich – im Gegensatz zu RxJS – diesmal dafür entschieden, keine Drittanbieter Library zu verwenden, sondern den Code selbst zu implementieren, damit die rückwärtskompatible Unterstützung mit der „alten Welt“, vor der Signals-Ära, bestmöglich gewährleistet werden kann. signal() erzeugt ein Objekt, das aber gleichzeitig auch eine Funktion ist (ja, auch das kann JavaScript). Der Funktionsaufruf retourniert den aktuell verwalteten Wert sofort. Über die Methoden set() und update() kann der Wert verändert werden, was wie erwähnt, immer immutable erfolgen muss. set() überschreibt den vorhanden Wert mit einem neu Übergebenen, während update() eine Lambda-Expression entgegennimmt, die auf den aktuellen Wert zugreifen kann und deren Return-Value als neuer Signal-Wert übernommen wird. signal() wird initial ein Wert übergeben, der per Type-Inference festlegt, welchen Typ das Signal künftig verwaltet. Wird kein Wert übergeben und stattdessen der Typ über die Spitzen Klammern angeführt, ist auch undefined Teil der erlaubten Typsignatur:

 

const firstname = signal<string>(); // WritableSignal<string | undefined>

 

Der Wert des Signals kann einfach per Funktionsaufruf, sowohl im TypeScript-Code als auch im Template – und zwar ganz ohne AsyncPipe – gelesen werden:

 

<p>Firstname: {{ firstname() }}</p>

 

Mit computed() wird abgeleiteter Zustand als Signal (readonly) definiert. Dieser Signaltyp bietet keine set() oder update() Methode an, kann aber stattdessen Signals lesen und wird dann zuverlässig benachrichtigt, wenn sich die abhängigen Signals verändern. computed() wird eine Lambda-Expression übergeben, die einen Return-Value zurückgibt, der den abgeleiteten Zustand ermittelt. Allerdings sind Computed Signals lazy – werden Sie von niemandem in der Codebase gelesen, läuft die Lambda-Expression auch niemals. Auch ein indirekter Leseprozess über ein oder mehrere weitere abgeleitete Signals ist aber ausreichend, solange am Ende der Kette ein Binding im Template erfolgt oder ein effect() direkt oder indirekt (z. B. toObservable(), resource(), rxMethod() [8]) zur Anwendung kommt.

Side-Effects können über effect() definiert werden. Dies erfolgt, wie bei computed, über eine übergebene Lambda-Expression, aber mit dem Unterschied, dass hier kein Wert zurückgegeben wird. Es werden aber auch hier Signals gelesen und der Effect wird für eine neue – immer asynchrone – Ausführung eingeplant, wenn zumindest eines der gelesenen Signals eine Wertänderung aussendet.

 

„Warum verwendet man nicht immer model()?“

 

Kommunikation zwischen Components

In modernen Angular Frontends werden üblicherweise zwei Komponentenkategorien verwendet. Smart Components sind Container, die in der Routing Konfiguration verwendet werden und Feature-spezifische Services wie z. B. Data Access oder State Management Services injecten. Die zweite Kategorie sind UI (oder Presentational bzw. Dump) Components, die sich um das Detail-Rendering kümmern, oft wiederverwendbar sind und ihren Zustand lediglich über Data Bindings erhalten. Eben dieses Data Binding ist nun auch mit Signals realisierbar und ermöglicht das lang ersehnte Feature der reaktiven Inputs. Diese UI Components werden von einer übergeordneten Smart Component in deren Template verwendet und dadurch am Bildschrim angeordnet. Das Schöne an der Signal Integration für Bindings ist, dass das Angular Team auch hier wieder die Kompatibilität mit bisherigen Implementierungen klar im Fokus hat. Dadurch ist es möglich, dass eine Smart Component refactort wird, ohne die UI Component verändern zu müssen oder umgekehrt. Die gebundenen Daten kommen zwar als Signal in der Kind-Komponente an, müssen aber nicht zwingend im Template der Parent Component als Signal gebunden werden. Die Smart Component kann einen ganz normalen Wert binden und in der UI Component wird der Wert über ein Input Signal bereitgestellt. Dies kann mit der Funktion input() erreicht werden, die ein nur lesbares Signal erzeugt. Änderungen können in diesem Fall also nur per Binding durchgeführt werden und nicht innerhalb der Komponente, die den Input definiert, selbst. Dieser Einschränkung muss beim Refactoring beachtet werden und macht gegebenenfalls eine Änderung der Implementierung notwendig. 

 

readonly firstname = input.required<string>();

 

Der Beispielcode zeigt einen Signal-basierten Input vom Typ string, der in diesem Fall als required definiert ist und daher von der Parent Component zwingend gesetzt werden muss. Erfolgt das nicht, zeigt die IDE eine rote Fehler-Unterwellung im Template und auch der Compiler wirft den Fehler aus. Inputs können mittlerweile auch durch den Router gesetzt werden, sofern dieser entsprechend konfiguriert ist:

 

provideRouter([{ path: 'edit/:id', component: EditComponent }], withComponentInputBinding())

 

Damit versucht der Router nun, seinen Zustand an gleichnamige Inputs der referenzierten Komponente zu binden. Dies betrifft dynamische Pfad-, Query- und Matrix-Parameter genauso wie Zustand aus dem statischen data-Objekt der Route oder dynamischen Daten, die per Router Resolver abgeholt werden [9].

Im nachfolgenden Beispiel wird der Input mit einem Initialwert definiert, der im Laufe des Lebenszyklus der Component aktualisiert werden kann. Im gegebenen Fall wird er Zustand des dynamischen Routen Parameter id an den gleichnamigen Component Input gebunden. Da der Parameter in diesem Fall aus der URL ermittelt wird, liegt er als String vor und kann über eine simple Transform-Funktion, die das Framework bereitstellt, in einen Number-Wert umgewandelt werden:

 

readonly id = input(0, { transform: numberAttribute });

 

Ein required Input darf keinen Initialwert definieren, da vorausgesetzt wird, dass die Parent Component diesen setzen muss. Trotzdem ist es wichtig zu beachten, dass der initial gebundene Wert zum Zeitpunkt der Komponenteninstanzierung – also bei Property-Zuweisung oder im Konstruktor – noch nicht bereitsteht. Will man diesen lesen, benötigt man, wie bisher, eine ngOnInit-Implementierung. Alternativ kann eine effect()-Definition im Konstruktor verwendet werden, der den Input liest. Für den Effect wird garantiert, dass dessen erstmalige Ausführung erst dann erfolgt, wenn der Input gesetzt wurde.

 

constructor() { effect(() => console.log(this.firstname()); }

 

Ein weiteres, neues Konzept wird bereitgestellt, um Bindungs zu definierten – erstmals kann ein Two-Binding mit einem einzigen Funktionsaufruf definiert werden. model() liefert ein schreibbares Signal inkl. set() und update() Methoden. Obwohl weiterhin intern ein EventEmitter verwendet wird, um die Parent Component zu benachrichtigten – dieses Konzept bleibt gleich – wird keine emit()-Methode angeboten. Stattdessen wird intern automatisch emit() aufgerufen, sobald per set() oder update() ein neuer Wert gesetzt wird.

 

readonly selected = model(false);

 

Um auch für den bisherigen @Output() ein dekoratorloses API anzubieten, gibt es nun die Funktion output<string>(). Diese retourniert einen OutputEmitterRef inkl. emit()-Methode – wie bisher.

 

protected readonly firstnameChange = output<string>();

 

Die Frage ist nun – warum verwendet man nicht immer model() anstatt einer Kombination von input() und output()? Möchte man wirklich jede Komponenten-interne Änderung sofort an die Elternkomponente weitergeben, dann ist die Verwendung von model() ideal und einfacher als bisher. Wäre das Model Signal aber z. B. mit einem Input Control in UI Component verbunden, will man wahrscheinlich nicht jedes neu eingegebene Zeichen an die Parent Component senden. Diese könnte gegebenenfalls bei jedem Event-Trigger einen http-Call senden. Stattdessen macht es mehr Sinn, die Elternkomponente erst dann zu informieren, wenn der Suchknopf geklickt wird. In diesem Fall kann ein manueller emit()-Aufruf über eine output()-Property eine einfach zu realisierende Performance-Optimierung sein. 

 

Signals und RxJS

Signals bieten zwar, wie RxJS, ein reaktives Konzept an, aber trotzdem gibt es einige relevante Unterschiede. Signals können RxJS auch nicht in allen Bereichen ersetzen – trotzdem strebt das Angular Team an, RxJS optional zu machen. Da sich das widersprüchlich anhört, sollten wir das genauer betrachten.

Signal stellen Zustand dar, der sich über die Zeit verändern kann. Der Zustand ist von Beginn an typkonform vorhanden. Ein RxJS Observable bildet einen Strom von Events ab. Hierbei ist nicht garantiert, dass der Stream ein initiales, synchrones Event aussendet oder überhaupt jemals ein einziges Event. Das BehaviorSubject bietet verpflichtend ein initiales, synchrones Event an und verhält sich dies bezüglich einem Signal sehr ähnlich. Auch ein startWith()-Operator kann für ein initiales, synchrones Event sorgen. In beiden Fällen können nachfolgende Operatoren, wie z. B. debounceTime() die synchrone Antwort allerdings wieder zunichtemachen. Zusammengefasst, kann RxJS sehr viel, aber es erfordert die „richtige“ Implementierung, wenn es sich wie ein Signal verhalten soll. Signals haben hingegen ein sehr striktes, klar ausgewähltes Verhalten, von dem man kaum abweichen kann. Das macht sie zwar nicht zum Generalisten, dafür glänzen sie in jener Nische, für die sie vorgesehen sind: am Übergang von unserem JavaScript-Zustand zum Rendering als Browser-DOM und für die generelle Verwaltung von Zustand auch abseits von Komponenten. Sie sind allerdings klar nicht für Event-basierte Kommunikation geeignet – mehr dazu später.

Um RxJS optional machen zu können, sind noch einige Änderungen an öffentlichen APIs notwendig, die aktuell noch auf Observables & Co basieren. Dennoch ist es bereits ersichtlich, wie dieses Ziel erreicht werden soll. Der Kern des Frameworks wird in Zukunft sowohl intern als auch hinsichtlich der öffentlichen APIs keine Abhängigkeit auf RxJS mehr haben. Trotzdem werden auch künftige Angular Versionen Kompatibilität mit RxJS gewährleisten, allerdings könnte ein ng new irgendwann die Frage stellen, ob RxJS verwendet werden soll. Vielleicht zu Beginn mit Standard-Antwort „Yes“ und später „No“. Jene Projekte, die weiterhin RxJS verwenden wollen, können dann von weiterhin gut unterstützten Secondary-Entrypoints bestimmter Framework-Pakete importieren und nur dann wird es notwendig sein, RxJS zu installieren. Im Core-Paket gibt es bereits den Secondary-Entrypoint @angular/core/rxjs-interop. Dieser bietet u. a. die Funktionen toObservable() und toSignal() an, die zwischen den beiden reaktiven APIs übersetzen können.

toObservable() wird eine Signal Referenz übergeben – das Signal wird diesmal also nicht per Funktionsaufruf gelesen. Stattdessen wird intern ein Effect verwendet, der bei Änderung des Signal-Werts ein neues Event im Observable-Stream aussendet. Da ein Effect standardmäßig an den Lebenszyklus der Component gebunden ist, erfolgt in diesem Fall ein automatischer Cleanup – der Effect wird beendet und nicht mehr für weitere Neuausführungen eingeplant. Manuelle Cleanup-Logik kann ebenso definiert werden, falls eine Registrierung unabhängig der Komponente-Lifetime verwaltet werden soll oder der Effect z. B. in einem Root-Level-Service definiert wurde. Hierbei gilt es zu beachten, dass sich alle Eigenheiten des Signals, die in weiterer Folge näher beschrieben werden, auch auf das Observable auswirken:

  • Mehrere synchron geänderte Werte werden zusammengefasst
  • Der Initialwert kommt aufgrund des Effects erst mit einer asynchronen Verzögerung im Observable-Stream an
  • Sendet das Signal keinen Update-Trigger aus, weil der Wert z. B. nicht korrekt immutable aktualisiert wurde, wird auch kein Observable-Event erzeugt

 

Der umgekehrte Anwendungsfall – also ein Observable in ein Signal umzuwandeln – kann mit toSignal() realisiert werden. Dadurch erfolgt eine sofortige interne Subscription auf den Stream und wird im Falle einer Component bis zum Destroy-Zeitpunkt aufrechterhalten. Die Entwickler:in muss also nicht manuell unsubscriben, sofern das Standardverhalten der automatischen Beendigung beim Zerstören der Komponente das Beabsichtigte ist.

Alle an das Observable gekettete Operatoren wirken sich auch auf das Setzen der Event-Werte im resultierenden Signal aus. Somit kann die volle Funktionsvielfalt von RxJS auch für Signals verwendet werden. Da der Signalwert, wie immer, sehr einfach per Funktionsaufruf gelesen werden kann, hat die Entwickler:in das Beste aus den beiden reaktiven Welten zur Hand: sehr komfortables Registrieren, um über Wertänderungen informiert zu werden (aus dem Signals API) und die Möglichkeit, maßgeschneidertes reaktives Verhalten zu definieren (über die Vielfalt der Operatoren von RxJS). Es ist klar zu empfehlen, in Komponenten verstärkt Signals zu verwenden. RxJS darf zwar weiterhin auch in Components Anwendung finden, ist aber zumeist sinnvoller in den Service-basierten Logik-Schichten hinter den Komponenten – also z. B. den Side-Effects von State Management Implementierungen – aufgehoben.

Ein Fallbeispiel mit Router Component Input Binding, Signal-based Input, RxJS Interop und Reactive Forms findet die Leser:in im Artikel-begleitenden GitHub-Repository. [10]

 

Listing 1

// passenger-edit.component.ts

private passengerService = inject(PassengerService);

id = input(0, { transform: numberAttribute });

private passenger = toSignal(

  toObservable(this.id).pipe(

    switchMap(id => this.passengerService.findById(id))

  ), { initialValue: initialPassenger }

);

 

Signals werden erwachsen

Das Angular Team hat von Beginn an klargestellt, dass es nicht das Ziel verfolgt, Utility-Funktionen, ähnlich der RxJS-Operatoren, für Signals zu veröffentlichen. Stattdessen ist die Zielsetzung, die grundlegenden reaktiven APIs als direkten und gut integrierten Bestandteil des Frameworks bereitzustellen und damit für die überwiegenden Anforderungen der Angular Community ein Tool bereitzustellen, das in den meisten Fällen zwar eine klare Meinung verfolgt, aber nicht bis ins letzte Detail (wie durch RxJS-Operatoren) angepasst werden kann. Als Konsequenz soll jedoch diese API-Familie einfacher zu erlernen sein als es RxJS bisher war. Will man solche auf Signals aufbauende Hilfsfunktionen nutzen, kann man diese entweder selbst entwickeln und über eine generische Library in den eigenen Repositories verwalten oder eine Community Bibliothek verwenden. Ein Beispiel dafür sind die ngxtension [11]. Wie immer gilt, sorgfältig zu entscheiden für welche Zwecke der Mehraufwand für die Verwaltung von Drittanbieterpaketen gerechtfertigt ist und wann man lieber – wenn das die jeweilige Lizenz erlaubt – sinnvolle, schlanke Implementierungen in die eigene Codebase übernehmen möchte. Ein Beispiel für eine hilfreiche, simple Utility-Funktion ist direkt mit dem vorhergehenden Kapitel verbunden: hat man ein Signal aus Ausgangspunkt, dessen Verhalten man mithilfe mehrerer RxJS Operatoren maßgeschneidert anpassen möchte, um dann am Ende das Resultat wieder als Signal im Template zu lesen, dann ist eine verschachtelte Verwendung von toSignal() und toObservable() sinnvoll. Dafür kann man alternativ auch eine generische Funktion schreiben [12].

 

Listing 2

// passenger-edit.component.ts

private passengerService = inject(PassengerService);

id = input(0, { transform: numberAttribute });

private passenger = signalOperators(this.id, pipe(

  switchMap(id => this.passengerService.findById(id))

), initialPassenger);

 

Das Angular Team trifft Design-Entscheidungen oft auch aus pragmatischen Gründen. Obwohl man, wie es als Nicht-Ziel sieht, für jeden denkbaren Anwendungsfall eine Signal-basierte Hilfsfunktion oder ein neues Signal API im Framework direkt anzubieten, weicht man von dieser Strategie gelegentlich ab, wenn damit ein klarer Mehrwert geschaffen werden kann. Ein Beispiel dafür ist das linkedSignal(). Zunächst wurde in einem Video-Interview [13] das Reset-Pattern vorgestellt und darauf verwiesen, dass man sich bei Bedarf vergleichbare Funktionalität auf Basis der bestehenden APIs selbst implementieren kann. Mit Version 19 wurde diese Funktionalität schlussendlich doch als Framework API veröffentlicht. Die Idee dahinter ist, dass man Signale miteinander in Verbindung setzen kann, ohne einen Effect verwenden zu müssen. Zusammengefasst, ist das linkedSignal() eine Kombination aus dem Writable- und dem Computed-Signal. Bei der Definition werden ein Source-Signal und eine Computation-Funktion angeführt. Gleichzeitig erlaubt das Linked Signal aber auch die Verwendung der Schreib-APIs set() und update(). Der hiermit gesetzt Zustand ist so lange gültig, bis das Source-Signal einen neuen Wert aussendet, damit die Computation-Funktion ausführt und so den aktuellen Signal-Wert überschreibt – deshalb der Name Reset-Pattern.

Wofür kann man so ein API in der Praxis verwenden?

Es hilft z. B. beim Refactoring von @Input() zu input(). Wie besprochen, ist der Signal-basierte Input readonly. Daher könnte man das Input Signal als Source für ein Linked Signal definieren und somit einen Komponenten-internen Zustandscontainer schaffen, dessen aktueller Wert nur per Bedarf per Output-EventEmitter zur Elternkomponente gesendet wird [14].

Ein weiter Anwendungsfall kann der Einsatz in einer Auswahlbox sein. Wird im ersten Select-Control ein Land ausgewählt, ändern sich die Stammdaten der Regionen im zweiten Feld. Bei Auswahl „Deutschland“ werden die Regionen „Bayern, Hessen, Hamburg, etc.“ verknüpft, bei „Österreich“ hingegen „Wien, Niederösterreich, Steiermark, etc.“. Bildet man die Auswahl mit linkedSignal() ab, hat der User die Möglichkeit eine konkrete Region auszuwählen, die als lokaler State im Signal-Container verwaltet wird. Ändert sich später das Land, werden die Regionen per neuem Trigger durch das verknüpfte Source-Signal aktualisiert und die Computation wird erneute angestoßen, wodurch initial z. B. die alphabethisch erstgereihte Region vorausgewählt wird. Ab nun kann durch User-Interaktion erneut eine passende Region ausgewählt werden, die ins Linked Signal geschrieben wird und so lange gültig bleibt, bis die Computation erneute angetriggert wird – eben das ist das Reset-Verhalten [15].

 

„Bei Wertänderung wird die asynchrone Loader-Funktion angetriggert und bisherige Abläufe werden abgebrochen.“

 

Asynchronität auch ohne RxJS

Noch mehr Aufsehen hat allerdings eine andere Angular 19 Neuerung ausgelöst – die Familie der Resource APIs. Damit ist es nun erstmals möglich ohne direkte Effect-Verwendung und ohne RxJS Interop APIs Signals mit asynchronen Abläufen in Verbindung zu setzen. Die hier eingesetzten Konzepte lösen ein Problem, das durch das aktiv angestrebte Signal-Verhalten geschaffen wurde: Signals, die immer einen synchron verfügbaren Zustand haben müssen, vertragen sich nicht so gut mit der Natur von asynchronen Operationen. Daher hat man nicht einfach wie bei RxJS Hilfsfunktionen geschaffen, die Signals in Promises transformieren können – diese beiden APIs haben zu wenige Gemeinsamkeiten – sondern eine komplexere Signatur entworfen, um die synchrone mit der asynchronen Welt zu verbinden. Am Ende entsteht nicht nur einzelnes Signal, sondern gleich ein Objekt, das mehrere Signals als Properties anbietet.

Alle Resource APIs sind aktuell experimentell und es ist erkennbar, dass das API zwar schon gut durchdacht, aber die Gesamtstory noch nicht fertiggestellt ist. Seit Angular 19 gibt es die Funktionen resource() und rxResource() [16]. Version 19.2 hat uns die httpResource() spendiert. Damit kann Asynchronität über Promises, über Observables oder mithilfe einer internen Abhängigkeit auf den HttpClient angetriggert werden.

resource() kommt daher ohne RxJS Dependency aus und kann direkt aus @angular/core importiert werden. Im Vergleich zu einer direkten Promise-Verwendung in einem Effect, werden hiermit einige Nachteile behoben: die Resource verhindert Race-Conditions, indem ein Signal, das die asynchrone Loader-Funktion triggert auch zum Abbruch eines ggf. noch laufenden vorhergehenden Aufrufs führt. Dies macht ein Abort-Signal als Eingehender-Parameter in die Lambda-Funktion möglich. Dabei gilt es zu beachten, dass das Abort-Signal ein (leider nicht sehr ergonomisches) Sprachkonstrukt von EcmaScript ist, um asynchrone Operationen abbrechen zu können – sie haben allerdings abseits der Namensgleichheit nichts mit dem Angular Signals API zu tun. Zusammengefasst, haben wir also ein Signal, das bei Wertänderung die Promise-basierte asynchrone Loader-Funktion antriggert und bisherige, noch andauernde Abläufe abbrechen kann (vgl. switchMap() Verhalten in RxJS). Auch ein Reload wird unterstützt, wenn man z. B. Daten vom Backend API erneut abrufen will, obwohl sich z. B. die ID als Signal-Trigger nicht verändert hat. In diesem Fall werden erneute Reload-Trigger so lange ignoriert bis der Neuladeablauf beendet ist (vgl. exhaustMap() Verhalten in RxJS). Diese beiden Verhaltensweisen passen vermutlich für die allermeisten Anwendungsfälle sehr gut. Will man allerdings ein abweichendes Verhalten definieren, das bei neuen Triggern angewendet wird, gibt es aktuell keine Möglichkeit dies zu konfigurieren und dabei wird es voraussichtlich auch bleiben. Hier bestätigt sich also der zuvor beschriebene Angular-Weg: klare Meinung, einfache Anwendbarkeit, aber nicht so viele Detailauswahlmöglichkeiten, als wir von RxJS gewohnt sind.

 

Listing 3

// passenger.service.ts

findByIdAsResource(id: Signal<number>): ResourceRef<Passenger | undefined> {

  return resource({

    params: id,

    loader: ({ params: id, abortSignal }) => fetch(

      [this.baseUrl, 'passenger', id].join('/'),

      { signal: abortSignal }

    ).then(res => res.json() as Promise<Passenger>)

  });

}

 

Braucht man also mehr Design-Freiheit, bietet es sich erneut an, RxJS miteinzubeziehen – dafür gibt es die rxResource() aus @angular/core/rxjs-interop. Hiermit kann der Loader anstelle eines Promises per Observable-Stream inkl. Folgeevents implementiert werden. Erneut triggert das Signal den Loader-Stream und verwendet hierfür das switchMap-Verhalten.

 

Listing 4

// passenger.service.ts

findAsResource(

  filter: Signal<{ firstname: string, lastname: string }>

): ResourceRef<Passenger[] | undefined> {

  return rxResource({

    params: filter,

    stream: ({ params: filter }) =>

      this.find(filter.firstname, filter.lastname)

  });

}

 

Last but not least, bietet die httpResource() aus @angular/common/http ein einfach zu verwendendes Interface an, um lesende http-Anfragen verwalten zu können. Wenngleich es derzeit noch keine Möglichkeit gibt, Daten über einen Backend-Call per PUT oder HOST verändern zu können (Projektname Mutation API), zeigt die httpResource() schon jetzt, wie eine Zukunft ohne RxJS, aber mit ergonomischen Signal APIs aussehen könnte. Im aktuellen experimentellen Status ist es zwar nicht zu empfehlen, die gesamte Codebase von der aktuellen HttpClient Verwendung auf die httpResource() umzustellen, aber eine künftige http-Implementierung könnte ohne interne RxJS-Abhängigkeit auskommen und die öffentliche Signatur der httpResource() müsste nicht zwingend verändert werden [17].

 

Listing 5

// passenger-edit.component.ts

id = input(0, { transform: numberAttribute });

protected passengerResource = httpResource<Passenger>(() => ({

  url: 'https://demo.angulararchitects.io/api/passenger',

  params: { id: this.id() }

}), { defaultValue: initialPassenger });

 

Bleib informiert

Brancheninsights im Newsletter erhalten:

[mc4wp-simple-turnstile]

Fazit

Die Signal Story ist mittlerweile schon sehr weit fortgeschritten. Das bedeutet für bestehende Codebases, das spätestens jetzt, wo die meisten APIs bereit stabil verfügbar sind, verstärkt auf Signals gesetzt werden sollte. Dies macht den eigenen, reaktiven Code leichtgewichtiger und einfacher zu lesen als dies zumeist bei RxJS der Fall war. Dennoch wird uns RxJS jedenfalls noch mittelfristig begleiten bis auch asynchrone Abarbeitungen, wie Backend-API-Kommunikation stabil per Resource samt Mutation API unterstützt werden. Bei Einsatz von maßgeschneiderter, reaktiver, service-basierter Logik, wie sie auch in Side-Effects von Enterpise State Management Patterns (Redux und Flux mit @ngrx/store oder @ngrx/signals) vorkommt, ist ebenfalls noch offen, ob es eine „bessere“ Lösung als RxJS geben wird, denn für event-basierte Datenströme sind Signals nicht geeignet. Es spricht also nichts dagegen, bestehenden RxJS-Code weiterzuverwenden bzw. auch auf Observables und Operatoren zurückzugreifen, wenn spezifisches Verhalten modelliert werden soll, das mit Signals nicht gut abbildbar ist.

 

Links & Literatur

[1] https://angular.dev/reference/migrations/signal-inputs

[2] https://angular.dev/reference/migrations/outputs

[3] https://angular.dev/reference/migrations/signal-queries

[4] https://angular.dev/reference/migrations/control-flow

[5] https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob

[6] https://github.com/tc39/proposal-signals

[7] https://github.com/proposal-signals/signal-polyfill/blob/main/src/signal.ts

[8] https://ngrx.io/guide/signals/rxjs-integration#rxmethod

[9] https://angular.dev/api/router/withComponentInputBinding

[10] https://github.com/mikezks/windev-signal-apis/blob/main/libs/domain/checkin/src/lib/feature-passenger/passenger-edit/passenger-edit.component.ts

[11] https://ngxtension.netlify.app

[12] https://github.com/mikezks/windev-signal-apis/blob/main/libs/shared/core/src/lib/util-signals/signal-operators.ts

[13] https://youtu.be/aKxcIQMWSNU?t=520

[14] https://github.com/mikezks/windev-signal-apis/blob/linked/libs/domain/booking/src/lib/ui-flight/flight-card/flight-card.component.ts

[15] https://github.com/mikezks/windev-signal-apis/blob/main/libs/domain/booking/src/lib/feature-flight/airport/airport.ts

[16] https://github.com/mikezks/windev-signal-apis/blob/main/libs/domain/checkin/src/lib/logic-passenger/data-access/passenger.service.ts

[17] https://github.com/mikezks/windev-signal-apis/blob/http-resource/libs/domain/checkin/src/lib/feature-passenger/passenger-edit/passenger-edit.component.ts

 

🔍 Frequently Asked Questions (FAQ)

1. Was sind Angular Signals?

Angular Signals sind neue reaktive Primitive im Angular-Framework, die einen synchronen, sofort verfügbaren Zustand bereitstellen. Sie ermöglichen eine direkte, performante Synchronisation zwischen JavaScript-Zustand und DOM – ohne async/await, without subscribe/unsubscribe und ohne AsyncPipe.

2. Was können Angular Signals besonders gut?

Signals glänzen überall dort, wo abgeleiteter Zustand und UI-Synchronisation gefragt sind. Sie liefern Werte synchron, vermeiden unnötige Re-Renders, fassen schnelle Update-Serien zusammen und helfen, die Framework-Komplexität zu reduzieren. Dadurch reagieren Komponenten effizienter und vorhersehbarer.

3. Wofür werden Angular Signals in der Praxis genutzt?

Signals kommen vor allem zum Einsatz bei:

  • State Management innerhalb von Komponenten
  • reaktiven Bindings wie input(), model() oder output()
  • abgeleiteten Werten mit computed()
  • Side-Effects über effect()
  • sowie asynchronen Operationen über Resource APIs (resource(), rxResource(), httpResource()).

4. Wer sollte Angular Signals einsetzen?

Jedes Angular-Team, das neue Features entwickelt oder bestehende Codebases modernisieren möchte, sollte Signals nutzen. Mit Angular 17–21 sind die APIs stabil geworden, und das Angular-Team empfiehlt ausdrücklich, Signals für neue Features standardmäßig einzusetzen.

5. Warum setzt Angular auf Signals und nicht weiter ausschließlich auf RxJS?

RxJS bleibt weiterhin wichtig, ist aber für viele grundlegende UI-Synchronisationsaufgaben überdimensioniert. Signals bieten einen einfacheren, deterministischen und eingebauten Weg, Zustand und DOM miteinander zu verbinden. Für komplexe Event-Streams oder spezielle Reaktivität bleibt RxJS weiterhin das Mittel der Wahl.

6. Wie verändert Signals das Entwickeln von Angular-Components?

Mit Signals können Inputs (input()), Two-Way-Bindings (model()) und Outputs (output()) ohne Decorators definiert werden. Der Zustand ist ohne AsyncPipe lesbar, und Change Detection wird schlanker. Dadurch werden Komponenten klarer, leichter zu lesen und simpler zu refactoren.

7. Was bedeutet die Einführung der Resource APIs für asynchrone Prozesse?

Die Resource APIs ermöglichen erstmals eine reaktive Asynchronität ohne RxJS. Sie verhindern Race Conditions, unterstützen Reload-Mechanismen und liefern mehrere Signals (z. B. value, loading, error) in einem konsistenten Objekt. Das macht datengetriebene Komponenten einfacher und robuster, auch wenn die API derzeit noch experimentell ist.

The post Mit Angular Signals das Web-Frontend verändern appeared first on JavaScript & Angular Days.

]]>
LangChain Framework: So integrieren Sie LLMs in Ihre Anwendungen https://javascript-days.de/blog/langchain-framework-llm-integration/ Wed, 24 Sep 2025 13:41:55 +0000 https://javascript-days.de/?p=107666 Das LangChain Framework erleichtert die Integration von LLMs wie OpenAI oder Ollama. Entwickler können damit KI-Anwendungen wie Chatbots, RAG-Systeme und Textzusammenfassungen umsetzen – in JavaScript, TypeScript oder Python.

The post LangChain Framework: So integrieren Sie LLMs in Ihre Anwendungen appeared first on JavaScript & Angular Days.

]]>
Wahrscheinlich kennen Sie das Meme mit dem bekannten Zitat aus dem Film „The Wolf of Wall Street“: „Sell me this pen“. Die Antwort lautet: „It’s AI powered“. Mittlerweile ist es ja schon fast nicht mehr zum Lachen – alle wollen AI, aber was die praktische Umsetzung angeht, ist sich keiner so richtig einig. Hier kommt das Framework LangChain ins Spiel.

Ein entscheidender Vorteil des AI-Booms für uns Softwareentwickler ist, dass sich Probleme herauskristallisieren, die in der gleichen oder ähnlichen Form überall auftreten. Die Integration eines KI-Modells ist ein typisches Beispiel. Die meisten Plattformen wie das OpenAI API, Google Gemini oder auch das lokale Ollama bieten HTTP-Schnittstellen, über die Sie mit einem LLM interagieren können. Zusätzlich dazu stellt Ihnen jeder Anbieter ein eigenes Paket zur Integration in Ihre Applikation zur Verfügung.

Das Problem an dieser Stelle ist jedoch, dass sich die Anbieter in den Details ihrer APIs unterscheiden. Das macht einen leichtgewichtigen Austausch der Modelle schwierig. Was für die Modelle gilt, gilt auch für andere Komponenten einer KI-Applikation. Wäre es da nicht schön, wenn es ein Framework gäbe, das Ihnen die Arbeit mit dem unübersichtlichen und schnell wachsenden KI-Ökosystem erleichtert? Das Framework, nach dem Sie suchen, heißt LangChain.

LangChain ist modular aufgebaut und adressiert die wichtigsten Aspekte einer AI-Applikation von der Anbindung von Modellen über den Umgang mit Prompts und Templates und der Anbindung von Vektordatenbanken bis hin zur Verarbeitung der Ausgabe. Dabei arbeitet das Framework, wie der Name schon andeutet, mit Ketten. Diese sind vergleichbar mit dem Konzept von Pipes, die Sie beispielsweise aus Unix-Shell oder der Rx-Bibliothek kennen. Die einzelnen Glieder der Kette haben jeweils eine Ein- und Ausgabe und können zu beliebig langen Ketten zusammengefügt werden.

Das LangChain-Framework existiert in zwei verschiedenen Varianten: Python und JavaScript bzw. TypeScript. Die grundlegende Architektur und die Konzepte sind in beiden Versionen die gleichen. Dieser Artikel verwendet für die konkreten Umsetzungsbeispiele die JavaScript-Variante.

 

 

Aufbau und Konzepte

Das wichtigste Paket des LangChain-Frameworks ist das Core-Paket. Es enthält die grundlegenden Strukturen für alle weiteren Komponenten. In der JavaScript-Variante trägt dieses Paket den Namen @langchain/core. Es enthält alle Basisklassen, wie beispielsweise BaseLLM, von der die konkreten Model-Klassen für die LLM-Integration ableiten. Außerdem definiert dieses Paket die LangChain Expression Language (LCEL), eine Schnittstelle, mit der Sie auf einfache Art die Ketten für Ihre Applikation definieren können. Das zweite Paket ist das langchain-Paket, das die Strukturen für Ketten, Agents und Retrieval Strategien beisteuert.

Zu diesen beiden Paketen kommt noch eine Vielzahl zusätzlicher Integrationspakete für die verschiedenen Bestandteile der Applikation. So können Sie das @langchain/openai-Paket nutzen, um das OpenAI API in Ihre Applikation einzubinden, oder Sie nutzen @langchain/pinecore, um die Pinecore-Vektordatenbank zu integrieren.

Für beide Pakete gilt, dass Sie sich problemlos sowohl für JavaScript als auch für TypeScript entscheiden können. LangChain liefert immer die aktuellen Typdefinitionen selbst aus, da das gesamte Framework in TypeScript umgesetzt ist. Die wichtigsten Bestandteile des Frameworks lernen Sie im Folgenden am konkreten Beispiel kennen und sehen dabei, wie die einzelnen Komponenten ineinandergreifen.

Die erste Beispielapplikation: ein Chatbot mit LangChain

Wenn Sie von KI-Applikationen hören, kommt Ihnen wahrscheinlich sehr schnell die typische Chatbot-Anwendung in den Sinn. Und genau mit einer solchen wollen wir den Einstieg in LangChain beginnen. Das Beispiel ist vollständig lokal lauffähig und setzt dafür auf Ollama als Plattform, die ein Sprachmodell lokal ausführt. Als konkretes Modell kommt Llama in der Version 3.2 zum Einsatz. Neben den bereits vorgestellten Paketen benötigen Sie dafür noch das @langchain/ollama-Paket, um mit der Plattform zu kommunizieren.

Egal, ob Sie Ollama oder eine andere Plattform wie OpenAI nutzen, die HTTP-Schnittstellen sind in der Regel zustandslos. Das bedeutet, dass das Modell kein Gedächtnis hat, die Konversation nach der Antwort also sofort wieder vergisst. Dieses Problem können Sie lösen, indem Sie eine Nachrichtenhistorie integrieren. Auch für diesen Fall bietet Ihnen LangChain eine passende Struktur. Doch bevor es an die konkrete Umsetzung geht, werfen wir einen Blick auf die Komponenten, die Sie für die Umsetzung eines solchen Chatbots benötigen:

  • Prompt: Der Prompt ist die Nachricht, die Sie an das Sprachmodell senden, um eine Antwort zu erhalten.

  • Prompt-Template: Mit dem Prompt-Template können Sie zum Prompt noch weitere Strukturen hinzufügen, um dem Model zusätzlichen Kontext zu bieten.

  • Model: Das Model steht für das Sprachmodell, mit dem Ihre Applikation interagiert. Dieser Klasse übergeben Sie im Fall von Ollama das konkrete Modell, das Sie verwenden möchten, oder bei OpenAI zusätzlich noch Ihren API-Key.

  • OutputParser: Mit dem OutputParser können Sie die Antwort des Modells manipulieren und den Inhalt der Nachricht extrahieren.

Der Prompt ist die Eingabe eines Menschen. Sie kann in ein Prompt-Template eingefügt werden. Diese Eingabe wird an das Modell weitergegeben, das dann die Antwort erzeugt. Der OutputParser liefert Ihnen schließlich die Antwort des Modells. In Listing 1 sehen Sie den Code des Beispiels.

Listing 1: Chatbot mit Historie in LangChain

import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import { AIMessage, HumanMessage } from '@langchain/core/messages';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import {
  RunnableSequence,
  RunnableWithMessageHistory,
} from '@langchain/core/runnables';
import { ChatOllama } from '@langchain/ollama';

const messageHistories: Record<string, InMemoryChatMessageHistory> = {};

const prompt = ChatPromptTemplate.fromMessages([
  [
    'system',
    'You are a helpful assistant who remembers all details the user shares with you.',
  ],
  ['placeholder', '{chat_history}'],
  ['human', '{input}'],
]);

const model = new ChatOllama({
  model: 'llama3.2',
});

const parser = new StringOutputParser();

const chain = RunnableSequence.from([prompt, model, parser]);

const withMessageHistory = new RunnableWithMessageHistory({
  runnable: chain,
  getMessageHistory: async (sessionId) => {
    if (messageHistories[sessionId] === undefined) {
      messageHistories[sessionId] = new InMemoryChatMessageHistory();
    }
    return messageHistories[sessionId];
  },
  inputMessagesKey: 'input',
  historyMessagesKey: 'chat_history',
});

const config = {
  configurable: {
    sessionId: 'theSession',
  },
};

const messages = [
  new HumanMessage('Hey AI, I need help with my JavaScript project.'),
  new AIMessage("Sure! What's the project about, and what's the issue?"),
];

const stream = await withMessageHistory.stream(
  {
    chat_history: messages,
    input:
      "I'm building a task list app, but the date filtering isn't working right.",
  },
  config
);
for await (const chunk of stream) {
  console.log(chunk);
}

Die erste Komponente der Implementierung ist das Prompt-Template. Dieses erstellen Sie mit der fromMessages-Methode der ChatPromptTemplate-Klasse. Ihr übergeben Sie ein Array aus Nachrichten, wobei jede Nachricht aus einem Tupel aus Nachrichtentyp und Nachricht gebildet wird. Es gibt drei verschiedene Typen von Nachrichten: system steht für den System-Prompt, mit dem Sie das Verhalten des Systems beeinflussen können. Den Typ ai nutzen Sie, um eine Antwort des Modells zu speichern und human steht schließlich für eine menschliche Eingabe. Eine Sonderrolle nimmt der Typ placeholder ein. Hier platzieren Sie später die Einträge der Chat-Historie.

Als Modell nutzt das Beispiel eine Instanz der ChatOllama. Hier wird nur der Wert für das zu verwendende Modell überschrieben, sodass das Llama-3.2-Modell verwendet wird. Diese Konfiguration sorgt dafür, dass die Applikation mit dem Ollama API auf http://localhost:11434 kommuniziert. Als Output Parser kommt im Beispiel der StringOutputParser zum Einsatz. Dieser nimmt ein Objekt vom Typ AIMessage entgegen und extrahiert den Textinhalt, da das ursprüngliche Objekt neben diesem Inhalt noch eine ganze Reihe von Metainformationen enthält, die für das Beispiel nicht relevant sind.

Prompt-Template, Modell und Output Parser fasst die from-Methode der RunnableSequence-Klasse zu einer Kette zusammen. Diese können Sie bereits verwenden, indem Sie die invoke– oder stream-Methode des zurückgegebenen Objekts verwenden. In diesem Fall hat Ihre Applikation jedoch noch kein Gedächtnis. Dieses erhält sie erst mit dem withMessageHistory-Objekt. Dafür instanziieren Sie die RunnableWithMessageHistory-Klasse. Dem Konstruktor übergeben Sie ein Objekt, das die Kette mit Prompt, Modell und Parser in der runnable-Eigenschaft enthält. Außerdem definieren Sie die getMessageHistory-Methode. Sie arbeitet mit einer SessionId, um verschiedene User-Sessions verwalten zu können. Jede Session erhält ihr eigenes InMemoryChatMessageHistory-Objekt, das im globalen messageHistories-Objekt festgehalten wird. Außerdem definieren Sie mit inputMessagesKey und historyMessagesKey zwei Platzhalter, die für den User-Prompt und die Chat-Historie stehen und auf die Sie im Prompt-Template zugreifen können.

Mit dem withMessageHistory-Objekt können Sie mit dem Modell interagieren. Im Beispielcode sehen Sie zwei verschiedene Arten: die stream– und die invoke-Methode. Die stream-Methode stellt Ihnen die Antworten als Stream aus Tokens zur Verfügung. Das hat den Vorteil, dass Sie mit der Anzeige der Ergebnisse schon beginnen können, bevor die Antwort vollständig vorliegt. Die einfachste Variante, die einzelnen Tokens zu konsumieren, ist die Verwendung der asynchronen for-of-Schleife. Im Gegensatz dazu erhalten Sie das Ergebnis bei der invoke-Methode erst, wenn das Modell die Antwort vollständig generiert hat. Egal, welche der beiden Methoden Sie nutzen, in jedem Fall übergeben Sie ein Objekt mit dem Prompt und optional können Sie zusätzlich noch eine Chat-Historie angeben. Im zweiten Argument geben Sie die Session-ID für die korrekte Zuordnung der Chat-Session an. Diese Implementierung können Sie im nächsten Schritt in ein beliebiges Web Application Framework, wie beispielsweise Express oder Nest, integrieren und eine Clientapplikation anbinden.

Einen Aspekt der Chat-Historie müssen Sie jedoch noch beachten: Die Historie darf nicht beliebig groß werden. Die LLMs, mit denen Sie hier arbeiten, verfügen über einen Kontext. Das ist Text, den Sie dem Modell für eine Interaktion übergeben können, um passende Antworten zu erzeugen. Dadurch kann beispielsweise der bisherige Gesprächsverlauf zur Verfügung gestellt werden. Die Größe des Kontexts ist jedoch limitiert. Sie liegt bei Llama 3.2 beispielsweise bei 128 000 Tokens. Sie sollten darauf achten, dass Ihre Konversation diese Größe nicht überschreitet, und wenn dies der Fall ist, entweder die Historie entsprechend limitieren und Ihre User darüber in Kenntnis setzen oder ältere Nachrichten einfach kontrolliert abschneiden. In diesem Fall kommt es zu Informationsverlust.

Neben dem typischen Chatbot können Sie mit LangChain noch viele weitere spannende Probleme lösen. In den folgenden Abschnitten erfahren Sie, wie Sie LangChain nutzen können, um sich Texte zusammenfassen zu lassen.

Setze JavaScript Trends um

16. - 20. März 2026

>

Textzusammenfassungen mit LangChain

Grundsätzlich können Sie einem LLM einen beliebigen Text übergeben und es mit einem Prompt auffordern, ihn für sich zusammenfassen zu lassen. Die einzige Limitierung ist hier das Kontextfenster. Bei modernen LLMs liegt seine Größe normalerweise bei 128 000 Tokens. Hier fangen die Probleme aber schon an: Ist das viel oder wenig? Gehen wir von einer normalen 12-Punkt-Schriftart und einem einfachen Zeilenabstand und etwas Seitenrändern aus, passen etwa 500 Wörter auf eine DIN-A4-Seite. Allerdings können Sie Wörter nicht einfach in Tokens umrechnen, sondern sollten eher von 0,75 Wörtern pro Token ausgehen. Damit ergeben die 128 000 Tokens etwa 190 bis 200 Seiten, also schon eine ganze Menge.

Irgendwann stoßen Sie jedoch selbst mit einem großen Kontext an eine Grenze, und auch sonst können Sie es einem LLM mit einem Trick leichter machen, große Mengen von Text zu verarbeiten. Dieser besteht darin, dass Sie den Text in kleinere Einheiten herunterbrechen und diese verarbeiten lassen. LangChain kann Sie dabei mit der SummarizationChain unterstützten. Der grundlegende Aufbau sieht dabei folgendermaßen aus:

  1. Das gesamte Dokument laden

  2. Unterteilen des Dokuments in kleinere Bestandteile

  3. Erzeugen der SummarizationChain

  4. Ausführen der Summarization Chain

Wie die Implementierung konkret im Code aussieht, sehen Sie in Listing 2.

Listing 2: Textzusammenfassung mit Langchain

import { Ollama } from '@langchain/ollama';
import { loadSummarizationChain } from 'langchain/chains';
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
import * as fs from 'fs';
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';

const filePath = './docs/Ch2.pdf';

const loader = new PDFLoader(filePath);
const pdf = await loader.load();
const text = pdf.map((doc) => doc.pageContent).join('\n\n');

const model = new Ollama({ model: 'llama3.2' });
const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
const docs = await textSplitter.createDocuments([text]);

const chain = loadSummarizationChain(model);
const res = await chain.invoke({
  input_documents: docs,
});
console.log({ res });

Für das Beispiel gehen wir von einer PDF-Datei als Eingabe aus. Diese müssen Sie zunächst in Text umwandeln. Da es sich hierbei um ein Problem handelt, das sehr häufig auftritt, gibt es in der LangChain-Community ein Paket, das eine Lösung für das Lesen von PDF-Dateien bietet. Sie finden es unter dem Namen PDFLoader im @langchain/community-Paket. Zusätzlich zu diesem Paket müssen Sie noch das pdf-parse-Paket installieren. Anschließend können Sie den PDFLoader mit dem Pfad zur PDF-Datei instanziieren und das Dokument mit der load-Methode des Loader laden.

Die wichtigsten Komponenten neben der SummarizationChain sind in diesem Beispiel das Modell, das auch in diesem Fall wieder Llama 3.2 ist, und der TextSplitter. Der TextSplitter zerlegt, wie der Name vermuten lässt, den Text in annähernd gleichgroße Teile. Der RecursiveCharacterTextSplitter ist am besten für generische Texte geeignet und zerlegt den Text anhand einer bestimmten Zeichensequenz. Bei der Instanziierung geben Sie die gewünschte Blockgröße, hier 1000 Zeichen, an. Als Trennzeichen für die Zerlegung nutzt dieser TextSplitter die Zeichenfolgen „\n\n“, „\n“ und „ “. Zuerst versucht das Werkzeug einen 1000-Zeichen-Block anhand der zwei Zeilenumbrüche abzutrennen. Ist das nicht möglich, wird das Gleiche mit einem Zeilenumbruch versucht und schließlich beim nächstgelegenen Leerzeichen. Damit soll der TextSplitter sicherstellen, dass möglichst zusammengehörige Textblöcke erzeugt werden.

Der createDocuments-Methode können Sie einen, aber auch mehrere Texte in Form eines String-Arrays übergeben. Die daraus resultierenden Dokumente nutzen Sie beim invoke-Aufruf der SummarizationChain als Eingabe. Um diese Kette zu erstellen, nutzen Sie die loadSummarizationChain-Funktion. Dieser müssen Sie beim Aufruf lediglich das Modell übergeben. Führen Sie den Code aus, erhalten Sie die Zusammenfassung des übergebenen Texts.

Sie können noch weiter Einfluss auf die Zusammenfassung nehmen, indem Sie beim Aufruf der loadSummarizationChain-Funktion ein Konfigurationsobjekt übergeben. Hier können Sie über die type-Eigenschaft den Typ der Zusammenfassung festlegen. Die SummarizationChain unterstützt hier drei verschiedene Werte:

  • map_reduce: Dieser Typ arbeitet nach dem Map-Reduce-Muster und erzeugt für jeden Textblock eine Zusammenfassung. Diese werden dann zu einer Gesamtzusammenfassung reduziert. Der map_reduce-Typ ist der Standard von LangChain, wenn Sie keinen Typ angeben.

  • refine: Mit diesem Typ bildet die SummarizationChain eine initiale Zusammenfassung über den ersten Block und verfeinert sie mit allen folgenden Blöcken, bis am Ende eine Gesamtzusammenfassung entstanden ist. Diese Strategie eignet sich besonders für sehr lange Texte.

  • stuff: Der stuff-Typ ist die einfachste Art der Zusammenfassung. Alle Textblöcke werden zu einem großen Block zusammengefasst und eine Zusammenfassung darüber gebildet. Diese Strategie ist nicht für Texte geeignet, die länger als das Kontextfenster sind.

Je nachdem, welchen Typ Sie auswählen, können Sie zusätzlich noch Prompt-Templates für die Zusammenfassung angeben und so die Zusammenfassung anpassen. Die Qualität des Ergebnisses hängt sowohl vom verwendeten Modell als auch vom gewählten Typ und zu einem kleinen Teil von den Prompt-Templates ab. Sie sollten mit verschiedenen Kombinationen experimentieren, um ein optimales Ergebnis für Ihre Anforderungen zu erreichen.

Die bisherigen beiden Beispiele waren auf Ihr lokales System beschränkt. LangChain ist jedoch nicht nur auf die Arbeit mit lokalen Interaktionen und Datenquellen beschränkt, wie Sie im nächsten Beispiel sehen werden.

Webseiten analysieren lassen

Sie können LangChain auch dazu verwenden, Webseiten einzulesen und Fragen dazu zu stellen. Sie benötigen dazu ein Modell, Embeddings und das WebBrowser-Werkzeug. Mit dem WebBrowser ist natürlich nicht der Browser auf Ihrem System gemeint, sondern eine Klasse aus dem tools-Modul von LangChain, das die gleiche Rolle wie ein Browser spielt. In Listing 3 sehen Sie, wie Sie mit Hilfe von LangChain und Wikipedia herausfinden können, wann JavaScript erfunden wurde.

Listing 3: Webseiten mit LangChain auswerten

import { WebBrowser } from 'langchain/tools/webbrowser';

import { Ollama, OllamaEmbeddings } from '@langchain/ollama';

const model = new Ollama({
  model: 'llama3.2',
});

const embeddings = new OllamaEmbeddings({
  model: 'llama3.2',
});

const browser = new WebBrowser({ model, embeddings });

const result = await browser.invoke(
  `"https://en.wikipedia.org/wiki/JavaScript", "Wann wurde JavaScript erfunden?"`
);

console.log(result);

Die Grundlage des Beispiels bildet wieder ein Modell wie das Llama 3.2. Außerdem benötigen Sie ein Embeddings-Modell. Dieses erzeugt eine Vektorrepräsentation aus einem Stück Text, die Sie mit LangChain weiterverarbeiten können. LangChain bietet Ihnen eine ganze Reihe von Embeddings wie OpenAI, MistralAI, Nomic oder die im Beispiel verwendeten Ollama Embeddings. Geben Sie nichts weiter an, nutzt die OllamaEmbeddings-Klasse das mxbai-embed-large-Modell. Das Problem mit diesem Modell ist, dass es einen verhältnismäßig kleinen Kontext hat, was schon bei einfachen Webseiten dazu führen kann, dass Sie die Fehlermeldung „Input length exceeds maximum context length“ erhalten. Die einfachste Lösung für dieses Problem ist, dass Sie ein anderes Modell angeben. Mit dem Modell und den Embeddings können Sie schließlich eine Instanz der WebBrowser-Klasse erzeugen. Diese stellt Ihnen die invoke-Methode zur Verfügung, die Sie mit einer Zeichenkette aufrufen. Zuerst geben Sie den URL der Webseite an, die Sie analysieren möchten, in unserem Beispiel ist das die Wikipedia-Seite über JavaScript. Anschließend formulieren Sie einen Prompt. Das WebBrowser-Werkzeug nutzt Axios, um eine Anfrage an den URL zu stellen und wertet die Antwort mit Hilfe des HTML-Parsers cheerio aus. Diese Daten wandelt das Embeddings-Modell in eine Vektorrepräsentation um und stellt sie dem Modell als Kontext zur Verfügung. Führen Sie diesen Code aus, erhalten Sie die Antwort, dass Brendan Eich JavaScript 1995 erfunden hat und zusätzlich dazu noch eine Reihe weiterführender Links.

Bleib informiert

Brancheninsights im Newsletter erhalten:

[mc4wp-simple-turnstile]

Websuche mit LangChain

Neben dem WebBrowser gibt es noch einige weitere Tools für Langchain. Eine Kategorie, die für Applikationen besonders spannend sein können, sind Suchmaschinenschnittstellen, mit denen Sie Ihrer Applikation das Tor zum Internet noch weiter öffnen können. Beim WebBrowser müssen Sie einen einzelnen URL angeben. Mit den Suchmaschinenintegrationen können Sie eine Suchmaschine befragen und erhalten aktuelle Informationen aus dem Internet, die Sie in Ihrer LangChain-Applikation weiterverwenden können. Beispiele für solche Suchmaschinenintegrationen sind die DuckDuckGoSearch oder die TavilySearch. Im folgenden Beispiel sehen Sie, wie Sie die TavilySearch nutzen können. Im einfachsten Fall erzeugen Sie eine Instanz des Werkzeugs und rufen die invoke-Methode mit Ihrer Suchanfrage auf. In Listing 4 sehen Sie jedoch, wie Sie es in eine Kette einbinden können.

Listing 4: Integration der Web-Suche in eine LangChain-Applikation

import { HumanMessage } from '@langchain/core/messages';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { RunnableLambda } from '@langchain/core/runnables';
import { TavilySearchResults } from '@langchain/community/tools/tavily_search';
import { ChatOllama } from '@langchain/ollama';

const tool = new TavilySearchResults({
  maxResults: 2,
});

const model = new ChatOllama({
  model: 'llama3.2',
});

const llmWithTools = model.bindTools([tool]);

const prompt = ChatPromptTemplate.fromMessages([
  ['system', 'You are a helpful assistant.'],
  [
    'system',
    `You have access to the following tool:\n\n${tool.description}\n\nUse this tool whenever you need additional information to answer the user's question.`,
  ],
  ['placeholder', '{messages}'],
]);

const chain = prompt.pipe(llmWithTools);

const toolChain = RunnableLambda.from(async (userInput, config) => {
  const humanMessage = new HumanMessage(userInput);
  const aiMsg = await chain.invoke(
    {
      messages: [humanMessage],
    },
    config
  );
  if (aiMsg.tool_calls && aiMsg.tool_calls.length > 0) {
    const toolMsgs = await tool.batch(aiMsg.tool_calls, config);
    return chain.invoke(
      {
        messages: [humanMessage, aiMsg, ...toolMsgs],
      },
      config
    );
  } else {
    return aiMsg;
  }
});

const toolChainResult = await toolChain.invoke(
  'What is the current weather in Seattle?'
);

console.log(toolChainResult.content);

Für die Verwendung der Tavily-Suchmaschine benötigen Sie einen API-Key. Die Plattform bietet Ihnen verschiedene Verträge an. Mit dem kostenlosen Einstieg können Sie 1000 Suchanfragen pro Monat stellen. Den API-Key geben Sie über die Umgebungsvariable TAVILY_API_KEY an. Mit der TavilySearchResults-Klasse können Sie eine Instanz erzeugen, die Sie als Werkzeug an das LLM binden können. Dies erfolgt, wie Sie im Code sehen, über die bindTools-Methode der Modell-Klasse. Im Prompt-Template teilen Sie dem LLM mit, dass es auf die Suchmaschinenintegration zugreifen kann. Die Werkzeugintegration bedeutet jedoch nicht, dass das LLM von selbst auf das Such-API zugreifen kann. Es erzeugt lediglich die Anfragen für die Suche, die Sie dann selbst ausführen müssen. Wie das funktioniert, sehen Sie im Aufruf der from-Methode der RunnableLambda-Klasse. Hier kommunizieren Sie zunächst mit dem LLM. Enthält dessen Antwort ein Element in der tool_calls-Eigenschaft, bedeutet das, dass Sie mit dem Tavily API kommunizieren sollten. Dies erfolgt mit der batch-Methode. Das Resultat geben Sie in einem invoke-Aufruf wiederum an das LLM, das die finale Antwort erzeugt. Mit dieser Konfiguration können Sie über das toolChain-Objekt und dessen invoke-Methode so über einen kleinen Umweg Ihr LLM an eine Suchmaschine anschließen.

Mit den Bausteinen, die Sie bis jetzt kennengelernt haben, können Sie noch viele weitere Anwendungsfälle umsetzen. Eines der wohl populärsten Beispiele einer KI-Anwendung ist nach dem Chatbot aktuell wohl RAG. Mit LangChain können Sie eine solche Applikation mit nur wenigen Zeilen umsetzen.

RAG mit LangChain

RAG steht für Retrieval Augmented Generation und ist eine Technik, mit deren Hilfe Sie die Möglichkeiten eines LLMs noch deutlich erweitern können. Sie können der Applikation aktuelle und spezialisierte Informationen zur Verfügung stellen, ohne dass Sie das LLM modifizieren müssen. RAG besteht aus zwei Teilen:

  • Retriever: Der Retriever sammelt die passenden Informationen, meist liegen diese in Form von Vektoren vor.

  • Generator: Die Informationen des Retrievers nutzt der Generator, um die Antwort auf einen Prompt zu erzeugen.

Das folgende Beispiel soll eine PDF-Datei zum Thema Python einlesen, verarbeiten und es Ihnen ermöglichen, Fragen zu stellen. Den Code für die RAG-Kette finden Sie in Listing 5.

Listing 5: Implementierung einer RAG-Applikation mit LangChain

import { Ollama, OllamaEmbeddings } from '@langchain/ollama';
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
import { MemoryVectorStore } from 'langchain/vectorstores/memory';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import {
  RunnablePassthrough,
  RunnableSequence,
} from '@langchain/core/runnables';
import { formatDocumentsAsString } from 'langchain/util/document';
import { StringOutputParser } from '@langchain/core/output_parsers';

const filePath = './python.pdf';
const loader = new PDFLoader(filePath);
const docs = await loader.load();

const textSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,
  chunkOverlap: 200,
});
const splits = await textSplitter.splitDocuments(docs);

const embeddings = new OllamaEmbeddings();
const vectorStore = await MemoryVectorStore.fromDocuments(splits, embeddings);

const retriever = vectorStore.asRetriever();
const prompt = ChatPromptTemplate.fromMessages([
  `You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question.
  If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.

   Question: {question} 

   Context: {context} 

   Answer:`,
]);

const model = new Ollama({
  model: 'llama3.2',
});

const ragChain = RunnableSequence.from([
  {
    context: (...args) => retriever.pipe(formatDocumentsAsString),
    question: new RunnablePassthrough(),
  },
  prompt,
  model,
  new StringOutputParser(),
]);

const result = await ragChain.invoke(
  'Was sind die Unterschiede zwischen list und tuple in Python'
);

console.log(result);

Der Code gliedert sich in zwei Teile. Im ersten Teil verarbeiten Sie die Informationen aus der PDF-Datei und im zweiten Teil arbeiten Sie mit dieser Datenquelle. Zunächst nutzen Sie den PDFLoader, um die Datei zu laden. Anschließend nutzen Sie den RecursiveCharacterTextSplitter, um Textblöcke mit 1000 Zeichen zu erzeugen. Die Blöcke überlappen sich dabei um 200 Zeichen. Die Überlappung erhöht die Genauigkeit und Relevanz der Antworten und stellt sicher, dass keine Informationen verloren gehen, wenn der Text beliebig unterteilt wird.

Die Textblöcke werden im nächsten Schritt in einen Vektorspeicher eingefügt. Für die Umwandlung des Texts in Vektoren kommen wieder die OllamaEmbeddings zum Einsatz. In dieser Variante können Sie das Standard-Embeddings-Modell mxbai-mebed-large verwenden, da die Textblöcke problemlos in dessen Kontext passen. Als Vektorspeicher nutzt das Beispiel den MemoryVectorStore von LangChain. Sie können an dieser Stelle aber auch eine persistente Vektordatenbank, wie beispielsweise FAISS oder ChromaDB, verwenden. Der größte Vorteil dieser Implementierung mit dem MemoryVectorStore ist, dass die Vektoren bei jedem Durchlauf erneut aufgebaut werden müssen, was unnötig Zeit in Anspruch nimmt. Arbeiten Sie also an einer Produktivapplikation, sollten Sie in jedem Fall auf eine Vektordatenbank zurückgreifen.

Sobald Sie den Datenspeicher aufgebaut haben, können Sie sich dem eigentlichen Teil der RAG-Applikation widmen und die RAG-Kette aufbauen. Die Kette besteht aus der Eingabe, einem Prompt-Template, dem Modell und einem Output-Parser. Die Besonderheit im Vergleich zu den bisherigen Implementierungen liegt hier im Einstieg in die Kette. Das Prompt-Template arbeitet mit zwei Platzhaltern: context und question. Die question, also die eigentliche Prompt-Eingabe aus dem Aufruf der invoke-Methode, wird unverändert an das Modell weitergereicht, dafür sorgt die RunnablePassthrough-Instanz. Beim Kontext sucht der Retriever nach relevanten Informationen aus dem Vektorspeicher. Die Resultate werden mit der formatDocumentsAsString in einen Stringwert umgewandelt, den das Modell als Kontext verwenden kann.

Die übrige Kette arbeitet dann ganz ähnlich wie die Kette im ersten Beispiel des Chatbots. Rufen Sie die invoke-Methode mit einer beliebigen Frage auf, liefert der Retriever den Kontext, fügt ihn ins Prompt-Template ein und übergibt es ans Modell. Der OutputParser extrahiert die Antwort und liefert sie als Zeichenkette aus.

Die Laufzeit der Applikation hängt von zahlreichen Faktoren ab. Entscheidend sind vor allem die Ressourcen des Systems, auf dem Sie sie ausführen. Steht eine leistungsstarke Grafikkarte zur Verfügung, beschleunigt das den Ablauf erheblich. Auch die Größe der PDF-Datei hat Einfluss auf die Laufzeit, je größer die Datei, desto mehr Textblöcke müssen gebildet, in Vektoren umgewandelt und in der Vektordatenbank gespeichert werden. Damit Sie einen Eindruck über die Verteilung der Laufzeit bekommen, sehen Sie hier eine Messung der Ausführung der einzelnen Applikationsteile:

  • Laden der PDF-Datei: 1 Sekunde

  • Aufteilen der Datei in Textblöcke: 0,2 Sekunden

  • Erzeugen und Speichern der Vektoren: 2,5 Minuten

  • Erzeugen der RAG-Kette: 0,3 Sekunden

  • Aufruf der invoke-Methode: 3,5 Sekunden

Wie Sie sehen, nimmt die Umwandlung der Textblöcke in Vektoren und deren Speicherung den mit Abstand größten Teil der gesamten Operation in Anspruch. Da sich aber weder der Inhalt des Dokuments noch die daraus abgeleiteten Vektoren verändern, müssen Sie diese Operation auch nur einmalig durchführen. Der übrige Prozess läuft verhältnismäßig schnell ab und kann durch ein leistungsstärkeres System oder ein kleineres Modell noch beschleunigt werden.

Fazit

LangChain erfindet das Rad der KI-Applikation nicht neu, sondern stellt Ihnen vielmehr eine Plattform mit einheitlichen Schnittstellen und einem mächtigen Baukastensystem zur Verfügung. Da es die Plattform sowohl für Python als auch für JavaScript bzw. TypeScript gibt, ist die Community auch entsprechend groß und die vorgefertigten und beschriebenen Lösungsansätze sind vielfältig. LangChain schreibt Ihnen nicht vor, welches Sprachmodell Sie für Ihre Applikation nutzen sollen, durch die nahtlose Integration vieler verschiedener Modelle von dem kommerziellen OpenAI API bis hin zu lokal gehosteten LLMs über Plattformen wie Ollama haben Sie die maximale Flexibilität für Ihre Lösung.

Die möglichen Anwendungsfälle reichen von den hier gezeigten typischen Applikationen wie Chatbots oder RAG-Applikationen bis hin zu komplexen Applikationen mit sehr langen Ketten.

LangChain ist so aufgebaut, dass Sie es als Standalone-Framework nutzen können und in jede beliebige Webapplikation integrieren können. Zusätzlich dazu gibt es um LangChain noch ein leistungsstarkes Ökosystem, das aus verschiedenen Open-Source- und kommerziellen Komponenten besteht:

  • LangChain: Ein Open-Source-Framework, das die Integration von Sprachmodellen, Prompts und Datenbanken erleichtert. Mit ihm können Sie komplexe Ketten für LLM-basierte Applikationen erzeugen.

  • LangGraph: Mit diesem Framework aus dem LangChain-Ökosystem können Sie Multi-Agenten-Systeme als gerichtete Graphen verwalten. Dabei definieren Sie die Logik der Agenten und können Themen wie Skalierbarkeit und Fehlertoleranz abdecken. LangGraph ist wie LangChain ebenfalls ein Open-Source-Projekt, das Sie ohne zusätzliche Kosten in Ihre Applikation integrieren können.

  • LangGraph Cloud: Eine kostenpflichtige Cloudumgebung für LangGraph-Applikationen. Durch die Infrastruktur können Sie Ihre Applikation einfacher skalieren und ausfallsicher gestalten.

  • LangSmith: Diese Entwicklungsplattform unterstützt Sie in allen Schritten des Entwicklungsprozesses einer LLM-Applikation. Der Fokus liegt auf dem Tracing, das Ihnen Einblicke in jede Stufe der Ketten und Agenten einer Applikation gibt.

Hinter diesem Ökosystem steht das Unternehmen LangChain, Inc., das sich aktiv um die Weiterentwicklung der einzelnen Komponenten als auch um die Erbringung der Dienste kümmert. Der Vorteil an diesem Ökosystem ist, dass Sie ohne Einschränkungen mit den Open-Source-Komponenten arbeiten und selbst entscheiden können, ob Sie die kostenpflichtigen Dienste in Anspruch nehmen möchten.

 

🔍 Frequently Asked Questions (FAQ)

1. Was ist LangChain?
LangChain ist ein Open-Source-Framework, das die Integration von LLMs (Large Language Models) wie OpenAI, Ollama oder Google Gemini erleichtert. Es bietet modulare Bausteine für Prompts, Vektordatenbanken und Ketten.

2. Was kann man mit LangChain machen?
Mit LangChain lassen sich Chatbots, RAG-Anwendungen, Textzusammenfassungen und sogar Websuchen entwickeln. Das Framework eignet sich für viele praxisnahe KI-Szenarien.

3. Wie funktioniert die Integration von LLMs mit LangChain?
LangChain stellt einheitliche Schnittstellen und Tools bereit, um LLMs flexibel anzubinden. Entwickler können Modelle austauschen, Prompts gestalten und Datenquellen wie PDFs oder Webseiten einbinden.

4. Wer nutzt LangChain?
LangChain wird vor allem von Softwareentwicklern und KI-Teams genutzt, die Anwendungen mit JavaScript, TypeScript oder Python entwickeln. Durch seine Community und Dokumentation ist es für Einsteiger und Profis interessant.

5. Welche Vorteile bietet LangChain gegenüber direkter API-Nutzung?
Statt jede API einzeln zu integrieren, liefert LangChain eine einheitliche Architektur. Das spart Zeit, erhöht die Flexibilität und macht komplexe LLM-Workflows einfacher umsetzbar.

6. Welche Rolle spielt RAG in LangChain?
RAG (Retrieval Augmented Generation) ermöglicht es, externe Informationen in die Antworten eines Modells einzubeziehen. LangChain bietet fertige Bausteine, um RAG-Anwendungen mit Vektordatenbanken schnell umzusetzen.

7. In welchen Sprachen kann man LangChain nutzen?
LangChain steht für JavaScript, TypeScript und Python zur Verfügung. Alle Varianten folgen denselben Konzepten und sind mit vielen Integrationen kompatibel.

The post LangChain Framework: So integrieren Sie LLMs in Ihre Anwendungen appeared first on JavaScript & Angular Days.

]]>
NgRx neu gedacht: Wie das Event API den Signal Store erweitert https://javascript-days.de/blog/ngrx-signal-store-event-api/ Tue, 12 Aug 2025 15:53:06 +0000 https://javascript-days.de/?p=107577 Das neue Event API für den NgRx Signal Store bringt bewährte Flux- und Redux-Prinzipien in die Welt von Angular State Management. Entwickler können flexibel zwischen leichtgewichtigen Signal Stores und komplexeren Redux-Workflows wechseln. Mit Event Groups, Reducer und Effects lassen sich Zustandsänderungen strukturiert umsetzen – ideal für skalierbare Anwendungen.

The post NgRx neu gedacht: Wie das Event API den Signal Store erweitert appeared first on JavaScript & Angular Days.

]]>
Das neue Event API bringt die Arbeitsweise, die man vom Redux-basierten NgRx Global Store kennt, in die Welt des Signal Stores. Da Redux impliziert, dass es nur einen zentralen Store gibt, nutzt das NgRx den allgemeineren Begriff Flux.

Eine der großen Stärken dieses neuen API ist es, dass es sich gezielt für ausgewählte Fälle nutzen lässt. Somit kann man leichtgewichtig starten und bei Bedarf einzelne Stores auf Flux umstellen.

In diesem Artikel zeige ich, wie sich dieses neue API nutzen lässt. Die verwendeten Codebeispiele finden sich unter [1] in den Branches 10b-first-reducer und 10d-redux.

Eventing zwischen Stores

Um zu veranschaulichen, dass sich das Event API gezielt einsetzen lässt, demonstriere ich zunächst die Etablierung einer losen Kopplung zwischen zwei Stores. Hier steht zunächst das Eventing an sich und weniger der Einsatz von Flux im Vordergrund. Letzterem wende ich mich weiter unten zu.

Das verwendete Beispiel besteht aus drei Stores: Der DessertStore verwaltet die geladene Liste mit Desserts und für die Bearbeitung eines ausgewählten Desserts kommt der DessertDetailStore zum Einsatz. Außerdem finden sich die Bewertungen der einzelnen Desserts in einem RatingStore (Abb. 1).

steyer_kolumne_1
Abb. 1: Signal Stores im Beispiel

Eine solche Aufteilung ist bei leichtgewichtigen Stores wie dem Signal Store nicht unüblich. Im Gegensatz zu Redux erfolgt hier die Aufteilung nicht anhand technischer Aspekte wie Reducer, Effects oder Selectors. Stattdessen wird ein kleiner Teil des Zustands gemeinsam mit darauf basierenden Berechnungen und Operationen gekapselt.

Der verwaltete Zustand entspricht häufig einer Entität aus Sicht eines (Teil-)Features. Diese Aufteilung hat im hier betrachteten Beispiel zur Folge, dass ein vom DessertDetailStore aktualisiertes Dessert ggf. auch im DessertStore aktualisiert werden muss, um die geladene Übersicht aktuell zu halten.

Um zu verhindern, dass beide Stores aneinandergekoppelt werden, bietet sich ein Event an, das die Aktualisierung eines Desserts anzeigt. Solche Events lassen sich mit der Funktion event definieren. Üblicher ist es jedoch, mehrere zusammengehörige Events im Rahmen einer EventGroup bereitzustellen (Listing 1).

Listing 1

import { type } from '@ngrx/signals';
import { eventGroup } from '@ngrx/signals/events'; 
import { Dessert } from './dessert';

export const dessertDetailStoreEvents = eventGroup({
  source: 'Dessert Detail Store',
  events: {
    dessertUpdated: type<{
      dessert: Dessert
    }>(),
  },
});

Alle Events einer Gruppe haben dieselbe Source. Diese gibt an, welche Teile der Anwendung sie auslösen. Somit lassen sich die Nachrichtenflüsse beim Debuggen besser nachvollziehen. Jedes Event definiert sich durch einen Namen (hier: dessertUpdate) und einem Typ (type) für seine Payload. Diese Payload enthält Daten, die das Event näher beschreiben.

Der DessertDetailStore löst das dessertUpdate Event nach dem Speichern eines Desserts aus. Dazu nutzt er den von der Eventing API bereitgestellten Dispatcher, den sich der Store injizieren lässt (Listing 2).

Listing 2

import { Dispatcher } from '@ngrx/signals/events';

[...]

export const DessertDetailStore = signalStore(
  [...]
  withProps(() => ({
    [...],
    _dispatcher: inject(Dispatcher)
  })),
  withMethods((store) => ({
    [...],
    save(id: number, dessert: Partial<Dessert>): void {
      
      [...]
      // Trigger event
      const event = dessertDetailStoreEvents.dessertUpdated({
        dessert: savedDessert
      });
      store._dispatcher.dispatch(event);

    },
  })),
);

Als Empfänger im DessertStore fungiert ein Reducer. Er nimmt den Payload des Events entgegen und liefert einen Updater zurück (Listing 3).

Listing 3

import { on, withReducer } from '@ngrx/signals/events';

[...]

export const DessertStore = signalStore(
  { providedIn: 'root' },
  withState({
    [...]
    desserts: [] as Dessert[],
  }),
  withReducer(
    on(dessertDetailStoreEvents.dessertUpdated, ({ payload }) => {
      const updated = payload.dessert;
      return (store) => ({
        desserts: store.desserts.map((d) =>
          d.id === updated.id ? updated : d,
        ),
      });
    }),
  ),
  [...]
)

Erlebe Angular Trends live

16. - 20. März 2026

>

Der Updater aktualisiert das aktualisierte Dessert in der Liste mit sämtlichen abgerufenen Desserts. Der zurückgelieferte Zustand ist partiell, d. h., er muss lediglich die geänderten Eigenschaften enthalten. Wie vom Signal Store gewohnt, lassen sich Updater auch in eigene Funktionen auslagern (Listing 4). Damit lässt sich der Reducer verkürzen (Listing 5).

Listing 4

export type DessertSlice = {
  desserts: Dessert[];
};

function updateDessert(updated: Dessert) {
  return (store: DessertSlice) => ({
    desserts: store.desserts.map((d) => 
      (d.id === updated.id ? updated : d)),
  });
}

Listing 5

withReducer(
  on(dessertDetailStoreEvents.dessertUpdated, ({ payload }) => {
    return updateDessert(updated);
  }),
),

Diese Möglichkeit kommt auch zum Einsatz, um Updater von Custom Features wie withEntity zu nutzen. Wird kein Updater benötigt, kann der Reducer auch direkt einen aktualisierten partiellen Zustand liefern. Ich greife diese Möglichkeit weiter unten auf.

 

Flux/Redux für einzelne Stores

Im nächsten Schritt möchte ich zeigen, wie sich das Event API nutzen lässt, um das Flux-Muster für einzelne Stores zu implementieren. Somit ergibt sich die vom Redux-basierten NgRx Global Store gewohnte Arbeitsweise:

  • Der Konsument löst ein Event aus (beim Global Store ist hier von Action die Rede).

  • Effects empfangen Events und stoßen asynchrone Seiteneffekte an. Das Ergebnis veröffentlichen sie in Form weiterer Events.

  • Reducer im Store nehmen Events entgegen und aktualisieren den verwalteten Zustand.

Auch hier gilt, dass man diese Vorgehensweise selektiv anwenden kann und nicht allen Stores aufzwingen muss. Besonders interessant ist dieses Muster, wenn mehrere Komponenten denselben Zustand konsumieren sowie verändern.

Die Implementierung startet wieder mit einer Event Group. Sie enthält ein Event loadDesserts, das einen Effect veranlasst, Desserts zu laden. Dieser Effect informiert daraufhin über den Ausgang dieses Unterfangens mit dem Event loadDessertsSuccess oder im Fehlerfall mit loadDessertsError (Listing 6).

Listing 6

import { eventGroup } from '@ngrx/signals/events'; 
import { type } from '@ngrx/signals';
import { Dessert } from './dessert';

export const dessertEvents = eventGroup({
  source: 'Dessert Feature',
  events: {
    loadDesserts: type<{
      originalName: string,
      englishName: string,
    }>(),
    loadDessertsSuccess: type<{
      desserts: Dessert[]
    }>(),
    loadDessertsError: type<{
      error: string
    }>(),
  },
});

Reducer

Die Reducer für das Laden von Desserts sind etwas simpler als im ersten Fall. Anstatt eines Updaters, der den aktuellen Zustand auf den neuen abbildet, liefern sie lediglich die neuen Werte zurück (Listing 7).

Listing 7

import { on, withReducer } from '@ngrx/signals/events';
[...]

export const DessertStore = signalStore(
  { providedIn: 'root' },
  withState({
    filter: {
      originalName: '',
      englishName: '',
    },
    loading: false,
    desserts: [] as Dessert[],
    error: '',
  }),
  withReducer(
    [...],
    on(dessertEvents.loadDesserts, ({ payload }) => {
      return {
        filter: payload,
        loading: true,
      };
    }),
    on(dessertEvents.loadDessertsSuccess, ({ payload }) => {
      return {
        desserts: payload.desserts,
        loading: false,
      };
    }),
    on(dessertEvents.loadDessertsError, ({ payload }) => {
      return {
        error: payload.error,
        loading: false,
      };
    }),
  ),
  [...]
);

Effects

Die Effects im Event API, die nichts mit dem gleichnamigen Konzept in der Signals-Welt zu tun haben, sind technisch gesehen RxJS-basierte Pipes, die eingehende Events auf ausgehende Events abbilden. Dazu binden sie in der Regel über einen Flattening-Operator wie switchMap oder exhaustMap eine asynchrone Aktion ein (Listing 8).

Listing 8

import { Events, withEffects } from '@ngrx/signals/events';
import { mapResponse } from '@ngrx/operators';

[...]

export const DessertStore = signalStore(
  [...],
  withProps(() => ({
    _dessertService: inject(DessertService),
    _toastService: inject(ToastService),
    _events: inject(Events),
  })),
  [...]
  withEffects((store) => ({
    loadDesserts$: store._events.on(dessertEvents.loadDesserts).pipe(
      switchMap((e) =>
        store._dessertService.find(e.payload).pipe(
          mapResponse({
            next: (desserts) => dessertEvents.loadDessertsSuccess({ desserts }),
            error: (error) =>
              dessertEvents.loadDessertsError({ error: String(error) }),
          }),
        ),
      ),
    ),
  })),
);

Auch wenn ein sprechender Name, z. B. loadDesserts$ im gezeigten Fall, wichtig für die Wartbarkeit ist, spielt er technisch gesehen keine Rolle, denn das Event API stößt den Effect nicht über seinen Namen, sondern über Events an. Der Store erzeugt Effects über den Service Event. Seine Methode on filtert die vom Event API empfangenen Events und liefert sie über ein Observable zurück. Übergibt der Store keinen Parameter an on, erhält er sämtliche Events. Übergibt der Store einen oder mehrere Event-Typen, liefert on nur die dazu passenden Events.

Um das Ergebnis der angestoßenen Seiteneffekte auf ein ausgehendes Event abzubilden, bietet sich der mit NgRx gelieferte Operator mapResponse an. Er ist eine Kombination aus map und catchError und erlaubt eine Transformation der Ergebnisse sowie von Fehlern.

Wie in den ersten Tagen des NgRx Global Store ist es essenziell, dass Fehler behandelt werden, z. B. mit catchError oder eben – wie gezeigt – mit mapResponse. Ansonsten schließt RxJS das Observable und weitere Events werden nicht mehr vom Effekt behandelt. Wie üblich gilt es, diese Fehler auch auf der Ebene zu behandeln, auf der sie aufgetreten sind. Im gezeigten Fall ist das innerhalb von switchMap, was zu einer weiteren Verschachtelung führt.

Eine Option wie resubscribeOnError beim Global Store, die Fehler automatisch kompensiert, existiert derzeit noch nicht. Das NgRx-Team denkt jedoch darüber nach.

Bleib informiert

Brancheninsights im Newsletter erhalten:

[mc4wp-simple-turnstile]

Store konsumieren

Um den Store zu konsumieren, lassen sich die einzelnen Komponenten in den Dispatcher injizieren und lösen damit das gewünschte Event aus:

this.#dispatcher.dispatch(
  dessertEvents.loadDesserts({
    originalName: this.originalName(),
    englishName: this.englishName(),
  }),
);

Bonus: Redux DevTools

Derzeit kommt der Signal Store mit keiner offiziellen Integration in die Redux DevTools [2], die beim Debuggen einen Blick in den Store erlauben und auch über den Verlauf der einzelnen Zustandsänderungen informieren. Allerdings lässt sich diese Möglichkeit auf der Basis des vom Signal Store gebotenen Erweiterungsmechanismus nachrüsten.

Eine Implementierung dieser Idee findet man im Communitypaket @angular-architects/ngrx-toolkit. Es bietet ein Feature withDevtools, das in den Store aufzunehmen ist (Listing 9).

Listing 9

import { withDevtools } from '@angular-architects/ngrx-toolkit';
[...]

export const DessertStore = signalStore(
  [...]
  withDevtools('DessertStore')
);

Der übergebene String kommt als Name für den Knoten zum Einsatz, der in den Dev Tools den jeweiligen Store repräsentiert. Wurden die DevTools als Browsererweiterung installiert, lassen sich die im Store hinterlegten Daten sowie deren Verlauf in der Developer-Konsole einsehen (Abb. 2).

steyer_kolumne_2.tif
Abb. 2: Redux DevTools für den Signal Store

Zusammenfassung

Das neue Event API für den NgRx Signal Store erlaubt es, leichtgewichtig zu starten und bei Bedarf gezielt Flux-Muster einzuführen. Mit zusätzlichen Tools wie den Redux DevTools lässt sich das Debugging vereinfachen. Damit liefert das Event API eine flexible Brücke zwischen einfacher Signals-Nutzung und erprobten State-Management-Praktiken aus der Redux-Welt.

 

🔍 Frequently Asked Questions (FAQ)

1. Was ist das neue Event API im NgRx Signal Store?
Das Event API erweitert den NgRx Signal Store um Flux-/Redux-Prinzipien. Es ermöglicht, Events, Reducer und Effects einzusetzen, ohne das gesamte Projekt auf komplexes State Management umzustellen.

2. Was kann man mit dem Event API umsetzen?
Mit dem Event API lassen sich Events definieren, die Zustandsänderungen in einem Store auslösen. Dadurch können Entwickler lose gekoppelte Stores, asynchrone Workflows und strukturierte Datenflüsse in Angular-Anwendungen realisieren.

3. Was wird mit Reducern und Effects im Signal Store gemacht?
Reducer reagieren auf eingehende Events und aktualisieren den Zustand, während Effects asynchrone Aktionen wie API-Aufrufe oder Fehlerbehandlung übernehmen und neue Events auslösen.

4. Wer nutzt das NgRx Signal Store Event API?
Das Event API richtet sich an Angular-Entwickler, die die Einfachheit von Signals nutzen, aber gleichzeitig die bewährten Muster aus Redux und NgRx in einzelnen Stores anwenden möchten.

5. Was ist der Vorteil gegenüber reinem Redux?
Im Gegensatz zu einem globalen Redux Store kann das Event API selektiv eingesetzt werden. Entwickler entscheiden selbst, ob ein Store leichtgewichtig bleibt oder Flux-/Redux-Prinzipien nutzen soll.

6. Wie unterstützt das Event API beim Debugging?
Über die Integration mit Redux DevTools lassen sich Zustandsänderungen im Signal Store visualisieren. Dadurch können Entwickler Events und State-Verläufe im Detail nachvollziehen.

7. Was bedeutet das für Angular State Management insgesamt?
Das Event API schafft eine Brücke zwischen einfacher Signal-Nutzung und komplexen NgRx-Workflows. Damit können Angular-Projekte flexibel skalieren – vom schlanken Store bis zum vollwertigen Redux-Ansatz.

The post NgRx neu gedacht: Wie das Event API den Signal Store erweitert appeared first on JavaScript & Angular Days.

]]>
Webentwicklung lernen: Der komplette Weg vom Anfänger zum Profi https://javascript-days.de/blog/javascript/webentwicklung-lernen-einstieg-bis-profi/ Thu, 01 May 2025 13:21:41 +0000 https://javascript-days.de/?p=107185 Die Webentwicklung ist ein sehr weites Feld mit zahlreichen Disziplinen, die es zu meistern gilt. Hier den Überblick zu behalten, ist selbst für erfahrene Entwickler nicht einfach. Noch schwieriger ist es, einen Einstieg zu finden und anschließend den passenden Weg für sich selbst zu wählen. Nur eins ist sicher: Das Lernen endet nie!

The post Webentwicklung lernen: Der komplette Weg vom Anfänger zum Profi appeared first on JavaScript & Angular Days.

]]>
Um eine Webapplikation von Grund auf zu entwickeln, sind viele Fertigkeiten erforderlich. Es beginnt bei der Infrastruktur, auf der die Applikation ausgeführt wird, und geht über das Backend mit der Businesslogik und Datenbanken für die Datenhaltung bis zum Frontend für die User-Interaktion, das im Browser mit HTML, CSS und JavaScript läuft. Hinzu kommen noch verschiedene Protokolle und Schnittstellen, die die einzelnen Systeme miteinander verbinden. Für jede Schicht, aus der eine Applikation besteht, gibt es verschiedene Programmiersprachen, Technologien, Bibliotheken und Frameworks.

Bei der Vielfalt der Möglichkeiten ist eine Kombination aus solidem Grundwissen, praktischer Erfahrung und Überblick wichtig. Eine Schritt-für-Schritt Anleitung oder einen perfekten Pfad vom Junior zum Senior gibt es nicht. Die Entwicklung hängt stark vom persönlichen Lerntyp, dem eigenen Umfeld und nicht zuletzt den Ressourcen ab, die Sie investieren können und wollen.

Einstieg in die Webentwicklung

Für den Einstieg in die schnelllebige Welt der Webentwicklung hat sich ein praxisorientierter Ansatz bewährt. Ein großer Vorteil der Webentwicklung ist, dass Sie sehr schnell greifbare Ergebnisse produzieren können und so Ihren Fortschritt auch sehen können. Dazu sollten Sie sich einen möglichst einfachen Technologiestack wählen und sich eine konkrete Aufgabenstellung suchen. Der Klassiker ist hier die To-do-App, bei der Sie Aufgaben erzeugen, anzeigen, modifizieren und wieder löschen können. Eine solche To-do-App ist nur eines von vielen Beispielen, dabei geht es weniger um das konkrete Thema, sondern vielmehr darum, dass Sie die Standardoperationen einer Webapplikation umsetzen. Diese werden typischerweise unter dem Akronym CRUD zusammengefasst. Es steht für Create, Read, Update und Delete.

Um mit der Entwicklung zu beginnen, benötigen Sie ein wenig Grundwissen über HTML, CSS und JavaScript. Für den Start reicht es jedoch, dass Sie wissen, wie Sie eine Webseite mit HTML strukturieren können, grundlegende Styles wie Ausrichtung, Positionierung und Farben sowie den Umgang mit Text-Styles.

  • Mozilla Developer Network [1]

  • W3Schools [2]

Eine Applikation sollten Sie jedoch nicht direkt auf diesen grundlegenden Technologien umsetzen, zu diesem Zweck gibt es Frameworks. Die meisten Entwickler machen ihre ersten Schritte in einem Framework und nicht mit den zugrunde liegenden Basistechnologien. Der Vorteil der Frameworks ist, dass Sie deutlich schneller Ergebnisse produzieren können, da diese für die meisten alltäglichen Problemstellungen wie Navigation, Formularbehandlung oder Zustandsverwaltung bereits fertige Lösungen bieten. Bei den Frameworks gibt es aktuell eine Handvoll etablierter Ansätze:

  • React [3]

  • Angular [4]

  • Vue [5]

  • Svelte [6]

Jedes dieser Frameworks bietet ein offizielles Tutorial an, das den Einstieg in die Arbeit mit dem Framework erleichtert. Zusätzlich dazu finden sich zahlreiche weitere Ressourcen rund um die Frameworks wie Blogartikel, Bücher und Videotrainings. Ein weiterer Vorteil der Frameworks ist, dass sie alle Werkzeuge mitbringen, die Sie für den Entwicklungs- und Releaseprozess benötigen, und diese auch schon mit einer sinnvollen Konfiguration versehen sind. Die wichtigsten dieser Werkzeuge sind die Bundler, allen voran webpack und Vite.

Zu einem Web-Frontend gehört auch immer ein Backend, das sich um das Speichern der Daten und viele weitere Aspekte wie Validierung, Authentifizierung und nicht zuletzt die serverseitige Businesslogik kümmert. Die Entwicklung von Backend-Applikationen ist eine Welt für sich und teilt sich wiederum in ein breites Spektrum auf. Als Webentwickler sollten Sie zumindest einen Einblick in die Komplexität der Backend-Welt haben. Es gibt auch immer mehr Entwickler, die den Pfad der Fullstack-Entwicklung einschlagen und sowohl im Frontend als auch im Backend unterwegs sind.

Eine naheliegende Wahl für den Einstieg in die Backend-Entwicklung ist die Plattform Node.js, auf der Sie mit JavaScript entwickeln können. Zu Beginn geht es hier vor allem um die Schnittstelle zum Frontend auf der einen Seite und die Anbindung einer Datenbank auf der anderen. Für Schnittstellen ist aktuell das REST-Paradigma etabliert. Es ist ein ressourcenbasierter Ansatz, der die Eigenschaften des HTTP-Protokolls nutzt, um Ressourcen über URL-Pfade abzubilden und mit den HTTP-Methoden die Art des Zugriffs zu beschreiben. Als Datenbanken kommen entweder klassische SQL-Datenbanken wie MySQL oder PostgreSQL, dokumentenorientierte Datenbanken wie MongoDB oder Key-Value-Stores wie Redis in Frage.

Der beste Weg, um Erfahrung in der Entwicklung zu sammeln, führt jedoch nicht über einfache CRUD-Applikationen, sondern über echte Projektarbeit. Im besten Fall arbeiten Sie in einem größeren Team an einer Applikation, dann haben Sie die Gelegenheit, die Technologien im praktischen Einsatz in einem größeren Verbund zu erleben. Der Vorteil ist hier, dass Sie mit verschiedensten Problemstellungen konfrontiert werden, die in einer einfachen Applikation nicht existieren. Das können komplexe Anforderungen des Kunden sein, aber auch Herausforderungen bei der Integration neuer Features in eine bestehende Codebasis oder Schnittstellen, die nicht für den geplanten Anwendungsfall gedacht sind.

In einem solchen Team können Sie von den anderen Teammitgliedern lernen und Ihr Wissen erweitern. Die besten Ansätze sind kooperative Formen der Softwareentwicklung in Form von Pair Programming, bei denen Sie direkt mit anderen Entwicklern zusammen an einem Feature arbeiten, oder Codereviews, bei denen Ihr Quellcode auf den Prüfstand gestellt und hinsichtlich Umsetzung, Integration und Architektur geprüft wird. Wertvoll werden solche Codereviews natürlich erst, wenn Sie auf Fehler aufmerksam gemacht und auch Strategien zur Behebung entwickelt werden.

In der Webentwicklung haben Sie Zugriff auf eine weltweite und sehr aktive Community, mit der Sie sich austauschen können. Dieser Austausch funktioniert sowohl asynchron über Diskussionsforen und Chats als auch synchron beispielsweise auf Konferenzen und anderen Veranstaltungen. Hier erfahren Sie nicht nur alles, was Sie über die neusten Entwicklungen wissen müssen, sondern haben auch Gelegenheiten, sich mit Gleichgesinnten zu unterhalten.

Kontinuierliche Weiterentwicklung auf dem Weg zum Experten

Der Übergang vom Einstieg zum Intermediate oder vom Junior zum Professional ist fließend. Es gibt keine Checkliste, die Sie abhaken können. Sie merken dennoch, dass die Schritte, die Sie machen, immer größer werden, die Probleme immer herausfordernder und Sie generell selbstsicherer werden. Fühlen Sie sich komfortabel in Ihrer Umgebung und mit Ihren Aufgaben, ist es an der Zeit, den nächsten Schritt zu gehen und Ihr Wissen weiter zu vertiefen.

Ein Bereich, der Sie während Ihrer gesamten Entwicklung begleiten wird, sind die Grundlagen der Programmiersprachen und Technologien. Haben Sie Ihre ersten Projekte umgesetzt, geht es an die Vertiefung Ihres Grundwissens. So können Sie sich beispielsweise mit dem korrekten semantischen Aufbau eines HTML-Dokuments beschäftigen. Oder Sie steigen tiefer in das Thema Styling von Web-Frontends mit CSS ein und lernen die Tricks und Kniffe von CSS sowie Werkzeuge wie den Präprozessor SCSS, CSS-Frameworks wie Tailwind oder generelle Konventionen wie BEM.

Auch beim Thema Programmierung gibt es viel zu lernen. Gerade der Bereich JavaScript bietet ein enormes Potenzial. Angefangen mit dem Umgang mit Events und asynchronen Programmflüssen, die Ihnen nahezu überall begegnen, sei es bei der User-Interaktion oder bei der Kommunikation mit einem Backend. Des Weiteren sollten Sie sich mit der Modularisierung Ihres Quellcodes beschäftigen und ihn in Klassen, Funktionen und Module aufteilen und dafür das Modulsystem verwenden. Noch besser als JavaScript ist TypeScript, da es Ihnen zusätzlich zum Funktionsumfang von JavaScript noch Typsicherheit bietet. Damit können Sie die Datentypen für Variablen und Funktionssignaturen festlegen. Der TypeScript Compiler kann aufgrund dieser Informationen Ihren Quellcode überprüfen und weist Sie noch vor der Ausführung Ihrer Applikation auf Probleme hin. Einige Frameworks wie Angular setzen standardmäßig auf TypeScript, andere wie React oder Vue unterstützen sowohl JavaScript als auch TypeScript, wobei Sie in jedem Fall auf die typsichere Variante setzen sollten.

Haben Sie ihre ersten Aufgabenstellungen in der Welt der JavaScript-Frameworks erledigt, gilt es, das Ökosystem um die Frameworks zu meistern. Neben den eigentlichen Frameworks gibt es noch zahlreiche zusätzliche Pakete und Erweiterungen, die Ihnen bei der Entwicklung von Applikationen helfen. Typische Beispiele sind Bibliotheken zum State Management oder Komponentenbibliotheken wie:

  • Redux: zentrales State Management für verschiedene Frameworks [5]

  • NgRx: ein Angular-Framework, das unter anderem reaktives State Management bietet [6]

  • Pina: eine flexible Lösung für zentrales State Management in Vue [7]

  • Material-UI: eine Komponentensammlung für React [8]

  • PrimeVue: eine moderne Komponentenbibliothek für Vue [9]

  • Angular Material: Material-Design-Komponenten für Angular [10]

Mit dem Umfang Ihrer Aufgabenstellungen steigt auch ihre Auswirkung auf das Gesamtsystem und Sie müssen sich immer häufiger auf der architektonischen Ebene der Applikation bewegen. Die Architektur einer Applikation betrifft ihren Aufbau und die Struktur einer Applikation, wie Daten verarbeitet werden und wie die einzelnen Teile miteinander interagieren. Die Architektur beeinflusst die Skalierbarkeit, Wartbarkeit, Sicherheit und Performance des Gesamtsystems ganz entscheidend.

Konkrete Ausprägungen von Architekturmustern sind Event-Systeme zur Kommunikation in und zwischen Applikationsteilen. Eine komponentenbasierte Architektur des Frontends, bei der die grafische Oberfläche aus einer Baumstruktur aus vielen kleinen lose gekoppelten Komponenten besteht, zwischen denen Informationen fließen. Oder Fullstack-Applikationen, bei denen ein Teil der Zuständigkeiten des Frontends zum Server verschoben werden und der Server das eigentlich für den Browser gedachte Framework ausführt und serverseitig die Strukturen für den Client vorbereitet. Dieser Ansatz wird als serverseitiges Rendern (SSR) bezeichnet und ist mittlerweile in allen gängigen Frontend-Frameworks als optionale Erweiterung verfügbar.

Je größer eine Applikation wird, desto wichtiger wird auch die Qualität des Quellcodes. Sie sollten Entscheidungen zu Style und Konsistenz Ihres Quellcodes treffen und Regeln für die Formatierung festlegen. Ein unverzichtbares Hilfsmittel hierbei ist ESlint, ein Werkzeug zur statischen Codeanalyse, das dabei hilft, einen festgelegten Codestandard im Bereich von JavaScript und TypeScript durchzusetzen. Damit Sie nicht jeden Aspekt des Codestyles selbst festlegen müssen, gibt es etablierte Standards wie den AirBnB JavaScript Style, für den es auch ESlint-Regelsätze gibt.

Neben der Codequalität gilt es auch, die Features einer Applikation in einer hohen Qualität abzuliefern. Ein Mittel, um das sicherzustellen, sind automatisierte Tests auf verschiedenen Ebenen. Diese reichen von leichtgewichtigen Unit-Tests bis zu umfangreichen Integrations- und Ende-zu-Ende-Tests. In der Webentwicklung gibt es dafür verschiedene Testframeworks. Im Fall von Unit-Tests können Sie beispielsweise auf Vitest oder Jest zurückgreifen, die sowohl JavaScript als auch TypeScript unterstützen und die Sie unabhängig vom Framework Ihrer Applikation einsetzen können. Für Ende-zu-Ende-Tests kommen Werkzeuge wie Cypress oder Playwright zum Einsatz. Sie schlüpfen in die Rolle der Benutzer Ihrer Applikation und überprüfen die verschiedenen Workflows aus der Browserperspektive.

Generell ist die Automatisierung von wiederkehrenden Aufgaben im Projekt ein wichtiger Faktor, um die Effizienz im Entwicklungsprozess zu steigern. Dabei sind automatisierte Tests ein Baustein, eine solide CI/CD-Pipeline ein weiterer. Diese Pipeline bereitet die Applikation für den Test- und Produktivbetrieb vor, führt automatisierte Überprüfungen durch und deployt die Applikation auf das Zielsystem.

Ein weiteres Thema, das mindestens genauso wichtig ist wie die Qualitätssicherung in einer Applikation, ist Sicherheit. In Webapplikationen haben Sie häufig mit sensiblen Daten Ihrer Nutzer zu tun, die Sie schützen müssen. Die Grundlagen bilden die sichere Übertragung von Daten über verschlüsselte Verbindungen, die Handhabung von Passwörtern und Schlüsseln sowie der Schutz vor gängigen Schwachstellen wie Cross-site Scripting, SQL Injections und Cross-site Request Forgery. Eine gute Anlaufstelle, wenn es um Sicherheit geht, ist OWASP (Open Worldwide Application Security Project). Es veröffentlicht regelmäßig die häufigsten Schwachstellen in Form der OWASP Top 10 [13] und stellt Ressourcen zur Identifikation, Vermeidung und Behebung dieser Schwachstellen zur Verfügung.

Die Ressourcen für die Weiterentwicklung für erfahrenere Entwickler sind deutlich weniger als für den Einstieg. Ein Grund hierfür ist, dass sie deutlich tiefer gehen müssen und auch die Qualitätsansprüche steigen. Dennoch können Sie gerade bei professionellen Trainingsplattformen oder auch auf Workshops bei Konferenzen fündig werden.

Eine weitere gute Möglichkeit zur Weiterentwicklung ist die praktische Arbeit an größeren Projekten zusammen mit anderen Entwicklern. Hier können Sie größere Aufgaben und die Verantwortung für Teilprojekte übernehmen. Zum Üben eignen sich außerdem komplexere Aufgabenstellungen, die Sie unabhängig vom Arbeitsalltag lösen können. Hier eignen sich vor allem spezialisierte Problemstellungen, die aufwendigere Lösungsansätze benötigen. Hier sollten Sie sich vor allem auf eine saubere Architektur konzentrieren.

Setze JavaScript Trends um

16. - 20. März 2026

>

Das Ende der Fahnenstange? Das eigene Wissen weitergeben!

Irgendwann haben Sie dann alles gesehen, was es in der Webentwicklung zu sehen gibt, oder? Dieser Zeitpunkt wird in der Realität niemals eintreten, denn die Problemstellungen und die möglichen Lösungsansätze sind so vielfältig, dass Ihnen garantiert niemals langweilig werden wird. Dennoch haben Sie irgendwann so viel Erfahrung gesammelt, dass Sie für nahezu jedes Problem eine Lösung finden können. Hier wird es dann auch irgendwann schwierig, passende Ressourcen für die Weiterentwicklung zu finden. Normalerweise suchen Sie sich in dieser Karrierestufe Ihre Problemstellungen selbst und entwickeln passende Lösungen dafür.

In Projekten stehen Sie an der Spitze und beteiligen sich maßgeblich an der Gestaltung der Architektur. Sie übernehmen die Verantwortung und sorgen dafür, dass die Qualität des Produkts so hoch wie möglich ist. Meist kümmern Sie sich um die anspruchsvollen Aufgaben und sind die Person, die gefragt wird, wenn man nicht mehr weiterweiß. Das bedeutet, dass Sie mit den wirklich spannenden Problemstellungen im Projekt konfrontiert werden und im Team eine Lösung dafür entwickeln müssen.

Spätestens jetzt ist es an der Zeit, Ihr Wissen an andere weiterzugeben. Das kann in Form eines Chefarchitekten in einem Projekt sein, der weniger erfahrene Kollegen anleitet und ihnen hilft, sich weiterzuentwickeln. Oder man gibt sein Wissen in Form von Schulungen oder anderen, öffentlichen Kanälen weiter. Wenn es darum geht, Wissen zu vermitteln, müssen Sie sich intensiv mit dem jeweiligen Thema auseinandersetzen, um die Zusammenhänge erklären und auf Fragen antworten zu können.

Wie beeinflusst KI unseren Lernpfad?

Die Webtechnologien werden stetig weiterentwickelt und fast im Tagesrhythmus tauchen neue Frameworks und Bibliotheken auf oder verschwinden wieder in der Bedeutungslosigkeit. Und als ob das noch nicht genug wäre, haben wir mit der zunehmenden Popularität und Verfügbarkeit von Generative AI ein weiteres, sehr mächtiges Werkzeug erhalten. Doch wie verändert KI den Arbeitsalltag und wie wirkt sie sich auf die Entwicklung von Personen aus?

Werkzeuge wie ChatGPT, Gemini, Claude, GitHub CoPilot und Cursor machen das Entwicklerleben deutlich angenehmer, da Sie damit einfache und sich wiederholende Aufgaben gut auslagern können. Sie können sich Datenbankstrukturen, Codeblöcke und ganze grafische Oberflächen von KI-Werkzeugen generieren lassen. Auch in der Wissensvermittlung spielen diese Werkzeuge eine immer größere Rolle. Dennoch ist bei allen Vorzügen auch Vorsicht geboten. Zum einen sind die KI-Tools noch mehr Werkzeuge, die Sie lernen müssen. Zum anderen beschleunigt die KI die Entwicklung noch weiter und erhöht die Menge an Informationen. Was noch erschwerend hinzu kommt: Sie können den Informationen nicht uneingeschränkt vertrauen. Das gilt sowohl für die Antworten auf Ihre Prompts in einem Chatinterface als auch beim generierten Quellcode.

Bleib informiert

Brancheninsights im Newsletter erhalten:

[mc4wp-simple-turnstile]

Fazit

Die Webentwicklung ist ein spannendes Betätigungsfeld mit sehr vielen Facetten und Möglichkeiten. Diese reichen vom Backend-Spezialisten über den Full-Stack-Allrounder bis hin zum UI- und UX-Experten.

Es ist auch nicht so, dass Sie einen einmal eingeschlagenen Pfad für Ihre gesamte Karriere beibehalten müssen. Sie können sich jederzeit umorientieren und andere Bereiche der Webtechnologien erkunden. Das Zusammenspiel der einzelnen Systeme und die architektonischen Prinzipien helfen Ihnen dabei.

Allerdings ist es unerlässlich, dass Sie sich in der schnell voranschreitenden IT-Welt kontinuierlich weiterentwickeln und am Ball bleiben, da Sie sonst schnell den Anschluss an die aktuellen Trends und Entwicklungen verlieren.

 

🔍 Frequently Asked Questions (FAQ)

1. Was ist Webentwicklung?
Webentwicklung bezeichnet den Prozess, Webseiten und Webanwendungen zu erstellen. Dazu gehören Frontend-Technologien (HTML, CSS, JavaScript) für die Nutzeroberfläche, Backend-Logik für Server und Datenbanken sowie Schnittstellen und Protokolle, die die Kommunikation ermöglichen.

2. Wer kann Webentwicklung lernen?
Jeder mit Interesse an Programmierung und digitaler Technik kann Webentwicklung erlernen — vom Quereinsteiger über Studenten bis hin zu Profis aus anderen IT-Bereichen. Einsteiger sollten einfache Projekte starten und Schritt für Schritt Wissen aufbauen.

3. Wofür braucht man HTML, CSS und JavaScript im Web?

  • HTML strukturiert den Inhalt einer Webseite (z. B. Überschriften, Absätze).
  • CSS sorgt für das Layout und das Design (Farben, Abstände, Positionierungen).
  • JavaScript macht Webseiten interaktiv (Eingaben, Dynamik, API-Kommunikation).

4. Was sind Frameworks und warum benutzt man sie?
Frameworks wie React, Angular, Vue oder Svelte bieten vordefinierte Strukturen, Komponenten und Tools, mit denen Entwickler schneller und effizienter komplexe Webanwendungen bauen können. Sie reduzieren Boilerplate-Code und bringen Best Practices mit.

5. Was unterscheidet Backend- von Frontend-Entwicklung?

  • Frontend befasst sich mit dem, was der Nutzer sieht und womit er interagiert (z. B. Website im Browser).
  • Backend liegt auf dem Server: Logik, Datenbankzugriff, Authentifizierung, API-Endpoints.

Ein Full-Stack-Entwickler verbindet beide Bereiche.

6. Warum ist es sinnvoll, mit echten Projekten zu lernen?
Echte Projekte bringen reale Herausforderungen mit: Integration mehrerer Technologien, Fehlerbehandlung, Architektur, Arbeitsaufteilung im Team. Das hilft, theoretisches Wissen in praktisches Können zu verwandeln und Routine aufzubauen.

7. Wie beeinflusst KI das Lernen in der Webentwicklung?
KI-Tools wie ChatGPT, GitHub Copilot oder ähnliche können Code-Snippets liefern, Routinetasks automatisieren und als Lernhilfe dienen. Aber: Sie ersetzen nicht das Verständnis. Kritisches Hinterfragen, eigenes Debugging und Qualitätskontrollen bleiben essenziell.

Links & Literatur

[1] Mozilla Developer Network: https://developer.mozilla.org/en-US/docs/Learn_web_development

[2] W3Schools: https://www.w3schools.com

[3] React: https://react.dev

[4] Angular: https://angular.dev

[5] Vue: https://vuejs.org

[6] Svelte: https://svelte.dev

[7 ]Redux: https://redux.js.org

[8] NgRx: https://ngrx.io

[9] Pina: https://pinia.vuejs.org

[10] Material-UI: https://mui.com

[11] PrimeVue: https://primevue.org

[12] Angular Material: https://material.angular.io

[13] OWASP Top 10: https://owasp.org/www-project-top-ten

The post Webentwicklung lernen: Der komplette Weg vom Anfänger zum Profi appeared first on JavaScript & Angular Days.

]]>
Streaming Resources in Angular 19.2 – Datenströme mit dem Resource API https://javascript-days.de/blog/angular/streaming-resources-in-angular-19-2-datenstrome-mit-dem-resource-api/ Mon, 03 Mar 2025 14:11:10 +0000 https://javascript-days.de/?p=107106 Mit den Streaming Resources dringen das Resource API sowie Signals in Bereiche von RxJS vor – sie erlauben es, kontinuierliche Datenströme darzustellen. Neben den Gemeinsamkeiten zu RxJS gibt es jedoch auch einige nicht offensichtliche, jedoch besonders wichtige semantische Unterschiede, die es zu berücksichtigen gilt.

The post Streaming Resources in Angular 19.2 – Datenströme mit dem Resource API appeared first on JavaScript & Angular Days.

]]>
Heute gehen wir anhand von zwei Beispielen auf die Details rund um Streaming Resources sowie auf die semantischen Feinheiten und das Zusammenspiel mit RxJS ein. Den Quellcode finden Sie unter [1].

Beispielanwendungen

Um die Nutzung von Streaming Resources zu demonstrieren, kommen hier zwei Beispiele zum Einsatz. Das erste ist quasi das „Hello World“ der reaktiven Programmierung: ein einfacher Timer, der im Sekundentakt hochzählt (Abb. 1).

steyer_kolumne_1
Abb. 1: Ein einfacher Timer auf Basis einer Streaming Resource

Das Schöne an diesem sehr einfachen Beispiel ist, dass es bereits vieles enthält, an dem die kleinen, jedoch wichtigen Feinheiten von Streaming Resources diskutiert werden können. Eine Variante dieses Beispiels kommt auch zum Einsatz, um die Interoperabilität mit RxJS zu zeigen.

NEU! Generative AI für Angular-Entwickler:innen

Erlebe das neue Bootcamp vom 19. – 20. März 2026

Das zweite Beispiel ist ein wenig praxisnäher. Es handelt sich dabei um einen einfachen WebSockets-basierten Chat auf der Basis einer Streaming Resource (Abb. 2).

steyer_kolumne_2
Abb. 2: Ein einfacher Chat auf Basis einer Streaming Resource

Dieses Chatbeispiel unterscheidet sich technisch gesehen in einem wichtigen Punkt vom Timer: Hier sind zum selben Zeitpunkt alle empfangenen Nachrichten und nicht nur die letzte von Bedeutung. Das stellt eine kleine Herausforderung in der Signal-basierten Welt dar, die sich allerdings lösen lässt.

Als Chatserver kommt ein Fork eines Beispielprojektes aus dem Mozilla Developer Network zum Einsatz [2]. Dieser Node-basierte Server liegt dem Beispielprojekt [1] bei und die readme.md zeigt, wie er sich starten lässt.

Anatomie einer Streaming Resource

Im Gegensatz zu herkömmlichen Resources liefert der Loader einer Streaming Resource immer die folgende Struktur zurück:

PromiseLike<Signal<
  { value: T; } 
  | { error: unknown;}>>;

Es handelt sich hierbei um ein Promise mit einem Signal, das ein Objekt vorhält. Das Objekt verweist entweder auf den zuletzt veröffentlichten Wert (value) oder auf einen aufgetretenen Fehler. Dadurch, dass das Signal im Verlauf der Zeit mehrere Werte bzw. Fehlerzustände annimmt, repräsentiert es einen Datenstrom.

Um in weiterer Folge den Programmcode zu vereinfachen, definiere ich für den Inhalt dieses Signals einen Typ namens StreamItem (Listing 1). Angular bietet derzeit dafür keinen expliziten Typ an. Das soll sich aber im Laufe der weiteren Entwicklung noch ändern.

Listing 1

export type StreamItem<T> =
  | {
      value: T;
    }
  | {
      error: unknown;
    };

Um eine Streaming Resource zu definieren, nutzt der Programmcode die Funktion resource, die auch bei herkömmlichen Resources zum Einsatz kommt. Anstatt eines Loaders bekommt diese Funktion jedoch einen Streaming Loader über die Eigenschaft stream übergeben (Listing 2). Da dieses Beispiel das Schlüsselwort async nutzt, ergibt sich das Promise, das das Signal bereitstellt, implizit.

Listing 2

const myResource = resource({
  request: requestSignal,
  stream: async (params) => {

    // 1. Create Signal representing the Stream
    const result = signal<StreamItem<number>>({ 
      value: 4711 
    });

    // 2. Set up async logic updating the Signal
    […]

    // 3. Set up clean-up handler triggered by AbortSignal
    params.abortSignal.addEventListener('abort', () => {
      […]
    });

    // 4. Return Signal
    return result;
  },
});

Typischerweise lässt sich ein Streaming Loader in vier Teile untergliedern, die im gezeigten Beispiel mit Kommentaren angedeutet sind:

  1. Zunächst erzeugt der Streaming Loader ein neues Signal, das den Datenstrom repräsentiert. Dieses Signal bekommt einen Initialwert.

  2. Danach startet der Streaming Loader eine asynchrone Operation, die im Laufe der Zeit mehrere Ergebnisse liefert. Diese Ergebnisse veröffentlicht er nach und nach über das Signal.

  3. Der Streaming Loader ist auch für das Bereitstellen einer Aufräumlogik verantwortlich. Diese beendet die zugrundeliegende asynchrone Operation, wenn ihre Werte nicht mehr benötigt werden.

  4. Am Ende liefert der Streaming Loader das Signal zurück.

Für das Bereitstellen der Aufräumlogik nutzt der Streaming Loader das vom Resource API bereitgestellte AbortSignal, das sich im übergebenen Parameterobjekt findet. Interessanterweise entsprechen diese Punkte mehr oder weniger auch der typischen Vorgehensweise bei der direkten Nutzung des Observable-Konstruktors in der RxJS-Welt.

switchMap-Semantik beim Übergang zwischen Streams

Die definierte Aufräumlogik läuft immer dann, wenn die Anwendung den aktuellen Datenstrom nicht mehr benötigt. Dafür gibt es zwei Gründe: Der erste tritt dann ein, wenn Angular den Building Block zerstört, der die Resource beherbergt. Stellen wir uns dazu eine Komponente mit einer Resource vor. Beim Verlassen zerstört der Router diese Komponente und im Rahmen dessen zerstört Angular auch die Resource.

Der zweite Grund ergibt sich, wenn sich das request-Signal der Resource ändert. Jede Änderung triggert den Streaming Loader und dieser liefert daraufhin einen neuen Stream. Beim Übergang zwischen diesen Streams nutzt das Resource API somit dieselbe Semantik wie switchMap in RxJS (Abb. 3).

steyer_kolumne_3
Abb. 3: switchMap-Semantik beim Übergang zu einem neuen Stream

Das bedeutet, dass die Resource immer nur den neuesten Stream konsumiert. Diese Vorgehensweise ist die gleiche, die in der Regel beim Laden von Daten zum Einsatz kommt. Wie generell bei Signals in Angular geht es also auch hier darum, für Standardfälle einfache Konzepte anzubieten. Für komplexere Szenarien kann die Anwendung auf Bibliotheken wie RxJS zurückgreifen.

Ein einfacher Resource-basierter Timer

Nachdem wir nun den prinzipiellen Aufbau einer Streaming Resource besprochen haben, möchte ich die konkrete Nutzung anhand eines ersten Beispiels demonstrieren: Ein Timer, der eine Zahl in einem bestimmten Intervall hochzählt. Listing 3 zeigt diese timerResource aus Sicht eines Konsumenten.

Listing 3

@Component([…])
export class TimerResourceComponent {
  ResourceStatus = ResourceStatus;

  startValue = signal(0);
  timer = timerResource(1000, this.startValue);

  forward(): void {
    this.startValue.update((v) => nextSegment(v));
  }
}

function nextSegment(currentValue: number): number {
  return Math.floor(currentValue / 100) * 100 + 100;
}

Der Timer liefert einen Stream, der beim übergebenen Startwert zu zählen beginnt. Der erste Parameter von timerResource repräsentiert das gewünschte Intervall in Millisekunden. Den Startwert repräsentiert das Signal startValue. Immer wenn er sich ändert, wechselt der Timer zu einem neuen Stream. Um diesen Umstand zu demonstrieren, springt die Methode forward zum nächsten vollen Hunderter, also z. B. von 17 auf 100 oder von 123 auf 200.

Erlebe Angular Trends live

16. - 20. März 2026

>

Factory für die Streaming Resource

Um den Einsatz der Streaming Resource für den Timer zu vereinfachen, handelt es sich bei der Funktion timerResource lediglich um eine Factory (Listing 4).

Listing 4

export function timerResource(
  timeout: number,
  startValue: () => number
): ResourceRef<number | undefined> {

  const request = computed(() => ({
    startValue: startValue(),
  }));

  const result = resource({
    request: request,
    stream: async (params) => {
      const counter = params.request.startValue;
      […]
    }
  });

  return result;
}

Diese Factory nimmt das gewünschte Intervall (timeout) sowie ein Signal mit dem Standardwert entgegen. Bei diesem Signal ist timerResource nur am Getter interessiert. Deswegen typisiert sie das Signal mit () => number. Der Rückgabewert ist eine ResourceRef<number | undefined>. Der Typparameter entspricht den Werten im Stream. Da die Resource den Stream asynchron erzeugen kann, setzt sie initial diesen Wert auf undefined. Um undefined zu vermeiden, wird Angular künftig auch die Möglichkeit bieten, einen eigenen Initialwert festzulegen.

Das Computed Signal request repräsentiert alle Parameter, die den Loader triggern. Da im besprochenen Beispiel lediglich startValue als Trigger zum Einsatz kommt, fühlt sich das Computed Signal ein wenig wie ein Durchlauferhitzer an. Trotzdem halte ich gerne an dieser Vorgehensweise fest, zumal sie es erlaubt, später den Trigger zu erweitern und den Namen startValue auch innerhalb des (Streaming) Loaders verfügbar macht: Dieser kann im diskutierten Fall über param.request.startValue den aktuellen Wert beziehen.

Streaming Loder für den Timer

Der Aufbau des Streaming Loaders entspricht den oben diskutierten vier Abschnitten (Listing 5). Die asynchrone Operation zum Hochzählen implementiert der Loader mit der althergebrachten setInterval-Funktion von JavaScript. Um das Verhalten von Fehlerzuständen zu demonstrieren, meldet der Timer bei den Werten 7 und 13 einen Fehler – abergläubige Benutzer:innen kommen somit auch auf ihre Kosten.

Listing 5

const result = resource({
  request: request,
  stream: async (params) => {
    let counter = params.request.startValue;

    // 1. Create Signal representing the Stream
    const resultSignal = signal<StreamItem<number>>({
      value: params.request.startValue,
    });

    // 2. Set up async logic updating the Signal
    const ref = setInterval(() => {
      counter++;
      console.log('tick', counter);

      if (counter === 7 || counter === 13) {
        resultSignal.set({ error: 'bad luck!' });
      } else {
        resultSignal.set({ value: counter });
      }
    }, timeout);

    // 3. Set up clean-up handler triggered by AbortSignal
    params.abortSignal.addEventListener('abort', () => {
      console.log('clean up!');
      clearInterval(ref);
    });

    // 4. Return Signal
    return resultSignal;
  },
});

Durch das asynchrone Verhalten von setInterval findet Schritt 4 vor der ersten Ausführung des Callbacks in Schritt 2 statt. Das bedeutet, dass der Loader zunächst das Signal mit seinem Initialwert zurückliefert und erst dann das Signal nach und nach neue Werte erhält.

Die Streaming Resource ausprobieren

Beim Ausprobieren des Beispiels sieht man zunächst, wie das Beispiel den Zähler im Sekundentakt inkrementiert. Anstatt der Werte 7 und 13 meldet es einen Fehler. Im Gegensatz zu RxJS beendet solch ein Fehler den Stream jedoch nicht. Sobald der Streaming Loader einen neuen Wert liefert, stellt die Resource ihn bereit.

Abbildung 4 zeigt die Konsolenausgaben des Loaders. Sie unterstreichen, dass die Streaming Resource zu einem Zeitpunkt lediglich den aktuellen Stream nutzt und sich somit die switchMap-Semantik ergibt.

steyer_kolumne_4
Abb. 4: Übergang zu neuen Streams

Kurz vor jeder Ausgabe von „clean up!“ hat der User die Funktion forward ausgelöst. Dies führt zu einem neuen Wert in startValue und dieser Umstand triggert den Loader erneut. Die Resource benachrichtigt das AbortSignal des alten Streams, den ihr abort-Handler beendet. Fortan nutzt die Resource den vom Loader gelieferten neuen Stream, der in unserem Beispiel mit dem nächsten Hunderter fortsetzt.

Interop mit RxJS und Observables

Gemeinsam mit dem Resource API führte Angular 19 auch die rxResource ein, die den Brückenschlag zur RxJS-Welt erlaubt. Ab Angular 19.2 sind rxResources immer Streaming Resources. Das bedeutet, eine rxResource liefert nach und nach die Werte des vom Loader zurückgelieferten Observables. Um dieses Verhalten zu demonstrieren, zeigt Listing 6 eine Variation des zuvor besprochenen Timers auf Basis von rxResource.

Listing 6

export function timerResource(
  timeout: number,
  startValue: () => number
): ResourceRef<number | undefined> {

  const request = computed(() => ({
    startValue: startValue(),
  }));

  return rxResource({
    request: request,
    loader: (params) => {
      const startValue = params.request.startValue;
      return interval(timeout).pipe(
        map((v) => v + startValue + 1),
        startWith(startValue),
        tap((v) => console.log('counter', v)),
        switchMap((value) => {
          if (value === 7 || value === 13) {
            return throwError(() => 'bad luck');
          }
          return [value];
        })
      );
    },
  });
}

Diese Implementierung ist dank der zahlreichen Operatoren, die RxJS anbietet, etwas kürzer. Prinzipiell ließen sich analoge Hilfsfunktionen auch für Signals und Resources bereitstellen. Allerdings habe ich das Gefühl, dass der Trend in der Signals-Welt eher in Richtung anwendungsfallbezogener und somit grobgranularer Building Blocks wie timerResource geht. Die Zeit wird zeigen, ob sich dieser Eindruck bewahrheitet.

Wichtige Details beim Einsatz von Streams mit rxResource

Ein kleines Detail bei der rxResource ist die Tatsache, dass die Anwendung den Streaming Loader nicht über die Eigenschaft stream, sondern über loader bereitstellt. Das mag der Tatsache geschuldet sein, dass ein Loader einer rxResource immer ein Streaming Loader ist. Möchte die Anwendung, wie in der ursprünglichen Implementierung in Angular 19.0 und 19.1 lediglich den ersten Wert des Observables nutzen, muss sie nun auf Operatoren wie first oder take(1) zurückgreifen.

Ein weiterer wichtiger Unterschied ergibt sich beim Auftreten eines Fehlers. Hier prallen die Semantiken von Observables und Resources aufeinander. Ein Observable schließt automatisch beim ersten unbehandelten Fehler. Aus diesem Grund erholt sich das Observable in der rxResource nach dem ersten Fehler, der in unserem Beispiel anstatt des Wertes 7 auftritt, nicht mehr. Allerdings kann sich eine rxResource aus dem Fehlerzustand retten, indem es auf einen neuen Stream wechselt. Dazu muss sich zum Beispiel das request-Signal ändern, das den Loader erneut anstößt, dass dieser ein neues Observable liefert.

Wie gewohnt kommt beim Wechsel auf neue Observables die switchMap-Semantik zum Einsatz. Das heißt, dass auch die rxResource zu einem Zeitpunkt lediglich das neueste Observable nutzt. Vorgänger schließt sie, indem sie sich abmeldet (unsubscribe).

Streaming Resources für einen Chat mit WebSockets

Das bis jetzt betrachtete Minimalbeispiel war bereits umfangreich genug, um die Nutzung von Streaming Resources und ihre semantischen Details zu betrachten. Nun möchte ich jedoch einen Schritt weiter gehen und einen etwas praxisnäheren Anwendungsfall zeigen. Dabei handelt es sich um einen einfachen Chatclient (Abb. 2), der es erlaubt, weitere Details zu diskutieren.

Der Angular-Client initialisiert den Chat mit der Factory chatConnection. Sie liefert eine Struktur, die die einzelnen Chatnachrichten als Streaming Resource repräsentiert und daneben noch weitere Signals sowie eine send-Methode anbietet (Listing 7).

Listing 7

@Component([…])
export class ChatResourceComponent {
  ResourceStatus = ResourceStatus;

  userName = signal('');
  chat = chatConnection('ws://localhost:6502', this.userName);
  messages = computed(() => this.chat.resource.value() ?? []);

  userNameInField = linkedSignal(() => this.chat.acceptedUserName());
  currentMessage = signal<string>('');

  send() {
    this.chat.send(this.currentMessage());
    this.currentMessage.set('');
  }

  join() {
    this.userName.set(this.userNameInField());
  }
}

Das an chatConnection übergebene userName-Signal triggert die Resource und führt zum Verbindungsaufbau mit dem Chatserver. Die Signals userNameInField und currentMessages sind an Eingabefelder gebunden. Um eine Verbindung aufzubauen, schreibt die Funktion join den Wert von userNameInField in das Signal userName.

Der Server kann den gewünschten Benutzernamen korrigieren, um für Eindeutigkeit zu sorgen. Deswegen ist userNameInField mit linkedSignal implementiert. Dieses Linked Signal überschreibt den lokalen Wert mit dem korrigierten Wert vom Server, der sich im Signal accpetedUserName befindet.

Möchten Benutzer:innen die aktuelle Nachricht absenden, übergibt die Anwendung die currentMessage an die send-Methode. Daraufhin setzt sie die currentMessage zurück, damit Benutzer:innen die nächste Nachricht erfassen können.

 

Glitch-free Property als Herausforderung bei Streaming Resources

Das Chatbeispiel unterscheidet sich in einem essenziellen Aspekt vom zuvor besprochenen Timer: Während beim Timer immer nur die neueste Nachricht aus dem Stream von Interesse war, sind es beim Chat sämtliche bisher empfangene Nachrichten oder zumindest alle Nachrichten aus einem definierten Zeitfenster. Anders ausgedrückt repräsentiert der Timer eher ein Objekt, das sich im Laufe der Zeit ändert, während der Chat hingegen mehrere Events mit Nachrichten repräsentiert, die die Anwendung im Laufe der Zeit empfängt und in Ihrer Gesamtheit zu berücksichtigen hat.

Die Art und Weise, wie wir hier den Timer betrachten, passt ganz gut zu Signals, zumal Signals jeweils den neuesten Wert wiederspiegeln. Hier passt auch die Glicht-free-Eigenschaft, die unnötige Zwischenwerte ignoriert: Würde die Resource den Timer in einem Atemzug, also in einem einzigen Task, von 0 auf 1, von 1 auf 2 und von 2 auf 3 ändern, sähe der Konsument der Resource nur den Wert 3.

Genau dieses Verhalten wäre jedoch bei einer Event-basierten Nutzung wie beim Chat fatal. Hier gingen einzelne Nachrichten verloren. Dieses Verhalten lässt sich leicht testen, indem man den Chatserver veranlasst, dieselbe Nachricht hintereinander mehrfach an denselben Client zu senden. Die meisten dieser Wiederholungen würde bei einer Implementierung, die lediglich die zuletzt empfangene Nachricht berücksichtigt, verloren gehen.

Diese Überlegung zeigt, dass Signals im Gegensatz zu Observables in RxJS nicht wirklich gut für das Repräsentieren von Events bzw. Nachrichten geeignet sind. Das stellt uns beim Einsatz von Streaming Resources vor eine Herausforderung, die sich glücklicherweise durch eine einfache Anpassung unserer Sichtweise bewältigen lässt: Lassen wir die Resource nicht nur den aktuellen Wert repräsentieren, sondern alle bisher empfangenen Nachrichten oder zumindest alle Nachrichten eines für uns interessanten Zeitfensters, können wir diese Gesamtheit an Nachrichten wieder als Wert betrachten, der sich im Laufe der Zeit ändert.

Anders ausgedrückt müssen wir das Sammeln und das Verwalten der einzelnen Nachrichten in die Resource verschieben. Diese Sichtweise passt auch gut zu meiner weiter oben erwähnten Beobachtung, wonach in der Signals-Welt eher anwendungsfallspezifische und somit grobgranulare Buildings Blocks zum Einsatz kommen.

Typen für Nachrichten

Für die einzelnen Nachrichten, die der Client mit dem Chatserver austauscht, legt das Beispiel einige Typen fest (Listing 8).

Listing 8

export type ChatRequest =
  | {
      type: 'username';
      id: number;
      name: string;
    }
  | {
      type: 'message';
      id: number;
      text: string;
    };

export type ChatResponse =
  | {
      type: 'id';
      id: number;
    }
  | {
      type: 'username';
      id: number;
      name: string;
    }
  | {
      type: 'message';
      id: number;
      name: string;
      text: string;
    };

Das Protokoll zwischen Client und Server gestaltet sich wie folgt:

  1. Der Client baut eine WebSockets-Verbindung auf.

  2. Der Server reagiert mit einer ChatResponse vom Typ id, über die der Client eine eindeutige Session-ID erhält.

  3. Der Client übersendet einen ChatRequest vom Typ username, mit dem er über den Namen des Benutzers informiert.

  4. Der Server bestätigt diesen Benutzernamen mit einer ChatResponse vom Typ username. Falls der gewünschte Benutzername schon vergeben ist, erhält der Client über diese Nachricht einen korrigierten Benutzernamen, den der Server durch Anhängen einer Zahl an den Wunschnamen bildet.

  5. Der Client übersendet ChatRequests mit seinen Nachrichten (Typ message). Der Server verteilt diese Nachricht in Form einer ChatResponse des Typs message an alle Clients.

Repräsentation des Chats

Die Verbindung mit dem Chat repräsentiert das Beispiel mit dem Typ ChatConnection (Listing 9).

Listing 9

export type SendFn = (message: string) => void;

export type ChatConnection = {
  resource: ResourceRef<ChatResponse[] | undefined>;
  connected: () => boolean;
  acceptedUserName: () => string;
  send: SendFn;
};

Das Herzstück der ChatConnection ist eine Resource, die sämtliche bisher empfangene Nachrichten in Form eines Arrays repräsentiert. Daneben liefert sie noch weitere Statusinformationen als Signals. Dazu gehört der Verbindungszustand (connected) und der ggf. vom Server korrigierte Benutzername (accpetedUserName). Die Methode send erlaubt es dem Chat, Nachrichten zum Server zu senden. Den übergebenen String bildet die Implementierung auf einen ChatRequest vom Typ message ab.

 

Factory für Chat

Die Factory für den Chat ist ähnlich wie die für den zuvor besprochenen Timer aufgebaut. Allerdings bereitet sie anfangs ein paar zusätzliche Variablen vor (Listing 10). Zu diesen Variablen gehört die connection, die die vom Streaming Loader aufgebaute WebSockets-Verbindung widerspiegelt, die zuvor erwähnten Signals connected und accpetedUserName sowie ein Signal id, das die aktuelle Usersitzung repräsentiert. Am Ende liefert die Factory ein paar dieser Signals sowie die erzeugte Streaming Resource und die Methode send als ChatConnection zurück. Das Signal id veröffentlicht sie nicht, da es sich dabei um ein Implementierungsdetail handelt.

Listing 10

export function chatConnection(
  websocketUrl: string,
  userName: () => string
): ChatConnection {

  let connection: WebSocket;

  const connected = signal(false);
  const id = signal(0);
  const acceptedUserName = signal('');

  const request = computed(() => ({
    userName: userName(),
  }));

  const chatResource = resource({
    request,
    stream: async (params) => {
      // init web socket connection
      // handle and collect messages
      […]
    },
  });

  const send: SendFn = (message: string) => {
    const request: ChatRequest = {
      type: 'message',
      id: id(),
      text: message,
    };
    connection.send(JSON.stringify(request));
  };

  return {
    connected,
    resource: chatResource,
    acceptedUserName,
    send,
  };
}

Der Streaming Loader der Resource legt sich zunächst ein Array messages zurecht, in dem er die empfangenen Chatnachrichten sammelt (Listing 11). Die restliche Implementierung folgt den zu Beginn besprochenen vier Punkten. Das vorbereitete Signal spiegelt den Stream wider und nimmt das verwaltete Array mit den empfangenen Nachrichten auf.

Listing 11

const chatResource = resource({
  request,
  stream: async (params) => {
    const userName = params.request?.userName;

    let messages: ChatResponse[] = [];

    // 1. Create Signal representing the Stream
    const resultSignal = signal<StreamItem<ChatResponse>>({
      value: messages,
    });

    if (!userName) {
      return resultSignal;
    }

    // 2. Set up async logic updating the Signal
    connection = new WebSocket(websocketUrl, 'json');

    connection.addEventListener('open', (event) => {
      console.log('[open]');
      connected.set(true);
    });

    connection.addEventListener('message', (event) => {
      const value = JSON.parse(event.data) as ChatResponse;
      console.log('[message]', value);

      if (value.type === 'id') {
        id.set(value.id);
        sendUserName(value.id, userName, connection);
      }

      if (value.type === 'username' && value.id == id()) {
        acceptedUserName.set(value.name);
      }

      if (value.type === 'message' || value.type === 'username') {
        messages = [...messages, value];
        resultSignal.set({ value: messages });
      }
    });

    connection.addEventListener('error', (event) => {
      const error = event;
      console.log('[error]', error);
      resultSignal.set({ error });
    });

    // 3. Set up clean-up handler triggered by AbortSignal
    params.abortSignal.addEventListener('abort', () => {
      console.log('clean up!');
      connection.close();
      connected.set(false);
      id.set(0);
      acceptedUserName.set('');
    });

    // 4. Return Signal
    return resultSignal;
  },
});


function sendUserName(id: number, userName: string, connection: WebSocket) {
  const message: ChatRequest = {
    type: 'username',
    id: id,
    name: userName,
  };
  connection.send(JSON.stringify(message));
}

Über die eingerichteten Event Handler empfängt der Client Informationen vom Server. Das message-Event kümmert sich um das weiter oben beschriebene Protokoll. Es ergänzt das Array messages um alle empfangenen Nachrichten. Das muss immutable, also durch eine flache Kopie, erfolgen, damit das Signal die Änderung wahrnimmt.

Das open-Event setzt connected auf true und das error-Event überführt den aktuellen Fehler in den Stream. Das abort-Event des AbortSignals schließt die WebSockets-Verbindung und setzt die verwalteten Eigenschaften auf ihren Ausgangszustand zurück. Am Ende liefert der Streaming Loader wie gewohnt das Signal zurück, das den Stream widerspiegelt.

Zusammenfassung

Streaming Resources ab Angular 19.2 repräsentieren Datenströme, ohne dabei auf RxJS angewiesen zu sein. Der asynchrone Streaming Loader aktualisiert über die Zeit hinweg ein Signal, das den Stream repräsentiert. Das request-Signal der Resource triggert den Streaming Loader, der einen neuen Datenstrom liefert und den alten beendet – das entspricht der aus RxJS bekannten switchMap-Semantik.

Allerdings repräsentieren Signals in Angular immer nur den aktuellen Wert; Zwischenwerte bei Änderungen, die im selben Task aufeinander folgen, werden ignoriert (Glitch-free-Eigenschaft). Das ist für sich ändernde Zustände, wie einen Zähler, gut geeignet, kann jedoch bei Event-Flüssen – wie einem Chat, der sämtliche empfangene Nachrichten anzeigen soll – zu Datenverlusten führen. Die Lösung liegt darin, die (in einem bestimmten Zeitfenster) empfangenen Nachrichten in der Resource zu sammeln und das so erhaltene Array über die Resource zu veröffentlichen. Dieses Array entspricht somit einem Wert, der sich im Laufe der Zeit ändert.

Die rxResource, mit der sich RxJS integrieren lässt, ist nun standardmäßig eine Streaming Resource. Möchte eine Anwendung lediglich den ersten Wert des zugrundeliegenden Observables nutzen, muss sie Operatoren wie first oder take(1) einsetzen. Im Gegensatz zu Observables wird eine Resource, die auf einen unbehandelten Fehler läuft, nicht geschlossen, sondern kann weiterhin Werte veröffentlichen. Da jedoch die rxResource auf einem Observable basiert, kann sich der aktuelle Stream nicht von einem Fehlerzustand erholen. Allerdings ist es auch hier möglich, auf einen neuen Stream zu wechseln, indem die Anwendung den Streaming Loader erneut anstößt.

 

Bleib informiert

Brancheninsights im Newsletter erhalten:

[mc4wp-simple-turnstile]

 

🔍 Frequently Asked Questions (FAQ)

1. Was sind Streaming Resources in Angular 19.2?
Streaming Resources sind eine Erweiterung des Resource API, mit der man kontinuierliche Datenströme darstellen kann — also ein Signal, das über die Zeit hinweg Werte (oder Fehlerzustände) liefert, anstatt nur einmalig.

2. Wozu verwendet man Streaming Resources?
Sie dienen dazu, reaktive, fortlaufende Updates (z. B. Timer, WebSocket-Nachrichten) mit dem Resource-API abzubilden, ohne direkt RxJS-Observables verwenden zu müssen.

3. Wie funktioniert der Streaming Loader unter der Haube?
Ein Streaming Loader erzeugt zunächst ein Signal, das initial einen Wert enthält. Dann startet eine asynchrone Logik, die im Verlauf neue Werte (oder Fehler) über dieses Signal verteilt. Zudem definiert er eine Aufräumlogik, z. B. via AbortSignal, um alte Streams sauber zu beenden.

4. Wie verhält sich der Übergang zwischen verschiedenen Streams (z. B. bei Parameteränderung)?
Wenn sich das request-Signal einer Resource ändert, startet Angular einen neuen Stream und beendet den alten — das entspricht der Semantik von switchMap aus RxJS. Somit wird immer nur der aktuellste Stream konsumiert.

5. Was ist der Unterschied zwischen Streaming Resources und rxResource / Observables?

  • rxResource ist eine Brücke zu RxJS und wird in Angular 19.2 automatisch als Streaming Resource behandelt.
  • Ein wichtiger Unterschied: Bei einem Fehler verhält sich eine Resource anders als ein Observable — ein unbehandelter Fehler schließt bei Resources den Stream nicht, sondern es ist möglich, danach wieder Werte zu liefern.
  • Wenn man mit rxResource nur den ersten Wert eines Observables nutzen will, muss man Operatoren wie first oder take(1) einsetzen.

6. Wie setzen Entwickler Streaming Resources praktisch ein?
Im Artikel werden zwei Beispiele vorgestellt:

  • Ein einfacher Timer, der im Sekundentakt zählt.
  • Ein WebSocket-Chat, in dem alle empfangenen Nachrichten in einem Array gesammelt und als Streaming Resource bereitgestellt werden (nicht nur der letzte Wert).

7. Welche Herausforderungen gibt es bei der Nutzung von Streaming Resources?
Eine Herausforderung ist die Glitch-Free-Eigenschaft von Signals: Wenn mehrere Zwischenwerte in einem Task erzeugt werden, werden nur Endwerte wahrgenommen. Das kann problematisch sein, wenn man ein Ereignis-Streaming (z. B. Chatnachrichten) darstellen will, weil man sonst Nachrichten verlieren könnte. Die Lösung besteht darin, die Nachrichten selbst in der Resource zu sammeln und als Gesamtwert (z. B. Array) zu publizieren.

 

The post Streaming Resources in Angular 19.2 – Datenströme mit dem Resource API appeared first on JavaScript & Angular Days.

]]>
Architektur für skalierbare und wartbare Anwendungen https://javascript-days.de/blog/react-server-components-und-actions/ Tue, 04 Feb 2025 13:41:15 +0000 https://javascript-days.de/?p=107002 React entwickelt sich mit Server Components und Server Actions zu einem leistungsstarken Full-Stack-Framework. Eine featurebasierte Architektur und Komponentenentkopplung sorgen für skalierbare, wartbare Anwendungen. Mit Prisma ORM lassen sich Datenabfragen in React effizient und performant gestalten.

The post Architektur für skalierbare und wartbare Anwendungen appeared first on JavaScript & Angular Days.

]]>
React entwickelt sich durch die Einführung von React Server Components und Server Actions immer mehr zu einem Full-Stack-Framework. Besonders in größeren Projekten kann es dabei schnell zu einer unnötig komplexen Verschachtelung kommen. Dieses Problem können Sie mit einer featurebasierten Architektur lösen, bei der Features voneinander entkoppelt funktionieren. Nur so bleibt Ihr Code übersichtlich und Ihre Anwendung skalierbar und wartbar.

Mit React Server Components (RSC) können Daten direkt aus der Datenbank in das User Interface integriert werden, während Server Actions die Möglichkeit bieten, Daten zurück in die Datenbank zu schreiben. Während diese Funktionen in kleinen Anwendungen oft direkt nebeneinander existieren, können sie in größeren Projekten schnell zu einer komplexen, ungewollten Verschachtelung von vertikalen Features führen. Wir zeigen, wie eine featurebasierte Architektur in React aufgebaut werden kann, um skalierbare und wartbare Anwendungen zu erstellen. In einer solchen Architektur werden Features so weit wie möglich voneinander entkoppelt, sodass sich jede Komponente und ihre Datenzugriffslogik auf ihre spezifische Domäne konzentrieren kann.

Featurebasierte React-Komponenten

In einer typischen Anwendung haben wir eine Datenbank mit mehreren untereinander verknüpften Tabellen. Zum Beispiel enthält eine Bloganwendung Tabellen für UserPost und Comment. Die Tabelle Post enthält einen Foreign Key auf die Tabelle User, während die Tabelle Comment sowohl einen Foreign Key auf User als auch auf Post enthält.

Der Einfachheit halber konzentrieren wir uns auf die Beziehung zwischen den Tabellen Post und Comment und lassen die Tabelle User außer Acht [1]. In Listing 1 ist ein Prisma-Schema für die Datenbankmodelle zu sehen.

Listing 1

// my-blog/prisma/schema.prisma
…
model Post {
  id        String  @id @default(cuid())
  title     String
  content   String
  comments  Comment[]
}

model Comment {
  id        String   @id @default(cuid())
  content   String
  post  Post @relation(fields: [postId], references: [id])
  postIdString
}

In einer React-Komponentenstruktur könnten wir eine Post-Komponente haben, die einen Post zusammen mit seinen Kommentaren anzeigt. Als Serverkomponente könnte die Post-Komponente wie in Listing 2 aussehen, wobei sowohl der Post als auch die zugehörigen Kommentare mit einem SQL Join gemeinsam aus der Datenbank abgerufen werden.

Listing 2

// my-blog/src/features/post/components/post.tsx
import { getPost } from '@/features/post/queries/get-post';

const Post = async ({ postId }: { postId: string }) => {
  const post = await getPost(postId);

  return (
  <div>
    <h1>{post.title}</h1>
    <p>{post.content}</p>
    <ul>
      {post.comments.map((comment) => (
        <li key={comment.id}>{comment.content}</li>
      ))}
    </ul>
  </div>
  );
}

export default Post;

Um unsere Komponenten zu optimieren, könnten wir die Post-Komponente in zwei separate Komponenten aufteilen: eine Post-Komponente, die den Post selbst darstellt, und eine Comments-Komponente, die die Kommentare anzeigt. Der Code in Listing 3 fokussiert jede Komponente auf ein einzelnes Feature, um eine saubere, featurebasierte Architektur zu schaffen.

Listing 3

// my-blog/src/features/post/components/post.tsx
import { Comments } from '@/features/comment/components/comments';
import { getPost } from '@/features/post/queries/get-post';

const Post = async ({ postId }: { postId: string }) => {
  const post = await getPost(postId);

  return (
  <div>
    <h1>{post.title}</h1>
    <p>{post.content}</p>
    <Comments comments={post.comments} />
  </div>
  );
}

Die Aufteilung von Komponenten in kleinere, spezialisierte Einheiten ist eine bewährte Praxis, die zahlreiche Vorteile bietet. In diesem Fall liegt der Schwerpunkt auf der Featurearchitektur, die durch die Entkopplung von Features und die Platzierung jeder Datei in einem eigenen Featureordner zu einer klareren und übersichtlicheren Struktur führt [2].

Featurebasierte Datenabfrage

Lassen Sie uns nun betrachten, wie diese featurebasierte Architektur auf Datenabruffunktionen in React angewendet werden kann. In einem einfachen Ansatz würde beispielsweise die Funktion getPost sowohl den Post als auch die zugehörigen Kommentare in einer einzigen Anfrage aus der Datenbank abrufen. Das könnte wie in Listing 4 bei der Nutzung von Prisma als ORM implementiert werden.

Listing 4

// my-blog/src/features/post/queries/get-post.ts
..
const getPost = async (postId: string) => {
  const post = await prisma.post.findUnique({
  where: { id: postId },
  include: { comments: true },
  });

  return post;
}

Auch hier wollen wir die Datenabfragefunktionen auf ihre jeweilige Domäne konzentrieren. So könnten wir die Funktion getPost in zwei separate Funktionen aufteilen: eine Funktion getPost, die nur die Posts selbst abfragt, und eine Funktion getComments, die nur die Kommentare abfragt.

Auf diese Weise vermeiden wir komplexe Kombinationen von verschachtelten Beziehungen (z. B. getPostWithComments oder getPostWithAuthor) in unseren Datenabfragefunktionen bei einer wachsenden Codebasis.

Anmerkung: Abfragen mit SQL Joins werden sicherlich Teil unserer größeren Anwendung sein – in einigen Fällen sind sie notwendig, um die Performance auf komplexen Seiten zu verbessern. Es bleibt immer ein Performance-Trade-off, die Abfragefunktionen nach Möglichkeit auf einen einzigen Zweck zu beschränken, leichtgewichtig und deskriptiv zu halten.

In Listing 5 sehen wir nun, wie die Funktion getPost implementiert werden kann. Und Listing 6 enthält die getComments-Funktion, die sich in einem eigenen Featureordner befindet.

Listing 5

// src/features/post/queries/get-post.ts
...
const getPost = async (postId: string) => {
  const post = await prisma.post.findUnique({
  where: { id: postId },
  // include: { comments: true },
  });

  return post;
}

Listing 6

// src/features/comment/queries/get-comments.ts
const getComments = async (postId: string) => {
  const comments = await prisma.comment.findMany({
  where: { postId },
  });

  return comments;
}

Durch die Trennung von Komponenten- und Datenabfragefunktionen vermeiden wir zwar das Problem endloser Variationen verschachtelter Beziehungen in unseren Datenabfragefunktionen, allerdings hat diese Trennung den Nachteil, dass wir nun zwei separate, spezialisierte Abfragen benötigen, anstatt einer einzigen (Listing 7). Hier kommt der erwähnte Trade-off zwischen Performance und Maintainability (featurespezialisierte Funktionen, nicht überladene Funktionen, deskriptive Funktionen) zur Geltung.

Listing 7

//my-blog/src/features/post/components/post.tsx
import { getPost } from '@/features/post/queries/get-post';
import { getComments } from '@/features/comment/queries/get-comments';
import { Comments } from '@/features/comment/components/comments';

const Post = async ({ postId }: { postId: string }) => {
  const post = await getPost(postId);
  const comments = await getComments(postId);

  return (
  <div>
    <h1>{post.title}</h1>
    <p>{post.content}</p>
    <Comments comments={comments} />
  </div>
  );
}
export default Post;

Um die Post-Komponente noch weiter von der Comments-Komponente zu entkoppeln, können wir die Datenabfrage direkt in der Comments-Komponente initiieren. Dadurch muss die Post-Komponente keine Informationen über die Kommentare verwalten und übergibt lediglich die postId. Listing 8 zeigt den aktuellen Zustand der Comments-Komponente und Listing 9 zeigt die Post-Komponente, die nun die postId übergibt.

Listing 8

// my-blog/src/features/comment/components/comments.tsx
import { getComments } from '@/features/comment/queries/get-comments';

const Comments = async ({ postId }: { postId: string }) => {
  const comments = await getComments(postId);

  return (
  <ul>
    {comments.map((comment) => (
      <li key={comment.id}>{comment.content}</li>
    ))}
  </ul>
  );
}
export default Comments;

Listing 9

//my-blog/src/features/post/components/post.tsx
import { Comments } from '@/features/comment/components/comments';
import { getPost } from '@/features/post/queries/get-post';

const Post = async ({ postId }: { postId: string }) => {
  const post = await getPost(postId);

  return (
  <div>
    <h1>{post.title}</h1>
    <p>{post.content}</p>
    <Comments postId={postId} />
  </div>
  );
}
export default Post;

Indem wir die Komponenten und ihre Datenabfragefunktionen klar auf ihre jeweilige Funktionalität fokussieren, schaffen wir eine skalierbare React-Anwendung, die sowohl wartungsfreundlich als auch erweiterbar ist.

 

Funktionsbasierte Architektur in React

Die aktuelle Entkopplung hat den Nachteil, dass wir eine sequenzielle Datenabfrage in den Komponenten durchführen. Das bedeutet, dass die Post-Komponente zuerst den Post holt und erst anschließend nach dem Wasserfallprinzip die Comment-Komponente die Kommentare lädt.

Mit anderen Worten, wir haben sequenzielle Datenabrufe in den Komponenten anstatt gleichzeitiger Abfragen. Das ist aus Performancesicht nicht ideal, insbesondere wenn die Kommentare nur die ID des Posts und nicht den gesamten Post selbst benötigen.

Wir hätten dieses Problem schon früher durch gleichzeitige Datenabrufe in der Post-Komponente lösen können, aber der Fokus lag zunächst darauf, die Funktionen so weit wie möglich zu entkoppeln. Jetzt können wir diese Optimierung durchführen. Wir verwenden die Component Composition in React und wenden diese auf die Parent-Komponente der Post-Komponente (in unserem Fall die PostPage-Komponente) an (Listing 10).

Listing 10

// my-blog/src/app/page.tsx
import { Post } from '@/features/post/components/post';
import { getPost } from '@/features/post/queries/get-post';
import { Comments } from '@/features/comment/components/comments';
import { getComments } from '@/features/comment/queries/get-comments';

const PostPage = async ({ postId }: { postId: string }) => {
  const post = await getPost(postId);
  const comments = await getComments(postId);

  return (
  <Post
    post={post}
    comments={<Comments comments={comments} />}
  />
  );
}
export default PostPage;

Im Fall der comments könnten wir in React auch Children verwenden. Unsere Lösung hat den Vorteil, dass wir die Props beschreibend halten und einen Namen wählen können, der klar kommuniziert, worum es sich handelt.

Der Code in Listing 11 zeigt, wie wir von einer sequenziellen Datenabfrage zu einer gleichzeitigen Datenabfrage in der PostPage-Komponente wechseln. Der Komponente Post werden dann post und comments in den Props übergeben (Listing 12). Und zuletzt wird die Komponente Comments per Props mit den comments gefüllt (Listing 13).

Listing 11

// my-blog/src/app/page.tsx
…
const PostPage = async ({ postId }: { postId: string }) => {
  const postPromise = getPost(postId);
  const commentsPromise = getComments(postId);

  const [post, comments] = await Promise.all([
  postPromise,
  commentsPromise,
  ]);

  return (
  <Post
    post={post}
    comments={<Comments comments={comments} />}
  />
  );
}
export default PostPage;

Listing 12

// my-blog/src/features/post/components/post.tsx
type PostProps = {
  post: Post;
  comments: ReactNode;
};

const Post = ({ post, comments }: PostProps) => {
  return (
  <div>
    <h1>{post.title}</h1>
    <p>{post.content}</p>
    {comments}
  </div>
  );
}
export default Post;

Listing 13

// my-blog/src/features/comment/components/comments.tsx
type CommentsProps = {
  comments: Comment[];
};

const Comments = ({ comments }: CommentsProps) => {
  return (
  <ul>
    {comments.map((comment) => (
      <li key={comment.id}>{comment.content}</li>
    ))}
  </ul>
  );
}
export default Comments;

Durch die Kombination einer featurebasierten Architektur mit der Möglichkeit, Daten innerhalb von Komponenten gleichzeitig abzurufen, vereinen wir das Beste aus beiden Welten: einfache Wartbarkeit und Erweiterbarkeit bei optimaler Performance. Besonders vorteilhaft ist die Flexibilität, einzelne Komponenten wie Post oder Comments unabhängig voneinander zu einer Clientkomponente zu machen, ohne dass die andere darunter leidet.

In kleinen React-Projekten mag diese Struktur nicht zwingend notwendig sein, in größeren Anwendungen ist es jedoch entscheidend, Komponenten und ihre zugehörige Logik (z. B. Datenabruf) auf ihre spezifische Domäne zu fokussieren. Dieser Ansatz trägt dazu bei, dass Komponenten innerhalb eines bestimmten Features nicht mit unnötiger oder unzusammenhängender Logik überlastet werden. Anstatt alle Funktionen in einer Komponente zu vereinen (siehe getPostWithComments oder getPostWithAuthor), konzentriert sich jede Komponente auf ihre spezifische Aufgabe. Dadurch bleibt der Code übersichtlich, leichter wartbar und erweiterbar, ohne dass unterschiedliche Verantwortlichkeiten vermischt werden. Das führt zu einer klareren Struktur und verhindert, dass einzelne Komponenten zu komplex oder unübersichtlich werden.

Natürlich gibt es Ausnahmen, wie in diesem Artikel erwähnt. In komplexeren Szenarien, in denen Abfragen mit SQL Joins erforderlich sind, können diese die Performance erheblich verbessern. Es ist jedoch wichtig, das n+1-Problem zu vermeiden. Auf einer Seite wie der individuellen PostPage ist es sinnvoll, nur eine Abfrage für den Post und eine weitere für die Kommentare zu machen. Auf der PostsPage hingegen ist es nicht sinnvoll, für jeden Post eine separate Anfrage für Kommentare zu erstellen, da dies zu unnötigen Datenbankabfragen führen würde. Hier bieten sich Joins als Lösung an – aber nur, wenn es wirklich notwendig ist, alle Kommentare auf einmal abzurufen. In vielen Fällen kann es effizienter sein, diese nur bei Bedarf oder über ein Hidden Pane (versteckter Bereich) zu laden.

Links & Literatur

[1] https://codeberg.org/astrid/entwickler_react_feature_based_architecture

[2] https://www.robinwieruch.de/react-folder-structure

Bleib informiert

Brancheninsights im Newsletter erhalten:

[mc4wp-simple-turnstile]

The post Architektur für skalierbare und wartbare Anwendungen appeared first on JavaScript & Angular Days.

]]>
JavaScript News & Trends in 2024 sowie Zukunftsprognose für 2025 https://javascript-days.de/blog/javascript-trends-und-news-in-2024-zukunftsprognose-fur-2025/ Thu, 12 Dec 2024 16:21:53 +0000 https://javascript-days.de/?p=106915 Was war heiß, was ging daneben, und was hat uns dieses Jahr zum Staunen gebracht? Unsere acht Expert:innen plaudern aus dem Nähkästchen: von KI im Browser über den ewigen React-Angular-Wettlauf bis hin zu Tools, die den Entwicklertag retten. Hier gibt’s Trends, Tipps und den einen oder anderen Lacher – garantiert JavaScript pur!

The post JavaScript News & Trends in 2024 sowie Zukunftsprognose für 2025 appeared first on JavaScript & Angular Days.

]]>
Welche Neuigkeit im JavaScript-Universum war dieses Jahr für dich am spannendsten?

Karsten Sitterberg: Die Einführung des neuen Buildsystems mit esbuild und Vite für Angular war für mich ein echter Meilenstein. Die Geschwindigkeit und Effizienz dieses Systems bringen deutliche Verbesserungen bei der Produktivität in der Entwicklung. Besonders spannend daran finde ich, dass Angular damit eine Technologie integriert, die ursprünglich aus dem Vue-Universum stammt. Neben der zunehmenden Zusammenarbeit zwischen Angular und dem Google-internen Framework Wiz findet sich so ein weiteres großartiges Beispiel dafür, wie Frameworks voneinander profitieren und sich gegenseitig inspirieren können.

Martina Kraus: Eindeutig die Entwicklung der KIs im Webbrowser. WebLLM bietet beispielsweise die Möglichkeit, Large Language Models (LLMs) direkt im Browser zu operationalisieren, ohne dass eine Verbindung zu externen Servern benötigt wird. Dieses API ermöglicht eine direkte Interaktion mit der GPU des Endgeräts, wodurch KI-Modelle erheblich beschleunigt werden können. Daneben wurde auch der neue Webstandard WebNN etabliert. Damit können Web-Applikationen und Frameworks Deep Neural Networks mit GPUs, CPUs oder speziell erstellten KI-Beschleunigern wie NPUs performanter machen. Ich bin selbst keine KI- oder LLM-Expertin, aber ich bin sehr begeistert von den Demos und vor allem der guten Integration dieser APIs in meine Frontend-Applikation. Es gibt so viele Anwendungsfälle, bei denen diese APIs hilfreich sein können. Etwa wenn es darum geht, ein lästiges Formular in einer Web App intelligent ausfüllen zu lassen, indem man schlichtweg einen Text – in dem theoretisch alle Infos enthalten sind – in ein dafür vorgesehenes Textfeld kopiert. Mit Hilfe der KI im Webbrowser können dann alle wichtige Daten herangezogen werden.

David Müllerchen: Das sich alle Frontend Framework Teams zusammengetan haben, um ein Abstract zu Signals zu schreiben. Das fand ich schon sehr nice. Ich bin ja eh sehr viel in Communities unterwegs und freu mich, wenn die Zusammenarbeit größer und die Abstände kleiner werden.

Sebastian Springer: Für mich, wie für viele andere, ist nach wie vor der Siegeszug der KI in alle Entwicklungsbereiche sehr spannend. Wobei es mir da weniger darum geht, dass man überall „AI powered“ draufschreibt, sondern vielmehr darum, dass z.B. die KI im Entwicklungsprozess einen echten Mehrwert bietet. Das hat natürlich auch einen entscheidenden Nachteil: Wenn man wieder einmal mit der Bahn unterwegs ist und die Verbindung so schlecht ist, dass man alle nervigen und sich wiederholenden Aufgaben plötzlich wieder selbst machen muss.

Der zweite Teil der KI in der Webentwicklung, die Integration in die Applikationen, ist für mich genauso wichtig wie die KI als Entwicklungswerkzeug. Mittlerweile hat wahrscheinlich jeder schon einen Chatbot mit dem OpenAI-API gebaut. Aber es geht noch deutlich mehr. Sowohl die Dienste, die man anbinden kann, als auch die Werkzeuge, Frameworks und Bibliotheken werden immer besser, sodass für die Enduser unserer Applikationen ein echter Mehrwert entsteht.

Aber das Leben besteht nicht nur aus KI. Für mich als alten React-Fanboy war 2024 das Jahr von React 19. Es gibt einige sehr sinnvolle Erweiterungen, die in Richtung Fullstack-Entwicklung (mit z. B. Next.js) gehen. Aber auch rein clientseitige Erweiterungen kommen nicht zu kurz. Und das beste Feature, das streng genommen noch nicht einmal React 19 ist: der Compiler, der die unselige manuelle Memoisierung endlich überflüssig macht und uns ohne weiteres Zutun einen Performancegewinn schenkt.

Manfred Steyer: Es war total spannend zu sehen, wie sich die Signal-Story in Angular entwickelt hat. Gerade jetzt mit dem neuen Resource API haben wir zum ersten Mal die Möglichkeit, in der Signal-Welt einen reaktiven Datenfluss end-to-end abzubilden – also von den Benutzereingaben bis hin zur Ausgabe. Die neuen Linked Signals bieten daneben eine lokale Arbeitskopie und sind ein einfacher, aber auch effektiver Weg, um Signals, die von einem Store kommen und readonly sind, an Formularfelder zu binden.

Nils Hartmann: Für das ganze JavaScript-Universum kann ich das nicht beantworten, denn ich bin eher monothematisch im React-Universum unterwegs. Da finde ich es einerseits spannend, dass die serverseitige- bzw. Fullstack-Entwicklung von React-Anwendungen durch React selbst, aber auch durch Next.js in Form von Vercel massiv vorangetrieben wird. Andererseits erfahre ich aber in Gesprächen und Workshops häufig, dass es nach wie vor eine gewisse Skepsis gegenüber den Fullstack-Ansätzen gibt. Und auch wenn das vielleicht eine subjektive Verzerrung durch meine Bubble ist, hätte ich den Wunsch, dass diese Bedenken ernst(er) genommen werden. Glücklicherweise gibt es mit neuen Versionen populärer Bibliotheken (etwa React Router oder TanStack Query) und auch dem gänzlich neuen TanStack Router zumindest aus der Community weiterhin viel Unterstützung für die Entwicklung von Single-Page-Anwendungen mit React.

Lena Rosenboom: Das ganze Jahr hat mich das Thema Barrierefreiheit beschäftigt. Immer mehr Unternehmen merken, dass sie ihre Websites anpassen müssen. Das künftige Gesetz ist zwar keine Neuigkeit, aber für viele Unternehmen kommt es, wie die DSGVO, doch sehr spontan.

Katja Potensky: In JavaScript direkt: Wahrscheinlich die Aufnahme von Shebangs !# in die offizielle Syntax. !# /usr/bin/env node ist schon cool! Im allgemeinen WebDev-Bereich finde ich das Popover API und die immer breiter werdende Abkehr von schwergewichtigen Frameworks gut.

 

Setze JavaScript Trends um

16. - 20. März 2026

>

Wo Licht ist, ist auch Schatten – welche Entwicklung in der WebDev-Welt fandest du dieses Jahr enttäuschend?

Karsten Sitterberg: Meiner Meinung nach hat Angular dieses Jahr an manchen Stellen die Bedürfnisse von Enterprise-Kunden aus den Augen verloren. Innovation und dadurch bedingt die Einführung vieler neuer Konzepte ist zwar spannend und teils auch wichtig, aber neue Konzepte bringen immer auch eine erhebliche kognitive Last mit sich. Vor allem, wenn gleichzeitg auch die älteren Konzepte noch stark im Code vertreten sind, erhöht die Menge der möglichen Programmiermuster zunächst einmal die Hürden für den Einstieg von neuen Entwicklern. Gerade für Teams, die eher auf Stabilität und (Abwäts-)Kompatibilität angewiesen sind, kann das zu einer Herausforderung werden.

Sebastian Springer: Was ich dieses Jahr sehr schade fand, waren die Schwierigkeiten, die das neue Major-Release von React mit sich brachte – beziehungsweise das Nicht-Release. Durch eine Änderung im Verhalten der Suspense-Komponente wurde eine massive Diskussion in der Community vom Zaun gebrochen, die schließlich dazu führte, dass sich das Release um mehrere Monate verzögerte. Andererseits ist es aber auch erfreulich, dass das React-Team auf das Feedback der Community reagiert und eine gute Lösung für alle Beteiligten findet.

Nils Hartmann: Obwohl die Version 19 von React schon im April 2024 angekündigt und in einem ersten Release-Kandidaten vorgestellt wurde, hat es Angular noch geschafft, React in der Versionsnummer zu überholen und seine Version 19 vor der von React herauszubringen 😉 Spaß bei Seite, es ist natürlich schade, dass es mit dem React-Release so lange dauerte, aber immerhin wurden die Probleme, die es mit dem Release-Kandidaten gab, ernst genommen und behoben.

Lena Rosenboom: Enttäuschend finde ich, dass viele große Unternehmen noch immer nicht verstanden haben, dass sie für das BFSG (Barrierefreiheitsstärkungsgesetz) handeln müssen. Außerdem finde ich es auch sehr schade, dass einige Entwickler sich gegen diese Änderungen aussprechen. Dabei wäre die digitale Welt eine bessere, wenn sie alle nutzen könnten.

Martina Kraus: Ich war lange Zeit eine Anhängerin von WebAssembly und habe auch selbst viel auf Konferenzen über WebAssembly gesprochen. Leider findet WebAssembly nur bei größeren Firmen wie Google Twitch oder Amazon Music Anklang. Mittelständische Firmen sehen oftmals keinen Vorteil und fragen sich, warum sie sich neben JavaScript und einer Backend-Technologie noch eine weitere Technologie ins Haus holen sollten. Die Integration von WebAssembly ist meist noch sehr schwierig und es fehlt eine gute Unterstützung der Tools beispielsweise zum einfachen Debuggen einer WebAssembly-Anwendung. Darüber hinaus sind gewisse Probleme wie die Garbage Collection in WebAssembly immer noch nicht vollständig gelöst. Lustigerweise nutzen Web APIs wie WebLLM und WebNN WebAssembly für die Ausführung der Berechnungen und Models – und genau da ergibt WebAssembly auch total Sinn – aber irgendwie schafft es WebAssembly nicht in die Köpfe der Entwickler.

David Müllerchen: Ich hatte irgendwie gehofft, dass das neue Date API schon kommt. Date Handling ist schon ein wilder Ritt, wenn man es nativ mit JavaScript machen will/muss. Ein Funktionsumfang wie day.js oder date-fns würde hier helfen

Katja Potensky: Ein weiteres Jahr ohne Pipeline Operator 🙁

 

 

 

 

Hast du einen Geheimtipp für die Leser – ein Tool, eine Technologie oder einen Produktivitätsbringer, den jeder einmal ausprobieren sollte?

Manfred Steyer: Ich bin derzeit von den Möglichkeiten der forensischen Code-Analyse sehr begeistert. Dabei kombiniert man Code-Metriken mit Informationen aus der Quellcodeverwaltung, z. B. der git history, um versteckte Muster zu entdecken. Mit diesen Mustern lässt sich auch einiges über die Qualität der vorherrschenden Architektur sagen. Und es ist wirklich lustig, Code, an dem man täglich arbeitet, aus dieser neuen Perspektive zu betrachten. Mein aktuelles Open-Source-Projekt Detective [1] bietet ein paar dieser Analysen an.

[1] https://www.npmjs.com/package/@softarc/detective

Karsten Sitterberg: Ich empfehle Testcontainers. Sie ermöglichen es, komplette Integrationstests in isolierten Containern durchzuführen – auch in der JavaScript-Welt perfekt für Datenbanken, APIs und andere Services. Gerade wenn mit komplexen Systemen gearbeitet wird, bieten Testcontainers eine hervorragende Möglichkeit, Tests auf stabile und reproduzierbare Füße zu stellen. Sie ermöglichen es auch, ohne ein komplexes Docker-Compose-Setup die gleiche Umgebung in der Pipeline wie auch lokal aufzubauen. Ein tolles Tool für nachhaltige Qualitätssicherung!

Martina Kraus: Nun, ich glaube es ist kein Geheimtipp mehr, aber ich möchte nochmal auf den GitHub Copiloten aufmerksam machen. Das Geld, das man selbst darin investieren muss, lohnt sich auf jeden Fall. Ich stand dem ganzen anfangs sehr skeptisch gegenüber – vermutlich auch weil der Programm-Code, den ChatGPT generiert hat, oft noch nicht einmal funktionierte, geschweige denn überhaupt kompiliert hat 😀 Die Qualität ist mit dem Copiloten deutlich höher, und ich nutze die Erweiterung für VS Code oder auch WebStorm sehr häufig, um mir schonmal ein Grundgerüst bzw. eine Grundstruktur meines Programms zu generieren. Natürlich ist das immer mit Vorsicht zu betrachten, und der generierte Code sollte immer noch einmal gründlich reviewt werden. Nichtsdestotrotz ist dieses Vorgehen für mich weitaus schneller, als wenn ich alles selbst schreiben müsste.

Lena Rosenboom: Jeder Entwickler sollte hin und wieder mal das Tool Accessibility Insights verwenden. Mit diesem Browser AddOn kann man seine Website auf Barrierefreiheit prüfen und bekommt Tipps, wie man sie verbessern kann.

David Müllerchen: Mein absolutes Lieblingstool ist VSCode. Ich habe noch ein paar wenige Extensions installiert, wie Framework-spezifische Tools (Angular Language Service) und UI Tweaks (Material Icon Theme). Aber ein Highlight ist immer Console Ninja. Das Tool zeigt dir die Konsolenausgaben in Code an. Sehr hilfreich!

Sebastian Springer: Mein persönlicher Geheimtipp (ok, so geheim ist der gar nicht) sind die KI-Helferlein, die die tägliche Arbeit enorm erleichtern. Konkret sind das GitHub Copilot, Continue oder Cursor – je nach persönlicher Vorliebe. Sich wiederholende Aufgaben, Vorschläge und auch (mehr oder weniger) gute Tipps sind Dinge, die sich mit diesen Werkzeugen gut erledigen lassen beziehungsweise die man von ihnen erwarten kann.

Nils Hartmann: Ein echter Geheimtipp ist es wohl nicht mehr, aber wenn Ihr React-Anwendungen baut, solltet Ihr euch die TanStack-Bibliotheken ansehen. Da gibt es viele bekannte Bibliotheken wie TanStack Table oder Query, die schon fast zum De-facto-Standard in React-Anwendungen gehören. Aber es finden sich auch neuere wie Start, Router oder Forms, die mit innovativen Ideen und neuen Konzepten aufwarten. Einige der Bibliotheken sind übrigens auch für andere SPA-Frameworks einsetzbar. Also auch Angular-, Vue- oder Svelte-Fans sollten mal einen Blick darauf werfen.

Katja Potensky: Gleich zwei: https://developer.mozilla.org/en-US/plus/docs/features/updates [2] und – unironisch – https://developer.mozilla.org/en-US/docs/Web/HTML/Element [3]. Einfach mal alle HTML-Elemente durchgehen, bevor man das nächste Mal Angular Material reinzieht.

Dein guter Vorsatz für 2025: Mit welchem Thema möchtest du dich im neuen Jahr besonders beschäftigen?

Sebastian Springer: Die guten Vorsätze für das nächste Jahr: ein bisschen mehr Python machen – schließlich ist das jetzt die Programmiersprache Nummer 1 auf GitHub, dicht gefolgt von JavaScript und TypeScript (jeweils als eigene Programmiersprache). 😉 Aber Spaß beiseite: Gerade was die Arbeit mit KI und Daten angeht hat Python die mit Abstand aktivste Community, und allein deshalb lohnt es sich schon, sich eingehender damit auseinanderzusetzen.

Der zweite gute Vorsatz für 2025 ist, mehr Automatisierung zu betreiben – und zwar in nahezu allen Bereichen: von Maschinen und Geräten über digitale Produkte bis hin zum Entwicklungsprozess. Denn auch hier tut sich gerade sehr viel, und das nicht nur dank vieler neuer KI-basierter Werkzeuge.

Lena Rosenboom: Alles rund um das BFSG wird mich auf jeden Fall beschäftigen, und ich bin gespannt, welche Unternehmen erst einen Monat vorher merken, dass für sie Handlungsbedarf besteht.

Martina Kraus: Da ich in der Web Security tätig bin, werde ich mich wohl weiterhin stark mit passwortloser Authentifizierung beschäftigen. Der FIDO2-Standard bzw. Passkeys sind nach meiner Meinung die Zukunft der sicheren Authentifizierung in Mobile- oder Web-Applikationen. Tatsächlich schreibe ich sogar gerade an einem Buch über Authentifizierung und Autorisierung, daher werde ich mich vermutlich sogar noch tiefer mit Themen wie OAuth 2.1, OpenID Connect und FIDO auseinandersetzen dürfen, als ich ohnehin schon habe.

David Müllerchen: Ich halte ja nichts von Vorsätzen. Ich lass mich gerne treiben und überraschen. Aber ich möchte mehr mit AI-Tools spielen. Die neusten Dinge teste ich erstmal bei mir um Livestream. (Immer montags 20:00 https://webdave.tv [4]).

Manfred Steyer: Ich werde die Neuerungen in Sachen Angular verfolgen und zeigen, wie man damit auch die Architektur einer Anwendung besser mit Leben erfüllen kann. Daneben sind ein paar Neuerungen für Native Federation, das die Umsetzung von Micro Frontends auf Basis von Web Standards erlaubt, geplant.

Karsten Sitterberg: In den letzten zwei Jahren habe ich mich intensiv mit MQTT im Kontext von Angular beschäftigt, vor allem durch verschiedene Kundenprojekte. Für 2025 habe ich mir vorgenommen, dieses Wissen zum einen noch zu vertiefen. Zum anderen möchte ich dieses Wissen auf den Bereich Kollaboration und Schulungen übertragen. Ich plane, das von mir bereits erfolgreich eingesetzte Schulungssystem um innovative Funktionen zu erweitern, um so noch praxisnähere und interaktivere Lernformate anbieten zu können.

Katja Potensky: CSS. Diese Sprache wurde viel mächtiger in letzter Zeit und kann mittlerweile extrem viel JS-Code sparen.

 

Welchen Trend in der Webentwicklungswelt siehst du momentan, der deiner Meinung nach im nächsten Jahr an Fahrt aufnehmen wird?

Nils Hartmann: Ich bin gespannt, ob und wie sich das “Fullstack-Thema” auf JavaScript- bzw. React-Basis weiter durchsetzt und auch in der Masse Verbreitung findet. Mit Remix und dem noch ganz neuen TanStack Start stehen zwei Frameworks parat, die etwas leichtgewichtiger als Next.js sind und den Umstieg von einer klassischen Single-Page-Anwendung etwas einfacher machen.

Manfred Steyer: Ich denke, wir werden immer häufiger über reaktive Programmierung nachdenken, und das wird unsere Sicht auf den Code, den wir schreiben, verändern. Das Thema ist nicht neu. Gerade mit RxJS haben wir schon länger die Möglichkeit, eine Anwendung reaktiv aufzubauen. Da Signals nun aber direkt in viele Frameworks wie Angular einziehen, bekommt das Thema mehr Schwung, und somit wird reaktive Programmierung nicht nur eine Option sondern auch das Standardvorgehen werden.

Martina Kraus: Eindeutig weiterhin das Thema KI im Webbrowser. Die bereits erwähnten Webstandards WebNN und WebLLM benötigen noch etwas Feinschliff, und ich bin überzeugt, dass wir noch ganz am Anfang sind in Sachen KI-Unterstützung im Webbrowser. Mit Googles Gemini kann ich direkt in DevTools mit Gemini chatten und erhalte Informationen darüber, warum gewisse Elemente aus dem DOM-Baum auf eine bestimme Weise dargestellt werden und kann Netzwerkanfragen und deren Performance intelligenter und schneller analysieren, als ich es selbst anhand der gegebene Daten tun könnte. Viele sind natürlich jetzt schon genervt von dem ganze KI-Hype, was nach meiner Meinung allerdings oftmals mit einer falschen Erwartungshaltung zu tun hat, was KI für uns tun kann. Als Security Engineer sehe großes Potential in der Unterstützung Security Scans, der Auswertung von Monitoring-Daten oder auch bei der Detektion von Angriffen auf meine Systeme.

Sebastian Springer: Der Trend, den ich am liebsten an Fahrt aufnehmen sehen würde, ist das Immersive Web – also die Arbeit mit 3D im Web in Kombination mit AR und VR. Apple hat hier dank der Vision Pro sehr gute Arbeit geleistet, und auch Meta geht mit ihrem Horizon OS in eine spannende Richtung. Bleibt abzuwarten, ob sich AR- und VR-Geräte in großer Stückzahl durchsetzen werden.

Etwas realistischer sieht es dann schon wieder bei der KI aus. Hier flaut der erste Hype endlich ab und weicht einem etwas vernünftigeren Mindset. Spannend werden die immer leistungsfähigeren SLMs wie Qwen 2.5 oder Llama 3.2, die nicht die teuren H100-Karten benötigen, um mit halbwegs akzeptabler Geschwindigkeit ausgeführt zu werden. Damit ergibt sich für uns die Möglichkeit, mit kleineren lokalen Modellen zu arbeiten, spezialisierte Applikationen zu erstellen und mit AI-Agenten ganz neue Anwendungsfälle umzusetzen.

Auch der SEO-Bereich könnte in nächster Zeit gehörig umgekrempelt werden, wenn die LLMs von OpenAI & Co plötzlich in der Lage sind, das (aktuelle) Internet zu durchsuchen und somit in Konkurrenz mit etablierten Suchmaschinen treten.

Karsten Sitterberg: Ich sehe einen klaren Trend, dass Unternehmen verstärkt in die Qualifikation ihrer Mitarbeiter investieren. Das zeigt sich auch in der steigenden Nachfrage nach Schulungen, die ich in den letzten Jahren erlebt habe. Der Fokus liegt zunehmend darauf, den Fachkräftemangel durch gut ausgebildete Mitarbeiter zu kompensieren. Qualifizierte Teams können mit weniger Personal effizienter arbeiten – ein Ansatz, der meiner Meinung nach 2025 noch an Bedeutung gewinnen wird.

Lena Rosenboom: Das Thema Inklusion ist für unsere digitale Gesellschaft sehr wichtig und wird, auch durch das BFSG, immer wichtiger werden. Entwickler müssen lernen, dass barrierefreier Code genauso dazu gehört wie ein sauberer Code. Wir müssen das Wissen in unseren Alltag integrieren, sodass es irgendwann nichts Besonderes mehr ist, barrierefreie Lösungen zu bauen.

Katja Potensky: Die weitere Abkehr von schwergewichtigen Frameworks wie Angular. HTMX hat hier eine Re-(De?-)volution losgetreten, und viele Entwickler fragen sich, warum sie für denselben Effekt zehn Stunden aufwenden sollen, wenn sie es auch in einer Stunde genauso gut tun könnten. Natürlich wird auch hier das Pendel zu weit ausschlagen, aber grundlegend ist weniger Komplexität schon eine gute Sache. In Verbindung mit dem Trend zu A11y und etablierten Patterns wird WebDev vielleicht sogar so einfach, dass man nicht mehr ständig vor technischen Herausforderungen steht, sondern sich auf die User Needs konzentrieren kann.

 

Vielen Dank für eure Antworten!

The post JavaScript News & Trends in 2024 sowie Zukunftsprognose für 2025 appeared first on JavaScript & Angular Days.

]]>
Christian Liebel im Interview: Der Weg in die webbasierte Zukunft https://javascript-days.de/blog/angular/christian-liebel-im-interview-ki-trends/ Wed, 04 Dec 2024 00:38:17 +0000 https://javascript-days.de/?p=106877 Auf den JavaScript und Angular Days im Oktober 2024 haben wir Christian Liebel getroffen und über Progressive Web Apps, neue Technologien wie Service Worker und den Einfluss generativer KI auf die Anwendungsentwicklung gesprochen. Der Trainer erläuterte, wie moderne Webtechnologien Webanwendungen nativer machen und welche Architekturtechniken entscheidend für die Entwicklung von skalierbaren Webapps sind. Zudem ging es um die langfristige Rolle von generativer KI in der Softwareentwicklung.

The post Christian Liebel im Interview: Der Weg in die webbasierte Zukunft appeared first on JavaScript & Angular Days.

]]>

 

Was ist deiner Meinung nach der größte Vorteil, wenn generative KI lokal und offline in einer Anwendung läuft?

Es hat verschiedene Vorteile, die stark davon abhängen, was man konkret benötigt. Zum einen ist es für den Datenschutz sehr vorteilhaft, da die Anfragen und Daten nie das eigene Gerät verlassen. Es ist auch super für die Latenz, da wir direkt auf dem eigenen Gerät arbeiten.

Wie siehst du die Zukunft von KI in der Frontend-Entwicklung?

KI wird nicht verschwinden; sie wird bleiben. Ich denke, wir werden mehr und mehr Anwendungsfälle sehen, bei denen KI eine Rolle spielt. Aktuell ist alles noch sehr chat-fokussiert – immer diese Chat-Nachrichtenboxen. Aber es gibt viel coolere Use Cases für generative KI, die über die Chatfunktionalität hinausgehen, und ich glaube, da werden wir mehr sehen.

NEU! Generative AI für Angular-Entwickler:innen

Erlebe das neue Bootcamp vom 19. – 20. März 2026

Was ist dein wichtigster Tipp für Entwickler:innen, die neu in die Welt der Progressiven Web Apps einsteigen wollen?

Progressive Web Apps (PWA) als webbasiertes Anwendungsmodell sind aus meiner Sicht der Weg der Zukunft. Ich kenne keine Software, die noch nativ gebaut wird – alles geht ins Web. Die wichtigsten Schritte dorthin sind, Webentwicklung zu lernen: HTML, CSS, JavaScript und später TypeScript. Darauf aufbauend sollte man Architektur-Patterns lernen, um in die Welt der Webanwendungsentwicklung einzutauchen.

Welche praktischen Ansätze gibt es in deinen Workshops?

In meinem Workshop schauen wir uns speziell die Technologien an, mit denen man Webanwendungen noch nativer machen kann. Dazu gehört das Web Manifest für die Installierbarkeit, Service Worker für die Offline-Fähigkeit und viele neue Webanwendungsschnittstellen, die durch Project Fugu ins Web gekommen sind.

Du bist zum wiederholten Male auf den JavaScript & Angular Days, was lässt dich immer wieder zurückkehren?

Ich bin immer wieder gerne dabei, weil wir viel Zeit haben, ein Thema intensiv zu behandeln. Bei normalen Konferenzen hat man oft nur 30 bis 60 Minuten, aber hier haben wir wirklich mindestens einen halben Tag Zeit, um uns den Themen ausführlicher zu widmen, als es in kurzen Sessions möglich wäre.

Hat sich der Fokus auf KI-Integration gut im Programm der diesjährigen JavaScript & Angular Days widergespiegelt, und welche KI-Workshops sollten im nächsten Jahr nicht fehlen?

Ich finde das Programm sehr gut. Generative KI reflektiert das, und wir haben diesen Trend in unserem Track ebenfalls aufgegriffen. Es ist nicht nur ein Trend – ich bin überzeugt, dass KI als wesentlicher Architekturbaustein bleiben wird. In vielen Workshops und Vorträgen haben wir das auch gesehen, und ich glaube, es ist uns gut gelungen.

The post Christian Liebel im Interview: Der Weg in die webbasierte Zukunft appeared first on JavaScript & Angular Days.

]]>