前端轮询接口的实现方案与最佳实践
在前端开发中,我们经常需要实时获取服务器的最新数据。轮询是一种常见的实现方式,本文将介绍几种主要的轮询方案,并分析它们的优缺点。
1. 短轮询 (Short Polling)
最简单直接的轮询方式,通过定时器定期发送请求。
javascript
let intervalId;
function startShortPolling() {
intervalId = setInterval(async () => {
try {
const response = await fetch('/api/getData');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// 处理数据
console.log('收到数据:', data);
} catch (error) {
console.error('轮询出错:', error);
}
}, 1000); // 每秒请求一次
}
function stopShortPolling() {
clearInterval(intervalId);
}上面使用setInterval() 实现的轮询存在一些问题,比如:
- 浏览器可能会因为请求次数过多而崩溃
- 上一次请求未完成时,下一次请求就直接发送,导致请求堆积
轮询机制:从 setInterval 改为 递归 setTimeout 优势: ✅ 自然避免请求堆积 ✅ API 重试期间不会发起新请求 ✅ 更清晰的控制流
js
// ❌ 旧方式:固定间隔,不等待上一次完成
setInterval(async () => {
await getVideoResult(uuid); // 可能需要 7 秒(包括重试)
}, 5000); // 但 5 秒后就发起新请求 → 堆积
// ✅ 新方式:等待上一次完成后才发起下一次
const pollOnce = async (uuid) => {
const result = await getVideoResult(uuid); // 等待完成(包括重试)
if (still_processing) {
// 只有在上一次完成后,才设置下一次
setTimeout(() => pollOnce(uuid), 5000);
}
};js
// 递归轮询函数:等待上一次请求完成后再发起下一次
const pollOnce = useCallback(async (taskUuid, startTime) => {
// 检查全局超时(5 分钟)
const elapsed = Date.now() - startTime;
if (elapsed > 300000) {
console.warn('⏱️ Global timeout reached (5 minutes)');
clearTimers();
setLoading(false);
tip({
type: 'warning',
content: 'Generation timeout. Please check the gallery later.',
});
return;
}
// 防止重复请求(轮询锁)
if (isPollingRef.current) {
console.warn('⚠️ Previous poll is still in progress, skipping...');
return;
}
isPollingRef.current = true;
try {
console.log('🔍 Polling video status:', taskUuid);
// API 层已经内置了重试机制,这里只需要处理业务逻辑
const result = await getVideoResult(taskUuid);
setVideoResult(result);
console.log('📊 Poll result:', result.status);
// 更新进度(根据状态)
if (isVideoProcessing(result.status)) {
setProgress(prev => Math.min(prev + 10, 90));
// 继续轮询:5 秒后再次查询
isPollingRef.current = false;
pollTimeoutRef.current = setTimeout(() => {
pollOnce(taskUuid, startTime);
}, 5000);
} else if (isVideoCompleted(result.status)) {
// 生成成功
setProgress(100);
setLoading(false);
clearTimers();
console.log('✅ Video generation completed:', taskUuid);
// 触发 Recoil 状态更新,通知 VideoGallery 刷新
setRefreshTrigger(prev => prev + 1);
tip({
type: 'success',
content: 'Video generated successfully!',
});
} else if (isVideoFailed(result.status)) {
// 生成失败
const errorMsg = result.error_msg || 'Video generation failed. Please try again.';
setLoading(false);
clearTimers();
tip({
type: 'error',
content: errorMsg,
});
} else {
// 未知状态,继续轮询
console.warn('⚠️ Unknown status:', result.status);
isPollingRef.current = false;
pollTimeoutRef.current = setTimeout(() => {
pollOnce(taskUuid, startTime);
}, 5000);
}
} catch (err) {
// 如果 API 层重试后仍然失败,说明是严重错误,停止轮询
console.error('❌ Polling failed after retries:', err);
setLoading(false);
clearTimers();
tip({
type: 'error',
content: err.message || 'Failed to query video status. Please check your network.',
});
}
}, [clearTimers, tip, setRefreshTrigger]);
// 开始轮询
const startPolling = useCallback((taskUuid) => {
clearTimers(); // 清除旧的定时器
console.log('🔄 Start polling:', taskUuid);
const startTime = Date.now();
// 设置全局超时(5 分钟)
globalTimeoutRef.current = setTimeout(() => {
clearTimers();
setLoading(false);
tip({
type: 'warning',
content: 'Generation timeout. Please check the gallery later.',
});
}, 300000);
// 立即开始第一次轮询
pollOnce(taskUuid, startTime);
}, [clearTimers, tip, pollOnce]);2. 长轮询 (Long Polling)
相比短轮询,长轮询可以减少无效请求,服务端在有新数据时才返回响应。
javascript
let xhr;
function startLongPolling() {
xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log('收到数据:', data);
}
// 无论成功失败,立即发起下一次请求
startLongPolling();
}
};
xhr.open('GET', '/api/getData');
xhr.send();
}
function stopLongPolling() {
xhr && xhr.abort();
}优点:
- 相比短轮询服务器压力小
- 实时性较好
缺点:
- 服务器需要保持连接
- 连接可能意外断开
3. SSE (Server-Sent Events)
服务器推送事件,建立单向连接,服务器可以主动推送数据。
javascript
// 客户端代码
const eventSource = new EventSource('/api/getData');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('收到SSE数据:', data);
};
eventSource.onerror = function(error) {
console.error('SSE错误:', error);
eventSource.close();
};
// 服务端代码 (Node.js)
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/api/getData') {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
const intervalId = setInterval(() => {
const data = {
timestamp: new Date().toISOString(),
value: Math.random()
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
req.on('close', () => clearInterval(intervalId));
}
});优点:
- 服务器可以主动推送
- 自动重连机制
- 比WebSocket更轻量
缺点:
- 单向通信
- 某些浏览器可能不支持
4. 基于 Axios 的长轮询
结合现代化HTTP客户端的长轮询实现,支持请求取消等高级特性。
javascript
function longPolling(url, callback) {
const source = axios.CancelToken.source();
async function sendRequest() {
try {
const response = await axios.get(url, {
cancelToken: source.token,
timeout: 30000 // 30秒超时
});
callback(response.data);
sendRequest(); // 继续下一次请求
} catch (error) {
if (axios.isCancel(error)) {
console.log('轮询被取消');
} else {
console.error('轮询错误:', error);
setTimeout(sendRequest, 5000); // 错误后5秒重试
}
}
}
sendRequest();
return () => source.cancel('用户取消轮询');
}
// 使用示例
const cancel = longPolling('/api/getData', (data) => {
console.log('收到数据:', data);
});
// 需要时取消轮询
// cancel();优点:
- 支持请求取消
- 超时处理
- 错误重试机制
- 使用Promise,代码更清晰
最佳实践建议
选择合适的轮询方案
- 对实时性要求不高:使用短轮询
- 需要实时性且服务器支持:优先使用SSE
- 需要双向通信:考虑WebSocket
错误处理
- 实现错误重试机制
- 设置合理的超时时间
- 添加错误日志
性能优化
- 合理设置轮询间隔
- 在组件卸载时清理轮询
- 考虑网络状况动态调整间隔
用户体验
- 添加加载状态提示
- 实现断线重连提示
- 提供手动刷新选项
通过合理使用这些轮询方案,我们可以根据具体需求实现更好的实时数据更新功能。