← Назад к вопросам
Как сделать двустороннее взаимодействие без 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, пропускной способности и поддержки браузерами.