← Назад к вопросам

Как сделать двустороннее взаимодействие без WebSocket?

2.3 Middle🔥 191 комментариев
#JavaScript Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как сделать двустороннее взаимодействие без WebSocket

Двустороннее взаимодействие (real-time communication) часто ассоциируется с WebSocket, но есть несколько альтернативных подходов, которые работают без них. Каждый подход имеет свои плюсы и минусы.

Метод 1: Polling (Опрос)

Это самый простой подход - клиент периодически спрашивает сервер об обновлениях.

// Простой polling
function usePolling(url: string, interval: number = 5000) {
  const [data, setData] = useState(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (err) {
        setError((err as Error).message);
      }
    };

    // Первый запрос сразу
    fetchData();

    // Затем повторяем каждые N миллисекунд
    const timer = setInterval(fetchData, interval);
    return () => clearInterval(timer);
  }, [url, interval]);

  return { data, error };
}

// Использование
function ChatMessages({ roomId }) {
  const { data: messages } = usePolling(`/api/rooms/${roomId}/messages`, 3000);
  return <div>{messages?.map(m => <p key={m.id}>{m.text}</p>)}</div>;
}

Метод 2: Long Polling

Сервер держит соединение открытым, пока не появятся новые данные.

function useLongPolling(url: string) {
  const [data, setData] = useState(null);
  const [isConnected, setIsConnected] = useState(false);
  const abortControllerRef = useRef(new AbortController());

  useEffect(() => {
    let isMounted = true;

    const longPoll = async () => {
      while (isMounted) {
        try {
          setIsConnected(true);
          const response = await fetch(url, {
            signal: abortControllerRef.current.signal,
          });

          if (!response.ok) throw new Error('Poll failed');

          const result = await response.json();
          if (isMounted) {
            setData(result);
          }
        } catch (err) {
          if (isMounted) {
            setIsConnected(false);
            // Ждем перед переподключением
            await new Promise(resolve => setTimeout(resolve, 5000));
          }
        }
      }
    };

    longPoll();

    return () => {
      isMounted = false;
      abortControllerRef.current.abort();
    };
  }, [url]);

  return { data, isConnected };
}

// Использование
function NotificationCenter() {
  const { data: notification, isConnected } = useLongPolling('/api/notifications/stream');
  return (
    <div>
      {isConnected ? 'Connected' : 'Disconnected'}
      {notification && <p>{notification.message}</p>}
    </div>
  );
}

Метод 3: Server-Sent Events (SSE)

Сервер отправляет события клиенту по HTTP. Это как WebSocket, но проще и только в одном направлении.

function useServerSentEvents(url: string) {
  const [data, setData] = useState(null);
  const [isConnected, setIsConnected] = useState(false);

  useEffect(() => {
    const eventSource = new EventSource(url);

    eventSource.onopen = () => {
      console.log('SSE connected');
      setIsConnected(true);
    };

    // Слушаем события от сервера
    eventSource.onmessage = (event) => {
      const payload = JSON.parse(event.data);
      setData(payload);
    };

    // Обработка ошибок
    eventSource.onerror = () => {
      console.error('SSE error');
      setIsConnected(false);
      eventSource.close();
    };

    // Слушаем специфичные типы событий
    eventSource.addEventListener('userJoined', (event) => {
      console.log('User joined:', event.data);
    });

    return () => {
      eventSource.close();
    };
  }, [url]);

  return { data, isConnected };
}

// Использование
function LiveChat({ roomId }) {
  const { data: message, isConnected } = useServerSentEvents(
    `/api/rooms/${roomId}/stream`
  );

  return (
    <div>
      {!isConnected && <p>Trying to reconnect...</p>}
      {message && <p>{message.text}</p>}
    </div>
  );
}

// Пример на сервере (Express + Node.js)
app.get('/api/notifications/stream', (req, res) => {
  // Установим правильные заголовки для SSE
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Access-Control-Allow-Origin': '*',
  });

  // Отправляем событие каждую секунду
  const interval = setInterval(() => {
    res.write(`data: ${JSON.stringify({ timestamp: Date.now() })}\n\n`);
  }, 1000);

  // Очистка при закрытии соединения
  req.on('close', () => {
    clearInterval(interval);
    res.end();
  });
});

Метод 4: Iframe + message PostMessage API

Использование скрытого iframe для двустороннего взаимодействия.

// Parent component
function Parent() {
  const [messages, setMessages] = useState<string[]>([]);
  const iframeRef = useRef<HTMLIFrameElement>(null);

  useEffect(() => {
    // Слушаем сообщения от iframe
    const handleMessage = (event: MessageEvent) => {
      if (event.origin !== window.location.origin) return;
      setMessages(prev => [...prev, event.data]);
    };

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, []);

  const sendToIframe = (data: any) => {
    iframeRef.current?.contentWindow?.postMessage(data, window.location.origin);
  };

  return (
    <div>
      <button onClick={() => sendToIframe({ action: 'ping' })}>
        Send to iframe
      </button>
      <iframe
        ref={iframeRef}
        src="/iframe-page.html"
        style={{ display: 'none' }}
      />
      {messages.map((msg, i) => <p key={i}>{msg}</p>)}
    </div>
  );
}

// iframe page
window.addEventListener('message', (event) => {
  if (event.data.action === 'ping') {
    // Отправляем ответ обратно
    window.parent.postMessage('pong', window.location.origin);
  }
});

Метод 5: Fetch с multipart response

Сервер отправляет несколько частей в одном ответе.

async function useMultipartFetch(url: string) {
  const [results, setResults] = useState<any[]>([]);

  useEffect(() => {
    const fetchMultipart = async () => {
      try {
        const response = await fetch(url);
        const reader = response.body?.getReader();
        if (!reader) return;

        let buffer = '';
        const decoder = new TextDecoder();

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;

          // Добавляем данные в буфер
          buffer += decoder.decode(value, { stream: true });

          // Парсим json линии
          const lines = buffer.split('\n');
          buffer = lines.pop() || ''; // Последняя неполная линия

          for (const line of lines) {
            if (line.trim()) {
              try {
                const data = JSON.parse(line);
                setResults(prev => [...prev, data]);
              } catch (e) {
                // Ignore parse errors
              }
            }
          }
        }
      } catch (error) {
        console.error('Multipart fetch error:', error);
      }
    };

    fetchMultipart();
  }, [url]);

  return results;
}

// Использование
function DataStream() {
  const data = useMultipartFetch('/api/data-stream');
  return <div>{data.map(item => <p key={item.id}>{item.name}</p>)}</div>;
}

// Сервер отправляет нджиhаемые json объекты
app.get('/api/data-stream', (req, res) => {
  res.setHeader('Content-Type', 'application/x-ndjson');
  
  let count = 0;
  const interval = setInterval(() => {
    res.write(JSON.stringify({ id: count++, name: `Item ${count}` }) + '\n');
    if (count > 10) {
      clearInterval(interval);
      res.end();
    }
  }, 1000);
});

Сравнение методов

const comparison = {
  'Polling': {
    latency: 'Средняя (5-10сек)',
    bandwidth: 'Высокая',
    complexity: 'Простая',
    realtime: 'Нет',
    pros: ['Просто', 'Работает везде'],
    cons: ['Медленно', 'Много запросов'],
  },
  'Long Polling': {
    latency: 'Низкая (< 1сек)',
    bandwidth: 'Средняя',
    complexity: 'Средняя',
    realtime: 'Почти',
    pros: ['Реал-тайм', 'Не требует특специальной инфраструктуры'],
    cons: ['Может быть нестабильно', 'Требует обработки таймаутов'],
  },
  'SSE': {
    latency: 'Низкая (мс)',
    bandwidth: 'Низкая',
    complexity: 'Средняя',
    realtime: 'Да (односторонний)',
    pros: ['Эффективно', 'Просто реализовать', 'Автоматическое переподключение'],
    cons: ['Только сервер -> клиент', 'Ограничение на количество соединений'],
  },
  'Multipart Fetch': {
    latency: 'Низкая',
    bandwidth: 'Средняя',
    complexity: 'Средняя',
    realtime: 'Да',
    pros: ['Работает везде', 'Двусторонний'],
    cons: ['Нужна обработка ошибок', 'Нет автоматического переподключения'],
  },
};

Практический пример: Chat без WebSocket

function Chat({ roomId }) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const pollingInterval = useRef<NodeJS.Timeout>();

  // Используем Polling для получения сообщений
  useEffect(() => {
    const fetchMessages = async () => {
      try {
        const response = await fetch(`/api/rooms/${roomId}/messages`);
        const newMessages = await response.json();
        setMessages(newMessages);
      } catch (error) {
        console.error('Fetch error:', error);
      }
    };

    fetchMessages();
    pollingInterval.current = setInterval(fetchMessages, 3000);

    return () => {
      if (pollingInterval.current) {
        clearInterval(pollingInterval.current);
      }
    };
  }, [roomId]);

  // Отправка сообщения
  const sendMessage = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!input.trim()) return;

    try {
      const response = await fetch(`/api/rooms/${roomId}/messages`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text: input, userId: getCurrentUserId() }),
      });

      if (response.ok) {
        setInput('');
        // Сразу же загружаем новые сообщения
        const messages = await fetch(`/api/rooms/${roomId}/messages`).then(r => r.json());
        setMessages(messages);
      }
    } catch (error) {
      console.error('Send error:', error);
    }
  };

  return (
    <div className="chat">
      <div className="messages">
        {messages.map(msg => (
          <div key={msg.id} className="message">
            <strong>{msg.userName}</strong>: {msg.text}
          </div>
        ))}
      </div>
      <form onSubmit={sendMessage}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Type message..."
        />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

Рекомендации по выбору

  • Polling: Для простых приложений с задержкой 5-10 секунд
  • Long Polling: Когда нужна задержка < 1 секунды, но WebSocket не доступен
  • SSE: Для потоков данных от сервера (notifications, live feed)
  • Multipart/Fetch: Для прототипов, когда нужна двусторонняя связь
  • WebSocket: Для полноценного реал-тайма (если доступен)

Лучший выбор зависит от требований к latency, пропускной способности и поддержки браузерами.