Перестаньте тратить впустую вызовы API и вместо этого реализовать шаблон лидера TAB

Перестаньте тратить впустую вызовы API и вместо этого реализовать шаблон лидера TAB

16 июля 2025 г.

Чему я буду научить тебя

Я собираюсь показать вам, как реализоватьОбразец лидера TABЭто устраняет избыточный опрос API на нескольких вкладках браузера. Вы научитесь создавать систему, где только одна вкладка обрабатывает данные данных, в то время как все остальные получают выгоду от общих обновлений кэша через LocalStorage и API BroadcastChannel.

К концу этого поста у вас будет полная реализация TypeScript, которая:

  • Автоматически выбирает вкладку «Лидер» для обработки опроса API
  • Общие данные к кэшированию на всех вкладках мгновенно
  • Обрабатывает краевые случаи, такие как закрытие вкладок и переходы лидерства
  • Интегрируется с запросом React и Redux/RTK

Почему это важно для вас

Каждый дополнительный вызов API несет стоимость и ухудшает пользовательский опыт.

Если вы строите панель панели администратора или любое многокамерное приложение, вы, вероятно, столкнетесь с этой проблемой прямо сейчас:

  • Пользователь открывает 5 вкладок вашего приложения
  • Каждая вкладка опросывает ваш API каждые 3 минуты
  • Ваш сервер получает в 5 раз необходимых запросов
  • Ваши ограничения по ставке API начинаются
  • Пользователи видят противоречивые данные на вкладках
  • Ваши хостинг раскачиваются

Это не просто техническая проблема; Это проблема бизнеса. Я видел, как компании тратят тысячи дополнительных в месяц на ненужные вызовы API просто потому, что они никогда не реализовали надлежащую координацию вкладок.

Почему большинство людей терпят неудачу в этом

Большинство разработчиков пытаются один из этих ошибочных подходов:

❌ Подход «игнорировать это»:Они надеются, что пользователи не откроют несколько вкладок. СПОЙЛЕР: Они будут.

❌ Подход «Отключить несколько вкладок»:Они пытаются полностью предотвратить несколько вкладок. Пользователи ненавидят это и работают вокруг.

❌ Подход "Complete WebSocket":Они чрезмерно инженеры с веб-окетами, когда достаточно простых API браузера.

❌ Подход «Общий работник»:Они используют Sharedworker, который имеет плохую поддержку браузера и ненужную сложность.

Реальная проблема? Они не понимают этогоКоординация TAB - это проблема лидерства, а не проблема связи.Вам нужна одна вкладка, чтобы быть «лидером», который выполняет работу, в то время как другие следуют.

Шаблон лидера TAB меняет все

Вот прорывное понимание:Относитесь к вкладкам браузера как к распределенной системе с выборами в лидере.

Вместо каждой вкладки действует независимо, вы устанавливаете иерархию:

  • Один лидер вкладкаобрабатывает все опросы API
  • Все вкладки последователяСлушайте обновления с помощью BroadcastCannel
  • Автоматическое отказоустойчивостьКогда вкладка «Лидер» закрывается
  • Общий кешВ LocalStorage все синхронизируется

Этот шаблон уменьшает вызовы API на 80-90%, одновременно улучшая согласованность данных по вкладкам.

Ключевые выводы

Реализуя этот шаблон, вы достигнете:

Массовое снижение затрат API- Только одна вкладка опросывает ваши конечные точки, независимо от того, сколько вкладок открыто

Улучшенная производительность- Больше нет дублирующих сетевых запросов, замедляющих ваше приложение

Лучший пользовательский опыт- Последовательные данные на всех вкладках с мгновенными обновлениями

Автоматическое отказоустойчивость- Когда вкладка «Лидер» закрывается, другая вкладка плавно вступает во владение

Нулевая конфигурация- Система самоорганизации без какого-либо пользовательского вмешательства

Структура агностик- Работает с React, Vue, Angular или Vanilla JavaScript

Готовый к производству- Обработка краев, таких как быстрое переключение вкладок и сбои сети

Тип-безопасная реализация- Полная поддержка TypeScript с правильной обработкой ошибок

Полная реализация

Давайте построим этот шаг за шагом.

