12k
All articles

使用 AbortController 取消进行中的 Fetch 请求

通过 AbortController 与 AbortSignal 取消 fetch 请求,处理搜索输入、组件卸载及超时场景,避免界面出现过时数据。

OpenReplay Team
OpenReplay Team
使用 AbortController 取消进行中的 Fetch 请求

现代 Web 应用程序经常发起用户可能不需要完成的 HTTP 请求。用户在搜索框中输入会产生多个请求,但只有最新的请求才重要。用户离开页面会使该页面的所有待处理请求变得无关紧要。如果没有适当的取消机制,这些不必要的请求会浪费带宽、消耗服务器资源,并可能导致过时的数据出现在您的 UI 中。

AbortController API 提供了一种清洁、标准化的方式来取消 fetch 请求和其他异步操作。本文将向您展示如何使用 AbortController 实现请求取消,涵盖搜索防抖、组件清理和超时处理等实用模式。

关键要点

  • AbortController 创建一个信号,fetch 会监听该信号以获取取消事件
  • 始终在 catch 块中检查 AbortError,以区分取消和失败
  • 在搜索界面中取消先前的请求以防止竞态条件
  • 在框架组件中使用清理函数在卸载时取消请求
  • 将 AbortController 与 setTimeout 结合使用以实现请求超时功能
  • 每个 AbortController 都是一次性使用的 - 为重试逻辑创建新的控制器
  • Node.js 18+ 包含原生 AbortController 支持

AbortController 和 AbortSignal 的作用

AbortController 创建一个控制器对象,通过关联的 AbortSignal 管理取消操作。控制器只有一个任务:在您想要取消操作时调用 abort()。信号充当通信通道,fetch 和其他 API 会监听该通道的取消事件。

const controller = new AbortController()
const signal = controller.signal

// 信号开始时未被中止
console.log(signal.aborted) // false

// 调用 abort() 改变信号的状态
controller.abort()
console.log(signal.aborted) // true

当您将信号传递给 fetch 并稍后调用 abort() 时,fetch promise 会以名为 AbortErrorDOMException 拒绝。这让您能够区分取消和实际的网络故障。

基本 AbortController 实现

要取消 fetch 请求,创建一个 AbortController,将其信号传递给 fetch,然后在需要时调用 abort()

const controller = new AbortController()

fetch('/api/data', { signal: controller.signal })
  .then(response => response.json())
  .then(data => console.log('Data received:', data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Request was cancelled')
    } else {
      console.error('Request failed:', error)
    }
  })

// 取消请求
controller.abort()

关键点:

  • 在发起请求之前创建控制器
  • controller.signal 传递给 fetch 选项
  • 调用 controller.abort() 来取消
  • 在 catch 块中检查 AbortError

如何在用户输入时取消搜索请求

搜索界面通常在每次按键时触发请求。如果没有取消机制,慢响应可能会乱序到达,显示过时查询的结果。以下是如何取消先前搜索请求的方法:

let searchController = null

function performSearch(query) {
  // 取消任何现有的搜索
  if (searchController) {
    searchController.abort()
  }

  // 为此次搜索创建新控制器
  searchController = new AbortController()

  fetch(`/api/search?q=${encodeURIComponent(query)}`, {
    signal: searchController.signal
  })
    .then(response => response.json())
    .then(results => {
      console.log('Search results:', results)
      updateSearchUI(results)
    })
    .catch(error => {
      if (error.name === 'AbortError') {
        console.log('Search cancelled')
      } else {
        console.error('Search failed:', error)
        showSearchError()
      }
    })
}

// 用法:每次新搜索都会取消之前的搜索
performSearch('javascript')
performSearch('javascript frameworks') // 取消 'javascript' 搜索

这种模式确保只有最新的搜索结果出现在您的 UI 中,防止较慢的请求覆盖较快请求的竞态条件。

在组件卸载时取消请求

在前端框架中,组件通常会发起 fetch 请求,这些请求应该在组件卸载时被取消。这可以防止内存泄漏和尝试更新已卸载组件的错误。

React 示例

import { useEffect, useState } from 'react'

