Типовые контракты и защита Zod для дашборда аналитики WebSocket в реальном времени
Как создать надёжную панель управления в реальном времени на TypeScript: определение контрактов данных, проверка сообщений WebSocket с помощью Zod и предотвращение дублирования подключений.
Запрос на «актуальные цифры прямо сейчас» кажется проблемой отображения графиков, но на самом деле это в основном проблема доверия к данным. Когда метрики поступают в виде неотформатированного JSON, когда два конечных точка называют одно и то же поле по-разному, а сокеты переподключаются в циклах, панель управления кажется активной, но никто ей не верит. В этом руководстве рассматривается небольшая панель реального времени для аналитики, созданная с использованием TypeScript, и показаны принципы, обеспечивающие её надежность: типизированный контракт, проверка данных на границе сокета, защищенное подключение и максимально простой дизайн.
Почему неотформатированная версия не может считаться надежной
Рассмотрим типичную отправную точку: незавершенная панель администрирования, написанная на простом JavaScript. Симптомы знакомы:
- Значения передаются по коду в виде типа
any, поэтому редактор не предоставляет никакой помощи. - Библиотека для графиков получает любой формат данных, который случайно отправил сервер.
user, другой — users_count; смысл подобен.NaN отображается в интерфейсе каждый раз, когда поле отсутствует или имеет неверную структуру.Ни один из этих случаев не является редким багом. У них одна общая причина: отсутствует четко оговоренный формат данных между источником данных и интерфейсом. Решение — единое правило, которое может соблюдать вся команда: если структура данных не определена и не проверена, она не попадает в интерфейс.
Определение объема панели управления, которой действительно будут пользоваться
Панели управления с впечатляющим внешним видом редко бывают полезными. В первой версии может быть включено только:
- Количество посетителей в данный момент
- Коэффициент конверсии за последние 24 часа
- Самые популярные страницы
- Текущий уровень ошибок
- Индикатор «последнее обновление», чтобы пользователи знали, что данные актуальны
Стек остаётся одинаково сфокусированным:
- Next.js с App Router
- TypeScript в строгом режиме
- Recharts для графиков
- WebSockets для передачи обновлений
- Zod для проверки каждого пакета данных до того, как его увидит React
Цель — не создать идеальный продукт. Это набор чисел, по поводу которых команда больше не будет спорить.
Сначала определяем контракт данных
Вместо загрузки JSON и надежды на его соответствие сначала точно опишите, чего ожидает интерфейс. Приведённые ниже типы охватывают общий показатель (с процентом изменения и ISO-временной меткой) и полный пакет данных в реальном времени, который передаётся через сокет.
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[];
};
Эти типы документируют намерения и обеспечивают автодополнение, но исчезают во время выполнения. Сообщение WebSocket — это просто строка, и TypeScript не может проверить содержимое, отправляемое сервером. Именно поэтому следующий шаг имеет большое значение.
Проверка каждого сообщения сокета с помощью Zod
Схема Zod отражает условия использования и добавляет правила, которые невозможно выразить с помощью типов: количество не может быть отрицательным, количество просмотров страниц должно быть целым числом, а коэффициенты конверсии и ошибок — дробями в диапазоне от 0 до 1. Поле updatedAt должно представлять собой допустимую строку с данными времени.
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(),
})
),
});
Благодаря этому некорректный пакет данных больше не приводит к сбою страницы или появлению значений NaN в графике. Он отклоняется, и последнее корректное состояние остается на экране.
Сохранение как ручно определенных типов, так и схемы приводит к искажениям. Распространенным решением является использование схемы в качестве источника истины и генерация типов с помощью z.infer<typeof LiveDashboardSchema>. Также проверьте версию Zod: в новых выпусках в качестве предпочтительного способа проверки даты и времени предлагается функция z.iso.datetime(), поэтому убедитесь, что API соответствует актуальной документации. Чтобы узнать больше о совместном использовании одной схемы в разных слоях, ознакомьтесь с использованием единой схемы Zod на фронтенде и бэкенде.
Как справиться с повторными подключениями и дублирующимися обработчиками
Функции реального времени часто выходят из строя определенным образом. Наивная первая версия продолжает подключаться бесконечно, добавляет новый обработчик сообщений при каждой попытке, накапливает обновления графиков поверх устаревших, в итоге замедляя работу браузера.
Решение заключается в том, чтобы рассматривать процесс подключения как небольшую машину состояний: idle, затем connecting, потом live; при прерывании связи переход на состояние reconnecting, а при восстановлении сети — обратно в live. Самое важное правило — одновременно может существовать только один сокет. Функция connect, приведённая ниже, обеспечивает его соблюдение: если сокет уже открыт или находится в процессе открытия, она немедленно возвращает результат. Приходящие сообщения обрабатываются с помощью функции safeParse, которая возвращает объект с результатом вместо того, чтобы выбрасывать исключение; таким образом недействительные данные фиксируются и игнорируются, в то время как действительные данные обновляют состояние.
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);
};
}
Перед запуском в производство стоит устранить несколько недостатков. Функция JSON.parse может выбросить исключение при обработке данных, не являющихся JSON, поэтому её следует обернуть в блоки try/catch. В приведённом фрагменте показан механизм защиты, но отсутствует логика повторной подключения; необходимо добавить обработчик события onclose с задержкой повторных попыток, чтобы простои сервера не приводили к чрезмерному количеству попыток подключения. В React следует закрывать сокет в функции очистки эффектов, чтобы при перезагрузке компонента (включая двойное вызов эффектов в режиме Strict Mode при разработке) не происходило утечек соединений.
Проектирование с учётом вопроса «куда смотреть в первую очередь?»
Хочется украсить интерактивную панель градиентами, светящимися карточками и множеством цветов. Лучший способ проверки — спросить у заинтересованных сторон, куда им следует смотреть в первую очередь, а затем убрать всё, что не отвечает на этот вопрос. Хорошая схема разметки выглядит так:
- Один ряд из максимум четырёх основных показателей
Live • updated 2s agoЗдесь тоже полезно использовать ввод текста. Когда у каждого показателя определённый формат, интерфейс не может автоматически создавать дополнительные элементы для данных, которые никто не указывал. Такие ограничения сохраняют честность дизайна.
Что замечают пользователи после запуска
Как только появляется такой панель управления, отзывы редко касаются архитектуры. Люди говорят, что наконец начинают доверять цифрам, что страница больше не тормозит, и удивляются тому, что она действительно работает в реальном времени. В этом и заключается настоящая функция панели управления: не просто галерея графиков, а инструмент, на который можно полагаться во время совещаний.
Основные выводы
- Указывайте границы и проверяйте каждый внешний пакет данных во время выполнения; одного TypeScript недостаточно, чтобы увидеть то, что отправляет сервер.
- Держите режим строгой проверки включённым — это приносит пользу каждый раз, когда меняется код.
- Рассматривайте живое соединение как машину состояний и разрешайте использование ровно одного сокета.
- Удаляйте элементы интерфейса, пока основная суть не станет очевидной.
- Всегда предпочитайте простой вид с корректными данными сложному виду с сомнительными данными.
Если вы создаете свой первый дашборд в реальном времени, не стремитесь сразу делать всё масштабно. Начните с одного введенного пакета данных, проверяемого схемой Zod, отобразите три числа вместе с временной меткой свежести, и добавляйте сокеты только тогда, когда эта основа будет устойчивой.
Связанные материалы
- Разделение слоев домена, данных и интерфейса в кодбейсе Next.js App Router — пример из Pokédex, показывающий, как разделить приложение Next.js App Router на слои домена, данных и представления с использованием Prisma, Zod, аутентификации через куки и кэширования.
- Технический SEO в Next.js App Router: метаданные, карточки сайта и JSON-LD — Узнайте, как инструменты для работы с метаданными, стандартные настройки лейаута, файл robots.ts, динамическая карточка сайта, корректно сформированный JSON-LD и аудит страниц создают прочную основу для SEO в приложениях Next.js.
- Vue 3 на практике: композаблы, типизированные контракты и пропорциональное состояние — Как API композиций Vue 3, типизированные параметры и сигналы, Pinia и постепенное внедрение новых функций позволяют приложению развиваться только настолько, насколько это необходимо, и определяют ситуации, когда Vue является неподходящим выбором.