Vue 3中的高级轮询解决方案:usePoller组合式函数详解

引言

在现代Web应用中,实时数据更新是一个常见需求,在AI场景下会用到很多。虽然WebSocket是实现实时通信的理想选择,但在某些情况下,轮询(Polling)仍然是一种简单有效的替代方案。本文将介绍一个强大的Vue 3组合式函数usePoller,它提供了一种优雅的方式来处理轮询请求,并且包含了错误重试、指数退避等高级特性。

usePoller的核心功能

usePoller是一个专为Vue 3设计的组合式函数,它封装了轮询的复杂逻辑,提供了以下核心功能:

  1. 定时发送请求:按照指定的时间间隔自动发送请求
  2. 智能错误处理:内置错误重试机制,支持指数退避策略
  3. 灵活的配置选项:支持立即执行、最大重试次数等配置
  4. 生命周期管理:组件卸载时自动清理资源
  5. 状态监控:提供轮询状态的实时反馈

usePoller源码

ts 复制代码
import { onScopeDispose, ref } from 'vue'

/**
 * 请求函数类型定义
 * @template T 响应数据类型
 * @template P 请求参数类型
 */
export type RequestFunction<T, P> = (params?: P) => Promise<T>

/**
 * 回调函数类型定义,用于处理成功的响应
 * @template T 响应数据类型
 */
type CallbackFunction<T> = (response: T) => void

/**
 * 错误回调函数类型定义
 * 返回 true 表示继续重试,false 表示停止轮询
 */
type ErrorCallbackFunction = (error: any) => boolean | Promise<boolean>

/**
 * 停止回调函数类型定义
 * @param reason 停止原因:'error'(错误)、'manual'(手动)、'max_retries'(达到最大重试次数)、'component_unmounted'(组件卸载)
 * @param error 当停止原因为错误时,提供错误对象
 */
type StopCallbackFunction = (reason: 'error' | 'manual' | 'max_retries' | 'component_unmounted', error?: any) => void

/**
 * usePoller - 一个用于处理轮询请求的Vue组合式函数
 *
 * 功能:
 * - 定时发送请求并处理响应
 * - 支持错误重试机制,包括指数退避策略
 * - 支持自定义错误处理
 * - 提供轮询状态监控
 * - 组件卸载时自动清理
 *
 * @template T 响应数据类型
 * @template P 请求参数类型,默认为void
 *
 * @returns {Object} 包含poll和stop方法以及isPolling状态的对象
 *
 * @example
 * // 基本使用
 * const { poll, stop } = usePoller<ApiResponse, ApiParams>()
 *
 * poll(
 *   fetchData,           // 请求函数
 *   5000,                // 轮询间隔(毫秒)
 *   handleResponse,      // 响应处理回调
 *   { id: '12345' },     // 请求参数
 *   {
 *     immediate: true,   // 是否立即执行第一次请求
 *     maxRetries: 3,     // 最大重试次数
 *     onError: (error) => {
 *       // 根据错误类型决定是否继续重试
 *       return error.status === 503 // 只有服务暂时不可用时才重试
 *     },
 *     onStop: (reason, error) => {
 *       // 处理轮询停止事件
 *       console.log(`轮询已停止,原因: ${reason}`, error)
 *     }
 *   }
 * )
 *
 * // 手动停止轮询
 * stop()
 */