function UserProfile({ userId }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    const controller = new AbortController()

    async function fetchUser() {
      try {
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal
        })
        
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`)
        }
        
        const userData = await response.json()
        setUser(userData)
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('Failed to fetch user:', error)
        }
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false)
        }
      }
    }

    fetchUser()

    // 清理函数取消请求
    return () => controller.abort()
  }, [userId])

  if (loading) return <div>Loading...</div>
  return <div>{user?.name}</div>
}

Vue 示例

export default {
  data() {
    return {
      user: null,
      controller: null
    }
  },
  async mounted() {
    this.controller = new AbortController()
    
    try {
      const response = await fetch(`/api/users/${this.userId}`, {
        signal: this.controller.signal
      })
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }
      
      this.user = await response.json()
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('Failed to fetch user:', error)
      }
    }
  },
  beforeUnmount() {
    if (this.controller) {
      this.controller.abort()
    }
  }
}

使用 AbortController 实现请求超时

网络请求可能会无限期挂起。AbortController 结合 setTimeout 提供了一个清洁的超时机制:

function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
  const controller = new AbortController()
  
  const timeoutId = setTimeout(() => {
    controller.abort()
  }, timeoutMs)

  return fetch(url, {
    ...options,
    signal: controller.signal
  }).finally(() => {
    clearTimeout(timeoutId)
  })
}

// 用法
fetchWithTimeout('/api/slow-endpoint', {}, 3000)
  .then(response => response.json())
  .then(data => console.log('Data:', data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Request timed out')
    } else {
      console.error('Request failed:', error)
    }
  })

对于可重用的超时逻辑,创建一个工具函数:

function createTimeoutSignal(timeoutMs) {
  const controller = new AbortController()
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
  
  // 当信号被使用时清理超时
  controller.signal.addEventListener('abort', () => {
    clearTimeout(timeoutId)
  }, { once: true })
  
  return controller.signal
}

// 用法
fetch('/api/data', { signal: createTimeoutSignal(5000) })
  .then(response => response.json())
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Request timed out or was cancelled')
    }
  })

被取消请求的正确错误处理

始终区分取消和真正的错误。AbortError 表示故意取消,而不是失败:

async function handleRequest(url) {
  const controller = new AbortController()
  
  try {
    const response = await fetch(url, { signal: controller.signal })
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }
    
    return await response.json()
  } catch (error) {
    if (error.name === 'AbortError') {
      // 这在我们取消时是预期的 - 不要记录为错误
      console.log('Request was cancelled')
      return null
    }
    
    // 这是需要处理的实际错误
    console.error('Request failed:', error)
    throw error
  }
}

对于有错误跟踪的应用程序,从错误报告中排除 AbortError:

.catch(error => {
  if (error.name === 'AbortError') {
    // 不要向错误跟踪报告取消操作
    return
  }
  
  // 报告实际错误
  errorTracker.captureException(error)
  throw error
})

管理多个请求

在处理多个并发请求时,您可能需要一次性取消所有请求或单独管理它们:

class RequestManager {
  constructor() {
    this.controllers = new Map()
  }
  
  async fetch(key, url, options = {}) {
    // 取消具有相同键的现有请求
    this.cancel(key)
    
    const controller = new AbortController()
    this.controllers.set(key, controller)
    
    try {
      const response = await fetch(url, {
        ...options,
        signal: controller.signal
      })
      return response
    } finally {
      this.controllers.delete(key)
    }
  }
  
  cancel(key) {
    const controller = this.controllers.get(key)
    if (controller) {
      controller.abort()
      this.controllers.delete(key)
    }
  }
  
  cancelAll() {
    for (const controller of this.controllers.values()) {
      controller.abort()
    }
    this.controllers.clear()
  }
}

// 用法
const requestManager = new RequestManager()

// 这些请求可以独立管理
requestManager.fetch('user-profile', '/api/user/123')
requestManager.fetch('user-posts', '/api/user/123/posts')

// 取消特定请求
requestManager.cancel('user-profile')

// 取消所有待处理的请求
requestManager.cancelAll()

浏览器支持和 Node.js 兼容性

AbortController 具有出色的浏览器支持:

  • Chrome 66+
  • Firefox 57+
  • Safari 12.1+
  • Edge 16+

对于 Node.js,AbortController 在 Node 18+ 中原生可用。对于早期版本,使用 abort-controller polyfill:

npm install abort-controller
// 对于 Node.js < 18
const { AbortController } = require('abort-controller')

// 正常使用
const controller = new AbortController()

总结

AbortController 为现代 Web 应用程序中取消 fetch 请求提供了一种清洁、标准化的方式。关键模式是:在每个请求之前创建一个控制器,将其信号传递给 fetch,并在需要取消时调用 abort()。始终将 AbortError 与真正的网络故障分开处理,以避免将故意取消视为错误。

最常见的用例——搜索请求管理、组件清理和超时处理——遵循您可以根据特定需求调整的直接模式。通过适当的实现,请求取消通过防止不必要的网络活动和过时数据更新来改善用户体验和应用程序性能。

准备在您的应用程序中实现请求取消了吗?从最常见用例的基本模式开始,然后根据需要扩展到更复杂的场景。在适当的请求管理方面的投资在应用程序性能和用户体验方面会带来回报。

常见问题

调用 abort() 后可以重用 AbortController 吗?

不可以,一旦调用 abort(),控制器的信号将永久保持在中止状态。您必须为每个新请求或重试尝试创建一个新的 AbortController。

调用 abort() 会阻止网络请求到达服务器吗?

不会,abort() 只会阻止您的 JavaScript 代码处理响应。如果请求已经通过网络发送,服务器可能仍会接收并处理它。

如何一次取消多个 fetch 请求?

创建多个控制器并将它们存储在数组或 Map 中,然后对每个控制器调用 abort()。或者,使用单个控制器并将其信号传递给多个 fetch 调用 - 调用一次 abort() 将取消所有请求。

如果在 fetch 已经完成后调用 abort() 会发生什么?

什么都不会发生。如果请求已经成功完成或因其他原因失败,abort() 调用将被忽略。

AbortController 可以与 async/await 语法一起使用吗?

可以,AbortController 在 Promise 链和 async/await 中的工作方式完全相同。当被取消时,fetch promise 将以 AbortError 拒绝,您可以使用 try/catch 块捕获它。

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers

We use cookies to improve your experience. By using our site, you accept cookies.