Шаг 1: Основной менеджер лидерства

Во -первых, нам нужна система для избрания и поддержания вкладки «Лидер:

// pollingLeaderManager.ts
type Listener = (isLeader: boolean, lastPollTime: number) => void;

const CHANNEL_NAME = 'polling-leader';
const LEADER_TTL = 5000;

let isLeader = false;
const tabId = `${Date.now()}-${Math.random()}`;
let channel: BroadcastChannel | null = null;
let pingInterval: NodeJS.Timeout | null = null;
let leaderTimeout: NodeJS.Timeout | null = null;
let listeners: Listener[] = [];
let initialized = false;

export let lastLeaderPollTime = 0;

function notifyListeners() {
  listeners.forEach(listener => listener(isLeader, lastLeaderPollTime));
}

export function subscribeToLeadership(listener: Listener) {
  listeners.push(listener);
  listener(isLeader, lastLeaderPollTime);

  return () => {
    listeners = listeners.filter(l => l !== listener);
  };
}

export function initPollingLeader() {
  if (initialized) return;
  initialized = true;

  channel = new BroadcastChannel(CHANNEL_NAME);

  const sendPing = () => {
    channel?.postMessage({ type: 'ping', tabId, timestamp: Date.now() });
  };

  const becomeLeader = () => {
    if (!isLeader) {
      isLeader = true;
      lastLeaderPollTime = Date.now();
      notifyListeners();
    }
    sendPing();
  };

  const loseLeadership = () => {
    if (isLeader) {
      isLeader = false;
      notifyListeners();
    }
  };

  const handleMessage = (event: MessageEvent) => {
    if (event.data?.type === 'ping' && event.data.tabId !== tabId) {
      loseLeadership();
      resetLeaderTimeout();
    }
  };

  const resetLeaderTimeout = () => {
    if (leaderTimeout) clearTimeout(leaderTimeout);
    leaderTimeout = setTimeout(() => {
      becomeLeader();
    }, LEADER_TTL + 500);
  };

  channel.addEventListener('message', handleMessage);
  resetLeaderTimeout();

  pingInterval = setInterval(() => {
    if (isLeader) sendPing();
  }, LEADER_TTL - 1000);

  window.addEventListener('beforeunload', () => {
    channel?.close();
    if (pingInterval) clearInterval(pingInterval);
    if (leaderTimeout) clearTimeout(leaderTimeout);
  });
}

Как это работает:

  • Каждая вкладка получает уникальный идентификатор и слушает вещательный канал
  • Вкладки лидера отправляют сообщения "Ping" каждые 4 секунды
  • Если вкладка не слышит пинг в течение 5,5 секунд, она предполагает лидерство
  • Чистая обработка отключения предотвращает лидеров зомби

Шаг 2: Крюк из опроса

Далее мы создаем крюк React, который обрабатывает фактическую логику опроса:

// useLeaderPollingEffect.ts
import { useEffect, useRef } from 'react';

const POLLING_INTERVAL = 180000; // 3 minutes
const POLLING_DEBOUNCE = 5000;
const LAST_POLL_TIME_KEY = 'last_poll_time';

function getLastPollTimeFromStorage(): number {
  const stored = localStorage.getItem(LAST_POLL_TIME_KEY);
  return stored ? parseInt(stored, 10) : 0;
}

function setLastPollTimeInStorage(time: number): void {
  localStorage.setItem(LAST_POLL_TIME_KEY, time.toString());
}

export function useLeaderPollingEffect(
  isLeader: boolean, 
  lastLeaderPollTime: number, 
  pollingFns: (() => void)[] = []
) {
  const intervalRef = useRef<NodeJS.Timeout | null>(null);

  useEffect(() => {
    if (!isLeader) {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
        intervalRef.current = null;
      }
      return;
    }

    const lastStoredPollTime = getLastPollTimeFromStorage();
    const currentTime = Date.now();
    const timeSinceLastPoll = currentTime - lastStoredPollTime;

    const delay = Math.max(0, POLLING_INTERVAL - timeSinceLastPoll);

    const runPolling = () => {
      pollingFns.forEach(fn => fn());
      setLastPollTimeInStorage(Date.now());
    };

    const timeout = setTimeout(
      () => {
        runPolling();
        intervalRef.current = setInterval(runPolling, POLLING_INTERVAL);
      },
      timeSinceLastPoll >= POLLING_INTERVAL ? POLLING_DEBOUNCE : delay
    );

    return () => {
      clearTimeout(timeout);
      if (intervalRef.current) clearInterval(intervalRef.current);
    };
  }, [isLeader, lastLeaderPollTime, pollingFns]);
}

