Skip to content

前端轮询接口的实现方案与最佳实践 ​

在前端开发中,我们经常需要实时获取服务器的最新数据。轮询是一种常见的实现方式,本文将介绍几种主要的轮询方案,并分析它们的优缺点。

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,代码更清晰

最佳实践建议 ​

  1. 选择合适的轮询方案

    • 对实时性要求不高:使用短轮询
    • 需要实时性且服务器支持:优先使用SSE
    • 需要双向通信:考虑WebSocket
  2. 错误处理

    • 实现错误重试机制
    • 设置合理的超时时间
    • 添加错误日志
  3. 性能优化

    • 合理设置轮询间隔
    • 在组件卸载时清理轮询
    • 考虑网络状况动态调整间隔
  4. 用户体验

    • 添加加载状态提示
    • 实现断线重连提示
    • 提供手动刷新选项

通过合理使用这些轮询方案,我们可以根据具体需求实现更好的实时数据更新功能。