Kontrakty w formacie tekstowym oraz mechanizmy Zod Guards dla żywego panelu analitycznego WebSocket
Jak stworzyć wiarygodną panelę sterującą w czasie rzeczywistym w TypeScript: zdefiniuj umowy dla danych przekazywanych, waliduj wiadomości WebSocket za pomocą Zod i zapobiegaj podwójnym połączeniom.
Zapytanie o „liczby na żywo, teraz” brzmi jak problem z wykresami, ale w rzeczywistości jest to głównie kwestia zaufania do danych. Gdy metryki przychodzą w postaci nietypowanego JSON, gdy dwa punkty końcowe nazywają to samo pole inaczej oraz gdy połączenia się łączą w pętlach, panel analityczny wygląda na aktywny, ale nikt w to nie wierzy. Ten przewodnik opisuje mały panel analityki w czasie rzeczywistym stworzony z użyciem TypeScript i pokazuje wzorce, które zapewniają jego niezawodność: typowany kontrakt, walidacja na granicy połączenia, zabezpieczone połączenie oraz celowo prosta struktura.
Dlaczego wersja nietypowana nie mogła być godna zaufania
Rozważmy typowy punkt wyjścia: nieukończoną panel administracyjny napisaną w prostym JavaScript. Objawy są znajome:
- Wartości przepływają przez kod jako
any, więc edytor nie oferuje żadnej pomocy. - Biblioteka do tworzenia wykresów otrzymuje dowolną formę danych, jaką akurat wysłał serwer.
user, a inna users_count – to podobne rozwiązania.NaN pojawia się w interfejsie użytkownika, gdy pole jest brakujące lub nieprawidłowe.Żaden z tych problemów nie jest wyjątkowo skomplikowany. Wszystkie mają jeden wspólny przyczynę: brak uzgodnionych zasad pomiędzy źródłem danych a interfejsem użytkownika. Rozwiązaniem jest jedna zasada, którą może stosować cały zespół: jeśli struktura danych nie jest zdefiniowana i sprawdzona, nie trafia do interfejsu użytkownika.
Ograniczenie zakresu dashboardu, którego rzeczywiście będą używać ludzie
Dashboardy o imponującym wyglądzie i te przydatne rzadko są tym samym. Wstępna, uproszczona wersja może zawierać tylko:
- Liczbę obecnych odwiedzających
- Wskaźnik konwersji w ciągu ostatnich 24 godzin
- Najczęściej odwiedzane strony
- Bieżący wskaźnik błędów
- Indykator „ostatnia aktualizacja”, aby użytkownicy wiedzieli, że dane są aktualne
Stack pozostaje równie skoncentrowany:
- Next.js z App Routerem
- TypeScript w trybie ścisłym
- Recharts do tworzenia wykresów
- WebSockets do wysyłania aktualizacji
- Zod do weryfikacji każdego danych przed ich otrzymaniem przez React
Celem nie jest doskonały produkt, lecz zestaw liczb, co do których zespół przestaje się kłócić.
Napisanie kontraktu danych najpierw
Zamiast pobierać JSON i mieć nadzieję, że będzie pasować, zacznij od dokładnego opisania tego, czego oczekuje interfejs użytkownika. Poniższe typy obejmują ogólną miarę (wraz z procentową zmianą i zapisem czasu w formacie ISO) oraz pełne dane przesyłane przez WebSockets.
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[];
};
Te typy dokumentują intencję i umożliwiają autodopisywanie, ale znikają w czasie wykonywania. Wiadomość z WebSockets to po prostu ciąg znaków, a TypeScript nie może sprawdzić, co wysyła serwer. Dlatego następny krok jest tak ważny.
Schemat Zod odzwierciedla umowę i dodaje reguły, których typy nie mogą wyrazić: liczby nie mogą być ujemne, liczba przeglądów stron musi być liczbą całkowitą, a wskaźniki konwersji i błędów to ułamki w zakresie od 0 do 1. Pole updatedAt musi być ważnym ciągiem znaków reprezentującym datę i godzinę.
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(),
})
),
});
Dzięki temu nieprawidłowy payload już nie powoduje awarii strony ani wycieku wartości NaN do wykresu. Jest odrzucany, a ostatni poprawny stan pozostaje na ekranie.
Zachowanie zarówno typów ręcznie zdefiniowanych, jak i schematu może prowadzić do odchyleń. Powszechnym rozwiązaniem jest traktowanie schematu jako źródła prawdy i wyprowadzanie typu za pomocą z.infer<typeof LiveDashboardSchema>. Sprawdź również wersję swojego Zod: najnowsze wydania oferują z.iso.datetime() jako preferowaną formę sprawdzania danych datowo-czasowych, więc upewnij się co do API zgodnie z aktualną dokumentacją. Aby dowiedzieć się więcej na temat współdzielenia jednego schematu pomiędzy różnymi warstwami, przeczytaj jak używać jednego schematu Zod zarówno w frontendzie, jak i backendzie.
Kontrola ponownych połączeń i dublowanych słuchaczy
Funkcje w czasie rzeczywistym zazwyczaj zawodzą w określony sposób. Prosta pierwsza wersja nieskończenie się łączy, za każdym razem dodaje nowego obsługującego wiadomości i nakłada aktualizacje wykresów na stare, co ostatecznie spowalnia przeglądarkę.
Rozwiązaniem jest traktowanie tej łączności jako małej maszyny stanowej: idle, następnie connecting, potem live; po restarcie sieci przechodzi na reconnecting, a następnie znowu na live. Najważniejszą zasadą jest to, że jednocześnie może istnieć tylko jeden sokiet. Funkcja connect poniżej ją egzekwuje: jeśli sokiet jest już otwarty lub w trakcie otwierania, natychmiast zwraca wynik. Przychodzące wiadomości są analizowane za pomocą safeParse, który zwraca obiekt z wynikiem zamiast rzucać błędem, dzięki czemu nieważne dane są rejestrowane i pomijane, podczas gdy ważne dane aktualizują stan.
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);
};
}
Niektóre luki należy załatać przed wdrożeniem do produkcji. Funkcja JSON.parse może sama wywołać błąd przy niepoprawnym formacie JSON, dlatego należy ją otoczyć blokiem try/catch. Przykład pokazuje mechanizm ochronny, ale nie sposób ponownego połączenia; trzeba dodać obsługę zdarzenia onclose z opóźnieniem, aby awaria serwera nie powodowała szybkiego cyklu ponownych prób połączenia. W React należy zamknąć socket w funkcji czyszczenia efektów, aby ponowne załadowanie strony (włączając podwójne wywołanie efektów w trybie Strict Mode podczas rozwoju) nie powodowało wycieku połączeń.
Projektowanie z myślą o pytaniu „gdzie najpierw spojrzeć?”
Kuszące jest udekorowanie żywego panelu kontrolnego gradientami, świecącymi kartkami i wieloma kolorami. Lepszym testem jest zapytanie interesariuszy, gdzie powinny najpierw spocząć ich oczy, a następnie usunięcie wszystkiego, co temu nie odpowiada. Dobry układ to:
- Rząd składający się co najwyżej z czterech głównych wskaźników
Live • updated 2s agoPisanie ręcznie również pomaga. Gdy każda miara ma określony kształt, interfejs nie może generować dodatkowych elementów do danych, których nikt nie określił. Takie ograniczenia zapewniają uczciwy design.
To, co zauważają użytkownicy po uruchomieniu
Gdy taka karta kontrolna trafia do użytkowników, opinie rzadko dotyczą architektury. Ludzie mówią, że w końcu ufają liczbom, że strona już się nie zamyka i są zaskoczeni, że rzeczywiście jest aktualna. To właśnie jest prawdziwym zadaniem karty kontrolnej: nie galeria wykresów, lecz narzędzie, na które można polegać podczas spotkań.
Główne wnioski
- Należy wpisywać granice i weryfikować każdy zewnętrzny pakiet danych w czasie wykonywania; sam TypeScript nie może zobaczyć, co wysyła serwer.
- Należy zachować tryb ścisły włączony; przynosi korzyści za każdym razem, gdy zmienia się kod.
- Traktuj połączenie w czasie rzeczywistym jak maszynę stanową i zezwalaj na dokładnie jeden sokiet.
- Usuń elementy interfejsu, dopóki główna idea nie stanie się oczywista.
- Lepiej mieć prosty widok z poprawnymi danymi niż wyrafinowany widok z wątpliwymi danymi.
Jeśli budujesz swój pierwszy panel analityczny w czasie rzeczywistym, unikaj rozpoczynania od zbyt skomplikowanych rozwiązań. Zacznij od pojedynczego danych wprowadzonych ręcznie, sprawdzonych za pomocą schematu Zod, pokaż trzy liczby wraz z datą aktualizacji i dodaj sokiety dopiero wtedy, gdy ta podstawa będzie już solidna.
Literatura pokrewna
- Oddzielanie warstw domeny, danych i interfejsu w kodzie Next.js App Router — studium przypadku Pokédex pokazujące, jak podzielić aplikację Next.js App Router na warstwy domeny, danych i prezentacji przy użyciu Prisma, Zod, autoryzacji przez pliki cookie oraz cache’owania.
- SEO techniczne w Next.js App Router: Metadane, sitemapy i JSON-LD — Dowiedz się, jak wspólne narzędzia do metadanych, domyślne ustawienia layoutu, plik robots.ts, dynamiczny sitemap, poprawnie skonstruowane JSON-LD oraz audyty stron tworzą solidną bazę SEO dla aplikacji Next.js.
- Vue 3 w praktyce: Komponenty, typowane parametry i stan proporcjonalny — Jak API kompozycyjne Vue 3, typowane parametry i wywołania, Pinia oraz stopniowe wdrażanie umożliwiają aplikacji rozwój tylko w takim stopniu, jakiego potrzebuje, oraz kiedy Vue nie jest odpowiednim wyborem.