Ключевые функции:

  • Только опросы, когда вкладка является лидером
  • Вычисляет умные задержки на основе последнего времени опроса
  • Предотвращает быстрый опрос во время лидерских переходов
  • Сохраняет время в сфере вкладки

Шаг 3: Главный крючок

Создайте простой интерфейс для использования компонентов:

// usePollingLeader.ts
import { useEffect, useState } from 'react';
import { initPollingLeader, subscribeToLeadership } from './pollingLeaderManager';

export function usePollingLeader() {
  const [isLeader, setIsLeader] = useState(false);
  const [lastPollTime, setLastPollTime] = useState(0);

  useEffect(() => {
    initPollingLeader();
    const unsubscribe = subscribeToLeadership((isLeader, lastPollTime) => {
      setIsLeader(isLeader);
      setLastPollTime(lastPollTime);
    });

    return unsubscribe;
  }, []);

  return { isLeader, lastPollTime };
}

Шаг 4: Использование реального мира

Вот как использовать его в вашем приложении:

// AuthorizedLayout.tsx
import { useMemo } from 'react';
import { usePollingLeader } from './usePollingLeader';
import { useLeaderPollingEffect } from './useLeaderPollingEffect';

export default function AuthorizedLayout({ children }) {
  const { isLeader, lastPollTime } = usePollingLeader();

  // Define your API calls
  const pollingFns = useMemo(() => [
    () => triggerGetAllAttributes(),
    () => triggerGetAllCustomEventsWithProperties(),
    () => triggerGetAllAttributesWithProperties(),
    () => triggerGetAllSegments(),
    () => triggerGetChannelConfig(),
  ], [/* your dependencies */]);

  // Only the leader tab will execute these
  useLeaderPollingEffect(isLeader, lastPollTime, pollingFns);

  return <div>{children}</div>;
}

Продвинутые соображения

Обработка ошибок

Добавьте границы логики и ошибок повторения:

const runPolling = async () => {
  try {
    await Promise.all(pollingFns.map(fn => fn()));
    setLastPollTimeInStorage(Date.now());
  } catch (error) {
    console.error('Polling failed:', error);
    // Implement exponential backoff
  }
};

Оптимизация производительности

  • ИспользоватьuseMemoДля функций опросов для предотвращения ненужных повторных ресурсов
  • Реализовать дедупликацию запроса на уровне API
  • Подумайте об использованииrequestIdleCallbackДля некритических обновлений

Тестирование

Ихничивая трансляция в ваших тестах:

// test-utils.ts
class MockBroadcastChannel {
  addEventListener = jest.fn();
  postMessage = jest.fn();
  close = jest.fn();
}

global.BroadcastChannel = MockBroadcastChannel;

Поддержка браузера и закупорители

FroadcastChannel обладает отличной современной поддержкой браузера, но рассмотрите запасные отступления:

const hasSupport = typeof BroadcastChannel !== 'undefined';
if (!hasSupport) {
  // Fallback to polling in each tab
  // Or use a different communication method
}

Заключение

Образец лидера TAB-это изменение игры для мульти-таб-приложений. Это разница между системой, которая элегантно масштабируется и осыпается по его запросам API.

Самая лучшая часть?Ваши пользователи никогда не заметят сложность; Они просто испытывают более быстрые, более последовательные данные во всех своих вкладках, в то время как ваши расходы API падают.

Начните с основной реализации выше, затем настройте ее для вашего конкретного варианта использования. Ваше будущее (и ваш счет хостинга) поблагодарит вас.


Хотите увидеть больше таких продвинутых шаблонов? Следуйте за мной за более глубокими погружениями, чтобы решить реальные проблемы на фронте.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE