Головна / Статті / Типовані контракти та захист Zod для дашборду аналітики WebSocket у реальному часі

Типовані контракти та захист Zod для дашборду аналітики WebSocket у реальному часі

Як створити надійну панель керування в реальному часі на TypeScript: визначити контракти даних, перевіряти повідомлення WebSocket за допомогою Zod та запобігати подвійним підключенням.

1298 слів

Запит на „актуальні цифри прямо зараз“ здається проблемою візуалізації даних, але насправді це переважно проблема довіри до даних. Коли показники надходять у вигляді нетипованого JSON, коли два кінцеві точки називають одне й те саме поле по-різному, а коли сокети підключаються знову та знову у циклах, панель керування виглядає активною, але ніхто їй не вірить. У цьому посібнику розглядається невелика панель аналітики в реальному часі, створена за допомогою TypeScript, та показані принципи, які роблять її надійною: типований контракт, перевірка на рівні сокета, захищене підключення та навмисно простий дизайн.

Чому нетиповану версію не можна було довіряти

Розгляньмо типову вихідну точку: напівготову панель адміністрування, написану на простому JavaScript. Симптоми знайомі:

  • Значення проходять через код у вигляді any, тому редактор не надає жодної допомоги.
  • Бібліотека для візуалізації отримує будь-яку форму даних, яку випадково надіслав сервер.
  • Один API повертає user, інший — users_count; це схожі підходи.
  • NaN відображається у інтерфейсі кожного разу, коли поле відсутнє або має неправильну форму.
  • Жоден з цих випадків не є екзотичними багами. Усі вони мають одну основну причину: відсутній узгоджений формат даних між джерелом даних та інтерфейсом. Рішенням є одне правило, яке може дотримуватися вся команда: якщо форма даних не описана та не перевірена, вона не потрапляє до інтерфейсу.

    Визначення меж панелі керування, якою будуть справді користуватися люди

    Панелі керування з вражаючим виглядом та корисні панелі керування рідко є одним і тим самим. Стисла перша версія може включати лише:

    1. Кількість відвідувачів у цю мить
    2. Коефіцієнт конверсії за останні 24 години
    3. Найбільш відвідувані сторінки
    4. Текущий рівень помилок
    5. Індикатор „остання оновлення“, щоб користувачі знали, що дані актуальні

    Стек залишається однаково зосередженим:

    • 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, покажіть три числа разом із часовим позначенням свіжості, а сокети додавайте лише тоді, коли ця основа буде стабільною.

    Пов’язана література