export function usePoller<T, P = void>() {
  const interval = ref<number>(1000)
  const timer = ref<ReturnType<typeof setInterval> | null>(null)
  const callback = ref<CallbackFunction<T> | null>(null)
  const errorCallback = ref<ErrorCallbackFunction | null>(null)
  const stopCallback = ref<StopCallbackFunction | null>(null)
  const request = ref<RequestFunction<T, P> | null>(null)
  const doImmediate = ref<boolean>(false)
  const maxRetries = ref<number>(3)
  const isPolling = ref<boolean>(false) // 轮询状态标志
  let retryCount = 0
  let requestParams: P | undefined
  let lastError: any = null

  /**
   * 停止轮询
   * @param reason 停止原因
   */
  const stopPolling = (reason: 'error' | 'manual' | 'max_retries' | 'component_unmounted' = 'manual'): void => {
    if (timer.value) {
      clearInterval(timer.value)
      timer.value = null
    }

    const wasPolling = isPolling.value
    isPolling.value = false // 设置轮询状态为停止
    retryCount = 0

    // 只有在之前正在轮询的情况下才触发回调
    if (wasPolling && stopCallback.value) {
      stopCallback.value(reason, reason === 'error' ? lastError : undefined)
    }
  }

  /**
   * 重试函数,处理请求失败的情况
   * @param error 错误对象
   * @returns 是否继续轮询
   */
  const retry = async (error: any): Promise<boolean> => {
    // 保存最后一次错误
    lastError = error

    // 检查轮询是否已停止
    if (!isPolling.value) {
      return false
    }

    // 如果有错误回调,让用户决定是否继续重试
    if (errorCallback.value) {
      const shouldRetry = await errorCallback.value(error)
      if (!shouldRetry) {
        return false // 不再重试,外部会处理停止轮询
      }
    }

    if (retryCount < maxRetries.value) {
      retryCount++
      // 改进的指数退避策略: 2^retryCount * 500ms 并添加随机抖动
      const backoffTime = Math.min(1000 * Math.pow(2, retryCount - 1) + Math.random() * 1000, 30000)
      await new Promise(resolve => setTimeout(resolve, backoffTime))

      // 再次检查轮询是否已停止(可能在等待期间被停止)
      if (!isPolling.value) {
        return false
      }

      if (request.value && callback.value) {
        try {
          const response = await request.value(requestParams)
          callback.value(response)
          return true
        } catch (error) {
          console.error('Error during retry:', error)
          return retry(error)
        }
      }
    } else {
      console.error('Max retry attempts reached')
      // 达到最大重试次数,通知外部
      stopPolling('max_retries')
      // 如果没有错误回调,则在达到最大重试次数后抛出错误
      if (!errorCallback.value) {
        throw error
      }
      return false // 达到最大重试次数,外部会处理停止轮询
    }
    return false
  }

  /**
   * 启动轮询
   */
  const start = async (): Promise<void> => {
    stopPolling() // 先停止之前的轮询
    isPolling.value = true // 设置轮询状态为启动
    lastError = null // 重置错误

    // 如果设置了立即执行,则立即发送第一次请求
    if (doImmediate.value && request.value && callback.value) {
      try {
        // 检查轮询是否已停止
        if (!isPolling.value) {
          return
        }

        const response = await request.value(requestParams)
        callback.value(response)
      } catch (error) {
        console.error(error)
        const shouldContinue = await retry(error)
        if (!shouldContinue) {
          stopPolling('error') // 如果不应继续重试,停止轮询
          return // 直接返回,不启动定时器
        }
      }
      doImmediate.value = false
    }

    // 再次检查轮询是否已停止(可能在immediate执行期间被停止)
    if (!isPolling.value) {
      return
    }

    // 设置定时器,开始轮询
    timer.value = setInterval(async () => {
      // 检查轮询是否已停止
      if (!isPolling.value) {
        stopPolling() // 确保定时器被清除
        return
      }

      if (request.value && callback.value) {
        try {
          const response = await request.value(requestParams)
          callback.value(response)
          // 成功后重置重试计数
          retryCount = 0
        } catch (error) {
          console.error(error)
          const shouldContinue = await retry(error)
          // 如果 retry 返回 false,表示不应继续重试,此时需要停止轮询
          if (!shouldContinue) {
            stopPolling('error')
          }
        }
      }
    }, interval.value)
  }

  /**
   * 开始轮询
   * @param req 请求函数
   * @param delay 轮询间隔(毫秒)
   * @param cb 响应处理回调
   * @param params 请求参数
   * @param options 配置选项
   * @param options.immediate 是否立即执行第一次请求
   * @param options.maxRetries 最大重试次数
   * @param options.onError 错误处理回调
   * @param options.onStop 停止轮询回调
   */
  const poll = (
    req: RequestFunction<T, P>,
    delay: number,
    cb: CallbackFunction<T>,
    params?: P,
    options: {
      immediate?: boolean
      maxRetries?: number
      onError?: ErrorCallbackFunction
      onStop?: StopCallbackFunction
    } = {},
  ): void => {
    const { immediate = false, onError, onStop, maxRetries: retries } = options

    request.value = req
    interval.value = delay
    callback.value = cb
    doImmediate.value = immediate
    requestParams = params

    if (retries !== undefined) {
      maxRetries.value = retries
    }

    if (onError) {
      errorCallback.value = onError
    }

    if (onStop) {
      stopCallback.value = onStop
    }

    start()
  }

  /**
   * 手动停止轮询
   */
  const stop = (): void => {
    stopPolling('manual')
  }

  // 组件卸载时自动清理
  onScopeDispose(() => {
    stopPolling('component_unmounted')
  })

  return {
    poll,
    stop,
    isPolling, // 暴露轮询状态,方便外部检查
  }
}

export default usePoller

基本用法

下面是usePoller的基本用法示例:

vue 复制代码
<script setup>
import { usePoller } from '@/composables/use-poller'
import { ref } from 'vue'

// 定义API请求函数
const fetchData = async (params) => {
  const response = await fetch(`https://api.example.com/data?id=${params.id}`)
  if (!response.ok) {
    throw new Error('请求失败')
  }
  return response.json()
}

// 使用usePoller
const { poll, stop, isPolling } = usePoller()
const data = ref(null)

// 处理响应数据
const handleResponse = (response) => {
  data.value = response
  console.log('数据已更新:', response)
}

// 开始轮询
poll(
  fetchData,           // 请求函数
  5000,                // 轮询间隔(毫秒)
  handleResponse,      // 响应处理回调
  { id: '12345' },     // 请求参数
  {
    immediate: true,   // 立即执行第一次请求
    maxRetries: 3,     // 最大重试次数
  }
)
</script>

<template>
  <div>
    <h1>实时数据</h1>
    <div v-if="isPolling">正在获取数据...</div>
    <pre v-if="data">{{ JSON.stringify(data, null, 2) }}</pre>
    <button @click="stop">停止轮询</button>
  </div>
</template>

使用场景与示例

场景一:实时数据仪表盘

在监控仪表盘中,需要定期刷新数据以显示最新状态。

vue 复制代码
<script setup>
import { usePoller } from '@/composables/use-poller'
import { ref } from 'vue'

const { poll, stop, isPolling } = usePoller()
const dashboardData = ref({})
const loading = ref(false)

// 获取仪表盘数据
const fetchDashboardData = async () => {
  loading.value = true
  try {
    const response = await fetch('/api/dashboard/stats')
    if (!response.ok) throw new Error('获取仪表盘数据失败')
    return await response.json()
  } finally {
    loading.value = false
  }
}

// 处理响应
const updateDashboard = (data) => {
  dashboardData.value = data
}

// 开始轮询,每30秒更新一次
poll(
  fetchDashboardData,
  30000,
  updateDashboard,
  undefined,
  {
    immediate: true,
    onError: (error) => {
      console.error('仪表盘数据更新失败:', error)
      return true // 继续重试
    },
    onStop: (reason) => {
      console.log(`仪表盘数据更新已停止,原因: ${reason}`)
    }
  }
)
</script>

<template>
  <div class="dashboard">
    <div class="status-bar">
      <span v-if="isPolling && !loading">自动更新中</span>
      <span v-if="loading">加载中...</span>
      <button @click="stop">停止自动更新</button>
    </div>
    
    <div class="dashboard-content">
      <!-- 仪表盘内容 -->
      <div v-for="(value, key) in dashboardData" :key="key" class="metric">
        <h3>{{ key }}</h3>
        <div class="value">{{ value }}</div>
      </div>
    </div>
  </div>
</template>

场景二:长时间运行的任务状态检查

当启动一个长时间运行的后台任务时,可以使用轮询来检查任务的进度。

