Getippte Verträge und Zod-Schutzmechanismen für ein lebendiges WebSocket-Analyse-Dashboard
Wie man ein zuverlässiges Echtzeit-Dashboard in TypeScript erstellt: Payload-Verträge definieren, WebSocket-Nachrichten mit Zod validieren und doppelte Verbindungen verhindern.
Eine Anfrage nach „Echtzeit-Zahlen, sofort“ klingt wie ein Problem mit der Darstellung von Diagrammen, ist aber hauptsächlich ein Problem des Datenvertrauens. Wenn Metriken als untypisiertes JSON ankommen, wenn zwei Endpunkte dasselbe Feld unterschiedlich benennen und wenn Sockets in Schleifen neu verbunden werden, wirkt das Dashboard beschäftigt, aber niemand glaubt es. Diese Anleitung folgt einem kleinen Echtzeit-Analyse-Dashboard, das mit TypeScript erstellt wurde, und zeigt die Muster, die es zuverlässig machen: einen typisierten Vertrag, Validierung an der Sockelgrenze, eine geschützte Verbindung sowie ein bewusst einfaches Layout.
Warum die untypisierte Version nicht vertrauenswürdig sein konnte
Betrachten wir einen typischen Ausgangspunkt: ein halbfertiges Admin-Panel, das in lockeren JavaScript-Code geschrieben ist. Die Symptome sind vertraut:
- Werte fließen im Code als
any, wodurch der Editor keine Hilfe anbietet. - Die Diagrammbibliothek erhält die Form, die der Server zufällig gesendet hat.
user zurück, eine andere users_count – für ähnliche Konzepte.NaN erscheint in der Benutzeroberfläche, sobald ein Feld fehlt oder fehlerhaft ist.Keiner dieser Fehler ist besonders ausgefallen. Sie haben alle denselben Ursprung: Es gibt keinen vereinbarten Vertrag zwischen der Datenquelle und der Benutzeroberfläche. Die Lösung ist eine einzige Regel, die das gesamte Team anwenden kann: Wenn die Struktur der Daten nicht typisiert und überprüft wird, erreicht sie die Benutzeroberfläche nicht.
Definierung des Umfangs eines Dashboards, das tatsächlich genutzt wird
Aussehenswertvolle Dashboards und nützliche Dashboards sind selten dasselbe. Eine erste, schlanke Version könnte nur Folgendes enthalten:
- Auszahlende Besucherzahl zum aktuellen Zeitpunkt
- Umwandlungsrate in den letzten 24 Stunden
- Am häufigsten besuchte Seiten
- Der aktuelle Fehleranteil
- Einen „Zuletzt aktualisiert“-Indikator, damit die Nutzer wissen, dass die Daten aktuell sind
Der Fokus bleibt gleichmäßig auf diesen Elementen:
- Next.js mit dem App Router
- TypeScript im strengen Modus
- Recharts für die Diagramme
- WebSockets zur Übertragung von Aktualisierungen
- Zod zur Validierung jedes Datenpakets, bevor React es erhält
Ziel ist kein perfektes Produkt – sondern eine Reihe von Zahlen, über die das Team nicht mehr streitet.
Zuerst den Datenvertrag schreiben
Anstatt JSON abzurufen und zu hoffen, dass es übereinstimmt, sollte man zunächst genau beschreiben, was die Benutzeroberfläche erwartet. Die unten aufgeführten Typen umfassen eine allgemeine Metrik (mit ihrem Prozentsatzänderung und einem ISO-Zeitstempel) sowie das vollständige Live-Datenpaket, das über die Socket-Verbindung übertragen wird.
type DashboardMetric = {
id: string;
label: string;
value: number;
deltaPercent: number;
updatedAt: string; // ISO
};
type LiveDashboardPayload = {
visitorsNow: number;
conversionRate: number;
topPages: Array<{ path: string; views: number }>;
errorRate: number;
metrics: DashboardMetric[];
};
Diese Typen dokumentieren die Absicht und bieten Autocompletion, verschwinden aber zur Laufzeit. Eine WebSocket-Nachricht ist lediglich ein String, und TypeScript kann nicht überprüfen, was der Server sendet. Deshalb ist der nächste Schritt so wichtig.
Validierung jeder Socket-Nachricht mit Zod
Das Zod-Schema spiegelt den Vertrag wider und fügt Regeln hinzu, die durch Typen nicht ausgedrückt werden können: Zahlen dürfen nicht negativ sein, Aufrufe einer Seite müssen Ganzzahlen sein, und Konversions- sowie Fehlerraten sind Brüche zwischen 0 und 1. Das Feld updatedAt muss eine gültige Datumszeit-String sein.
import { z } from "zod";
const LiveDashboardSchema = z.object({
visitorsNow: z.number().nonnegative(),
conversionRate: z.number().min(0).max(1),
topPages: z.array(
z.object({
path: z.string(),
views: z.number().int().nonnegative(),
})
),
errorRate: z.number().min(0).max(1),
metrics: z.array(
z.object({
id: z.string(),
label: z.string(),
value: z.number(),
deltaPercent: z.number(),
updatedAt: z.string().datetime(),
})
),
});
Durch diese Maßnahme führt ein fehlerhafter Payload nicht mehr dazu, dass die Seite abstürzt oder NaN-Werte in einen Chart gelangen. Er wird abgelehnt, und der letzte gültige Zustand bleibt auf dem Bildschirm sichtbar.
Die Beibehaltung sowohl der handgeschriebenen Typen als auch des Schemas führt zu Abweichungen. Eine gängige Verbesserung besteht darin, das Schema als Quelle der Wahrheit zu betrachten und den Typ mithilfe von z.infer<typeof LiveDashboardSchema> abzuleiten. Überprüfen Sie außerdem Ihre Zod-Version: Neuere Versionen bieten z.iso.datetime() als bevorzugte Form zur Überprüfung von Datumszeiten, daher sollten Sie die API anhand der aktuellen Dokumentation überprüfen. Für eine ausführlichere Betrachtung des Teilen eines Schemas zwischen verschiedenen Schichten siehe die Verwendung eines einzigen Zod-Schemas im Frontend und Backend.
Kontrolle von Neuverbindungen und doppelten Listern
Echtzeitfunktionen neigen dazu, auf bestimmte Weise zu versagen. Eine naive erste Version verbindet sich endlos neu, fügt bei jedem Versuch einen neuen Nachrichtenverarbeiter hinzu, stapelt Diagrammaktualisierungen auf veralteten auf und blockiert letztendlich den Browser.
Die Lösung besteht darin, die Verbindung als kleine Zustandsmaschine zu betrachten: idle, anschließend connecting, dann live; bei Wiederherstellung des Netzwerks wechselt sie zu reconnecting und zurück zu live. Die wichtigste Regel ist, dass gleichzeitig nur ein Socket existieren darf. Die untenstehende connect-Funktion sorgt dafür: Wenn bereits ein Socket geöffnet oder im Öffnungsprozess ist, gibt sie umgehend zurück. Eintreffende Nachrichten werden mit safeParse analysiert, welches statt eines Fehlers ein Ergebnisobjekt zurückgibt; dadurch werden ungültige Daten protokolliert und übersprungen, während gültige Daten den Zustand aktualisieren.
let socket: WebSocket | null = null;
function connect() {
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
return;
}
socket = new WebSocket(process.env.NEXT_PUBLIC_WS_URL!);
socket.onmessage = (event) => {
const parsed = LiveDashboardSchema.safeParse(JSON.parse(event.data));
if (!parsed.success) {
console.warn("Invalid live payload", parsed.error);
return;
}
setDashboard(parsed.data);
};
}
Vor der Veröffentlichung sollten einige Lücken geschlossen werden. JSON.parse kann bei einem nicht-JSON-Inhalt Fehler auslösen, daher sollte es in try/catch eingebettet werden. Der Codeausschnitt zeigt den Schutzmechanismus, aber nicht den Weg zur Neukonnexion; fügen Sie eine onclose-Verarbeitung mit Verzögerungen hinzu, damit ein Serverausfall keinen sofortigen Neukonnexionsschleifen auslöst. In React sollte der Socket in einer Effect-Cleanup-Funktion geschlossen werden, damit bei Neuinitialisierungen (einschließlich der doppelten Aufrufe von Effects im Strict Mode zur Entwicklung) keine Verbindungen leckagen.
Entwurf unter Berücksichtigung der Frage „Wo schaue ich zuerst?“
Es ist verlockend, ein Live-Dashboard mit Gradienten, leuchtenden Karten und vielen Farben zu gestalten. Ein besseres Testverfahren ist es, einen Stakeholder zu fragen, wohin seine Augen zuerst schauen sollten, und anschließend alles wegzunehmen, was dieser Frage nicht entspricht. Ein gut funktionierender Layout-Entwurf sieht wie folgt aus:
- Eine Zeile mit maximal vier Hauptmetriken
Live • aktualisiert vor 2 SekundenAuch das Eingeben von Inhalten hilft hier. Wenn jede Metrik eine definierte Form hat, kann die Benutzeroberfläche keine ad-hoc Widgets für Daten erstellen, die niemand angefordert hat. Diese Einschränkungen sorgen dafür, dass das Design ehrlich bleibt.
Was Nutzer nach dem Start bemerken
Sobald eine solche Dashboard-Lösung verfügbar ist, bezieht sich die Rückmeldung selten auf die Architektur. Die Nutzer sagen, sie vertrauen endlich den Zahlen, die Seite friert nicht mehr ein, und sie sind überrascht, dass sie tatsächlich in Echtzeit aktualisiert wird. Das ist die eigentliche Aufgabe eines Dashboards: nicht eine Galerie von Diagrammen, sondern ein Werkzeug, auf das man in Meetings vertrauen kann.
Kernpunkte
- Geben Sie die Grenzen ein und validieren Sie jeden externen Datensatz zur Laufzeit; allein TypeScript kann nicht erkennen, was der Server sendet.
- Lassen Sie den strengen Modus aktiv; er zahlt sich jedes Mal aus, wenn sich der Code ändert.
- Betrachten Sie eine Live-Verbindung als Zustandsmaschine und erlauben Sie genau einen Socket.
- Entfernen Sie Interface-Elemente, bis die Hauptinformationen klar hervortreten.
- Ziehen Sie eine einfache Ansicht mit korrekten Daten einer aufwendigen Ansicht mit zweifelhaften Daten vor.
Falls Sie Ihr erstes Live-Dashboard entwickeln, widerstehen Sie dem Drang, sofort mit großem Umfang zu beginnen. Starten Sie mit einem einzigen eingegebenen Payload, der durch ein Zod-Schema überprüft wird, zeigen Sie drei Zahlen zusammen mit einem Aktualisierungszeitstempel an und fügen Sie Sockets erst hinzu, wenn diese Grundlage steht.
Verwandte Artikel
- Trennung von Domain-, Daten- und UI-Schichten in einer Next.js App Router Codebase — Eine Fallstudie aus dem Pokédex, die zeigt, wie man eine Next.js App Router-Anwendung mithilfe von Prisma, Zod, Cookie-Authentifizierung und Caching in Domain-, Daten- und Präsentationsschichten aufteilt.
- Technisches SEO im Next.js App Router: Metadata, Sitemaps und JSON-LD — Erfahren Sie, wie gemeinsam genutzte Metadata-Hilfsfunktionen, Standard-Einstellungen für das Root-Layout, robots.ts, ein dynamischer Sitemap, korrektes JSON-LD sowie Seitenaudits einer Next.js-Anwendung eine solide SEO-Base bieten.
- Vue 3 in der Praxis: Composables, typisierte Kontrakte und proportionales State-Management — Wie die Composition API von Vue 3, typisierte Props und Emits, Pinia sowie schrittweise Implementierungen es ermöglichen, dass eine Anwendung nur so komplex wird, wie es wirklich notwendig ist – und wann Vue die falsche Wahl ist.