vue 复制代码
<script setup>
import { usePoller } from '@/composables/use-poller'
import { ref } from 'vue'

const { poll, stop, isPolling } = usePoller()
const taskStatus = ref({
  status: 'pending',
  progress: 0,
  result: null
})

// 启动任务
const startTask = async () => {
  const response = await fetch('/api/tasks', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ type: 'data-processing' })
  })
  
  if (!response.ok) throw new Error('启动任务失败')
  const { taskId } = await response.json()
  
  // 开始轮询任务状态
  pollTaskStatus(taskId)
  
  return taskId
}

// 检查任务状态
const checkTaskStatus = async (taskId) => {
  const response = await fetch(`/api/tasks/${taskId}`)
  if (!response.ok) throw new Error('获取任务状态失败')
  return await response.json()
}

// 处理任务状态更新
const handleTaskUpdate = (data) => {
  taskStatus.value = data
  
  // 如果任务已完成或失败,停止轮询
  if (['completed', 'failed'].includes(data.status)) {
    stop()
  }
}

// 开始轮询任务状态
const pollTaskStatus = (taskId) => {
  poll(
    () => checkTaskStatus(taskId),
    2000, // 每2秒检查一次
    handleTaskUpdate,
    undefined,
    {
      immediate: true,
      maxRetries: 5,
      onError: (error) => {
        console.error('检查任务状态失败:', error)
        // 只有在网络错误时继续重试,其他错误停止轮询
        return error.name === 'NetworkError'
      }
    }
  )
}
</script>

<template>
  <div class="task-monitor">
    <h2>任务状态监控</h2>
    
    <div v-if="!isPolling && taskStatus.status === 'pending'">
      <button @click="startTask">启动任务</button>
    </div>
    
    <div v-else class="status-display">
      <div class="status">状态: {{ taskStatus.status }}</div>
      
      <div v-if="taskStatus.status === 'processing'" class="progress">
        <progress :value="taskStatus.progress" max="100"></progress>
        <span>{{ taskStatus.progress }}%</span>
      </div>
      
      <div v-if="taskStatus.status === 'completed'" class="result">
        <h3>任务结果:</h3>
        <pre>{{ JSON.stringify(taskStatus.result, null, 2) }}</pre>
      </div>
      
      <div v-if="taskStatus.status === 'failed'" class="error">
        <h3>任务失败:</h3>
        <p>{{ taskStatus.error }}</p>
      </div>
    </div>
  </div>
</template>

场景三:聊天应用中的消息轮询

在不使用WebSocket的简单聊天应用中,可以使用轮询来获取新消息。

vue 复制代码
<script setup>
import { usePoller } from '@/composables/use-poller'
import { ref, computed } from 'vue'

const { poll, stop, isPolling } = usePoller()
const messages = ref([])
const lastMessageId = ref(0)
const newMessage = ref('')
const userId = ref('user123') // 假设这是当前用户ID

// 获取新消息
const fetchNewMessages = async (params) => {
  const response = await fetch(`/api/chat/messages?after=${params.lastId}`)
  if (!response.ok) throw new Error('获取消息失败')
  return await response.json()
}

// 处理新消息
const handleNewMessages = (data) => {
  if (data.messages && data.messages.length > 0) {
    messages.value = [...messages.value, ...data.messages]
    // 更新最后一条消息的ID
    const maxId = Math.max(...data.messages.map(m => m.id))
    if (maxId > lastMessageId.value) {
      lastMessageId.value = maxId
    }
  }
}

// 发送消息
const sendMessage = async () => {
  if (!newMessage.value.trim()) return
  
  try {
    const response = await fetch('/api/chat/messages', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        content: newMessage.value,
        userId: userId.value
      })
    })
    
    if (!response.ok) throw new Error('发送消息失败')
    
    // 清空输入框
    newMessage.value = ''
    
    // 获取最新消息(包括刚发送的)
    const data = await fetchNewMessages({ lastId: lastMessageId.value })
    handleNewMessages(data)
  } catch (error) {
    console.error('发送消息失败:', error)
  }
}

// 开始轮询新消息
const startPolling = () => {
  poll(
    fetchNewMessages,
    3000, // 每3秒检查一次新消息
    handleNewMessages,
    { lastId: lastMessageId.value },
    {
      immediate: true,
      maxRetries: 10,
      onError: (error) => {
        console.error('获取新消息失败:', error)
        // 网络错误时继续重试
        return true
      },
      onStop: (reason) => {
        if (reason !== 'manual') {
          alert('消息更新已停止,请刷新页面')
        }
      }
    }
  )
}

// 组件挂载时开始轮询
startPolling()

// 计算属性:按时间排序的消息
const sortedMessages = computed(() => {
  return [...messages.value].sort((a, b) => a.timestamp - b.timestamp)
})
</script>

<template>
  <div class="chat-container">
    <div class="chat-header">
      <h2>聊天室</h2>
      <div class="status">
        <span v-if="isPolling" class="online">在线</span>
        <span v-else class="offline">离线</span>
        <button v-if="isPolling" @click="stop">暂停更新</button>
        <button v-else @click="startPolling">恢复更新</button>
      </div>
    </div>
    
    <div class="messages-container">
      <div v-if="messages.length === 0" class="no-messages">
        暂无消息
      </div>
      
      <div v-else class="message-list">
        <div 
          v-for="msg in sortedMessages" 
          :key="msg.id" 
          :class="['message', msg.userId === userId ? 'own' : 'other']"
        >
          <div class="message-header">
            <span class="username">{{ msg.username }}</span>
            <span class="time">{{ new Date(msg.timestamp).toLocaleTimeString() }}</span>
          </div>
          <div class="message-content">{{ msg.content }}</div>
        </div>
      </div>
    </div>
    
    <div class="message-input">
      <input 
        v-model="newMessage" 
        placeholder="输入消息..." 
        @keyup.enter="sendMessage"
      />
      <button @click="sendMessage">发送</button>
    </div>
  </div>
</template>

场景四:带有条件轮询的表单验证

在某些表单中,可能需要在用户输入特定值后验证该值是否可用(如用户名检查)。

vue 复制代码
<script setup>
import { usePoller } from '@/composables/use-poller'
import { ref, watch } from 'vue'

const { poll, stop, isPolling } = usePoller()
const username = ref('')
const validationStatus = ref({
  isChecking: false,
  isValid: null,
  message: ''
})

// 检查用户名是否可用
const checkUsername = async (params) => {
  // 模拟网络延迟
  await new Promise(resolve => setTimeout(resolve, 500))
  
  const response = await fetch(`/api/users/check-username?username=${params.username}`)
  if (!response.ok) throw new Error('验证失败')
  return await response.json()
}

// 处理验证结果
const handleValidationResult = (result) => {
  validationStatus.value = {
    isChecking: false,
    isValid: result.available,
    message: result.available ? '用户名可用' : '用户名已被占用'
  }
  
  // 如果用户名可用或已明确不可用,停止轮询
  if (result.available || result.status === 'final') {
    stop()
  }
}

// 监听用户名变化
watch(username, (newValue) => {
  // 停止之前的轮询
  stop()
  
  // 重置验证状态
  validationStatus.value = {
    isChecking: false,
    isValid: null,
    message: ''
  }
  
  // 用户名长度至少3个字符才开始验证
  if (newValue.length >= 3) {
    validationStatus.value.isChecking = true
    
    // 开始轮询验证
    poll(
      checkUsername,
      2000, // 每2秒检查一次
      handleValidationResult,
      { username: newValue },
      {
        immediate: true,
        maxRetries: 3,
        onError: (error) => {
          console.error('用户名验证失败:', error)
          validationStatus.value.message = '验证服务暂时不可用'
          validationStatus.value.isChecking = false
          return false // 不再重试
        }
      }
    )
  }
})
</script>

<template>
  <div class="username-form">
    <div class="form-group">
      <label for="username">用户名</label>
      <input 
        id="username" 
        v-model="username" 
        type="text" 
        placeholder="请输入用户名"
      />
      
      <div class="validation-status">
        <span v-if="validationStatus.isChecking || isPolling" class="checking">
          正在验证...
        </span>
        <span 
          v-else-if="validationStatus.isValid !== null" 
          :class="validationStatus.isValid ? 'valid' : 'invalid'"
        >
          {{ validationStatus.message }}
        </span>
        <span v-else-if="username.length > 0 && username.length < 3" class="hint">
          用户名至少需要3个字符
        </span>
      </div>
    </div>
  </div>
</template>

场景五:带有智能重试的文件上传状态检查

在大文件上传场景中,客户端发起上传后,可以使用轮询来检查服务器端的处理状态。

vue 复制代码
<script setup>
import { usePoller } from '@/composables/use-poller'
import { ref, computed } from 'vue'

const { poll, stop, isPolling } = usePoller()
const file = ref(null)
const uploadStatus = ref({
  state: 'idle', // idle, uploading, processing, completed, failed
  progress: 0,
  uploadId: null,
  result: null,
  error: null
})

// 计算上传状态文本
const statusText = computed(() => {
  switch (uploadStatus.value.state) {
    case 'idle': return '准备上传';
    case 'uploading': return `上传中 ${uploadStatus.value.progress}%`;
    case 'processing': return '服务器处理中...';
    case 'completed': return '上传完成';
    case 'failed': return `上传失败: ${uploadStatus.value.error}`;
    default: return '';
  }
})

// 处理文件选择
const handleFileChange = (event) => {
  file.value = event.target.files[0]
  // 重置上传状态
  uploadStatus.value = {
    state: 'idle',
    progress: 0,
    uploadId: null,
    result: null,
    error: null
  }
}

// 上传文件
const uploadFile = async () => {
  if (!file.value) return
  
  try {
    uploadStatus.value.state = 'uploading'
    
    // 创建FormData对象
    const formData = new FormData()
    formData.append('file', file.value)
    
    // 上传文件
    const response = await fetch('/api/files/upload', {
      method: 'POST',
      body: formData,
      // 使用上传进度事件
      onUploadProgress: (progressEvent) => {
        const percentCompleted = Math.round(
          (progressEvent.loaded * 100) / progressEvent.total
        )
        uploadStatus.value.progress = percentCompleted
      }
    })
    
    if (!response.ok) throw new Error('上传失败')
    
    const data = await response.json()
    uploadStatus.value.uploadId = data.uploadId
    uploadStatus.value.state = 'processing'
    
    // 开始轮询检查处理状态
    pollProcessingStatus(data.uploadId)
  } catch (error) {
    console.error('文件上传失败:', error)
    uploadStatus.value.state = 'failed'
    uploadStatus.value.error = error.message
  }
}

// 检查文件处理状态
const checkProcessingStatus = async (params) => {
  const response = await fetch(`/api/files/status/${params.uploadId}`)
  if (!response.ok) throw new Error('获取处理状态失败')
  return await response.json()
}

// 处理状态更新
const handleStatusUpdate = (data) => {
  // 更新处理进度
  if (data.state === 'processing') {
    uploadStatus.value.progress = data.progress || 0
  } 
  // 处理完成
  else if (data.state === 'completed') {
    uploadStatus.value.state = 'completed'
    uploadStatus.value.result = data.result
    stop() // 停止轮询
  } 
  // 处理失败
  else if (data.state === 'failed') {
    uploadStatus.value.state = 'failed'
    uploadStatus.value.error = data.error
    stop() // 停止轮询
  }
}

// 开始轮询处理状态
const pollProcessingStatus = (uploadId) => {
  poll(
    checkProcessingStatus,
    3000, // 每3秒检查一次
    handleStatusUpdate,
    { uploadId },
    {
      immediate: true,
      maxRetries: 5,
      // 智能重试策略
      onError: (error) => {
        console.error('检查处理状态失败:', error)
        
        // 如果是服务器暂时不可用,继续重试
        if (error.status === 503) {
          return true
        }
        
        // 如果是其他错误,停止轮询并更新状态
        uploadStatus.value.state = 'failed'
        uploadStatus.value.error = '处理状态检查失败: ' + error.message
        return false
      },
      onStop: (reason, error) => {
        if (reason === 'max_retries') {
          uploadStatus.value.state = 'failed'
          uploadStatus.value.error = '处理状态检查超时,请稍后查看结果'
        }
      }
    }
  )
}

// 取消上传
const cancelUpload = () => {
  stop() // 停止轮询
  
  // 如果有上传ID,通知服务器取消
  if (uploadStatus.value.uploadId) {
    fetch(`/api/files/cancel/${uploadStatus.value.uploadId}`, {
      method: 'POST'
    }).catch(error => {
      console.error('取消上传失败:', error)
    })
  }
  
  // 重置状态
  uploadStatus.value = {
    state: 'idle',
    progress: 0,
    uploadId: null,
    result: null,
    error: null
  }
  
  // 清除文件选择
  file.value = null
  document.getElementById('file-input').value = ''
}
</script>

<template>
  <div class="file-uploader">
    <h2>文件上传</h2>
    
    <div class="upload-form">
      <div class="file-input">
        <input 
          id="file-input"
          type="file" 
          @change="handleFileChange"
          :disabled="uploadStatus.state !== 'idle'"
        />
        <div v-if="file" class="file-info">
          <span>{{ file.name }}</span>
          <span>({{ (file.size / 1024).toFixed(2) }} KB)</span>
        </div>
      </div>
      
      <div class="actions">
        <button 
          @click="uploadFile" 
          :disabled="!file || uploadStatus.state !== 'idle'"
        >
          上传
        </button>
        <button 
          @click="cancelUpload" 
          :disabled="uploadStatus.state === 'idle' || uploadStatus.state === 'completed'"
        >
          取消
        </button>
      </div>
    </div>
    
    <div v-if="uploadStatus.state !== 'idle'" class="status-container">
      <div class="status-text">{{ statusText }}</div>
      
      <div v-if="['uploading', 'processing'].includes(uploadStatus.state)" class="progress-bar">
        <div class="progress" :style="{width: `${uploadStatus.progress}%`}"></div>
      </div>
      
      <div v-if="uploadStatus.state === 'completed'" class="result">
        <h3>处理结果:</h3>
        <div class="result-content">
          <a :href="uploadStatus.result.url" target="_blank">查看文件</a>
          <div class="metadata">
            <div>文件ID: {{ uploadStatus.result.fileId }}</div>
            <div>处理时间: {{ uploadStatus.result.processedAt }}</div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

高级使用技巧

1. 动态调整轮询间隔

在某些情况下,可能需要根据服务器负载或响应时间动态调整轮询间隔。

javascript 复制代码
// 初始轮询间隔为5秒
let pollInterval = 5000

// 根据响应时间动态调整轮询间隔
const adaptivePolling = async () => {
  const { poll, stop } = usePoller()
  
  const fetchWithTiming = async (params) => {
    const startTime = Date.now()
    const result = await fetchData(params)
    const responseTime = Date.now() - startTime
    
    // 根据响应时间调整下一次轮询间隔
    if (responseTime > 1000) {
      // 服务器响应慢,增加间隔
      pollInterval = Math.min(pollInterval * 1.5, 30000) // 最大30秒
    } else if (responseTime < 200 && pollInterval > 2000) {
      // 服务器响应快,减少间隔
      pollInterval = Math.max(pollInterval * 0.8, 2000) // 最小2秒
    }
    
    return result
  }
  
  const handleResponse = (data) => {
    // 处理数据...
    
    // 停止当前轮询并以新的间隔重新开始
    stop()
    poll(fetchWithTiming, pollInterval, handleResponse, params, { immediate: false })
  }
  
  // 开始初始轮询
  poll(fetchWithTiming, pollInterval, handleResponse, params, { immediate: true })
}

2. 条件轮询

只在特定条件满足时才进行轮询:

javascript 复制代码
import { usePoller } from '@/composables/use-poller'
import { ref, watch } from 'vue'

const { poll, stop, isPolling } = usePoller()
const shouldPoll = ref(false)
const data = ref(null)

// 监听条件变化
watch(shouldPoll, (newValue) => {
  if (newValue && !isPolling.value) {
    // 开始轮询
    poll(fetchData, 5000, handleResponse, params, { immediate: true })
  } else if (!newValue && isPolling.value) {
    // 停止轮询
    stop()
  }
})

// 根据用户交互或其他条件改变shouldPoll的值
const startDataUpdates = () => {
  shouldPoll.value = true
}

const stopDataUpdates = () => {
  shouldPoll.value = false
}

3. 多轮询协调

在复杂应用中,可能需要同时管理多个轮询:

javascript 复制代码
import { usePoller } from '@/composables/use-poller'
import { reactive } from 'vue'

// 创建多个轮询实例
const userDataPoller = usePoller()
const notificationsPoller = usePoller()
const systemStatusPoller = usePoller()

// 统一管理所有轮询
const pollers = reactive({
  userData: userDataPoller,
  notifications: notificationsPoller,
  systemStatus: systemStatusPoller
})

// 启动所有轮询
const startAllPollers = () => {
  pollers.userData.poll(fetchUserData, 10000, handleUserData, null, { immediate: true })
  pollers.notifications.poll(fetchNotifications, 5000, handleNotifications, null, { immediate: true })
  pollers.systemStatus.poll(fetchSystemStatus, 30000, handleSystemStatus, null, { immediate: true })
}

// 停止所有轮询
const stopAllPollers = () => {
  Object.values(pollers).forEach(poller => poller.stop())
}

// 根据网络状态管理轮询
window.addEventListener('online', startAllPollers)
window.addEventListener('offline', stopAllPollers)

性能优化建议

使用轮询时,需要注意以下性能优化点:

  1. 合理设置轮询间隔:间隔太短会增加服务器负载,间隔太长会影响实时性
  2. 使用条件轮询:只在需要时才启动轮询,不需要时及时停止
  3. 实现指数退避:在错误情况下,逐渐增加重试间隔
  4. 设置最大重试次数:避免无限重试导致资源浪费
  5. 组件卸载时清理:确保在组件销毁时停止轮询

结论

usePoller组合式函数为Vue 3应用提供了一种强大而灵活的轮询解决方案。通过封装复杂的轮询逻辑,它使开发者能够轻松实现各种实时数据更新场景,同时内置的错误处理和重试机制确保了应用的稳定性和可靠性。

虽然在许多情况下WebSocket可能是更好的实时通信选择,但轮询仍然是一种简单、兼容性好的替代方案,特别适用于不需要高频率更新的场景。通过本文介绍的usePoller和各种使用场景,你应该能够在自己的Vue 3应用中轻松实现高质量的轮询功能。

相关推荐
鸿是江边鸟,曾是心上人22 分钟前
echarts使用记录
javascript·ecmascript·echarts
好_快25 分钟前
Lodash源码阅读-nth
前端·javascript·源码阅读
maybe啊30 分钟前
js 使用 Web Workers 来实现一个精确的倒计时,即使ios手机锁屏或页面进入后台,倒计时也不会暂停。
开发语言·前端·javascript
好_快32 分钟前
Lodash 源码阅读-baseNth
前端·javascript·源码阅读
好_快34 分钟前
Lodash源码阅读-join
前端·javascript·源码阅读
好_快37 分钟前
Lodash源码阅读-isIndex
前端·javascript·源码阅读
好_快40 分钟前
Lodash源码阅读-reverse
前端·javascript·源码阅读
Gazer_S2 小时前
【基于 SSE 协议与 EventSource 实现 AI 对话的流式交互】
前端·javascript·人工智能·交互
咔咔库奇2 小时前
【three.js】三维交互核心技术 - 射线检测与物理级拖拽实现
开发语言·javascript·ecmascript
银迢迢3 小时前
如何创建一个Vue项目
前端·javascript·vue.js