Files
kazoottt-blog-v2/src/content/note/在前端使用abort取消请求.md
2025-03-04 11:07:00 +00:00

6.4 KiB
Raw Blame History

toAstro, astroType, published, toWexin, toJuejin, toZhihu, category, title, date, author, tags, finished, slug, description, NotionID-notionnext, link-notionnext, date_created, date_modified
toAstro astroType published toWexin toJuejin toZhihu category title date author tags finished slug description NotionID-notionnext link-notionnext date_created date_modified
true null true null null null null 在前端使用abort取消请求 2024-04-17T00:00:00.000Z KazooTTT
前端
实践
网络
abortController
request
true use-abort-on-the-frontend-to-cancel-the-request 本文介绍了在不同前端框架中如何取消HTTP请求的方法。在原生JavaScript中使用AbortController接口来实现请求的取消。在React中通过useState和useEffect钩子管理AbortController的状态并在组件卸载时自动取消请求。在SolidJS中利用createSignal和onCleanup来处理AbortController确保在需要时可以中断请求。这些方法都通过创建AbortController实例并在fetch请求中使用其signal属性来控制请求的取消。 801e2fa1-dfa9-4b4f-b579-ef7f6658b9d3 https://kazoottt.notion.site/abort-801e2fa1dfa94b4fb579ef7f6658b9d3 20250104 20250304

在前端使用 abort 取消请求

举个例子,在写 llm 的 chat 的时候,经常会出现需要取消请求的场景。

如何在前端取消请求,涉及到一个接口:AbortController.AbortController() - Web API 接口参考 | MDN

在原生的 js 的写法,参考 mdn 的写法。

let controller
const url = "video.mp4"

const downloadBtn = document.querySelector(".download")
const abortBtn = document.querySelector(".abort")

downloadBtn.addEventListener("click", fetchVideo)

abortBtn.addEventListener("click", () => {
  if (controller) {
    controller.abort()
    controller = null
    console.log("Download aborted")
  }
})

function fetchVideo() {
  controller = new AbortController()
  const signal = controller.signal
  fetch(url, { signal })
    .then((response) => {
      console.log("Download complete", response)
    })
    .catch((err) => {
      console.error(`Download error: ${err.message}`)
    })
}

在 react 的写法

import React, { useState, useEffect } from "react"

const RequestComponent = () => {
  const [responseData, setResponseData] = useState(null)
  const [error, setError] = useState(null)
  const [loading, setLoading] = useState(false)
  const [controller, setController] = useState(null)

  useEffect(() => {
    // 组件被卸载的时候,取消请求
    return () => {
      if (controller) {
        controller.abort()
      }
    }
  }, [controller])

  const fetchData = async () => {
    setLoading(true)
    setError(null)

    const abortController = new AbortController()
    setController(abortController)

    try {
      const response = await fetch("https://api.example.com/data", {
        signal: abortController.signal,
      })

      if (!response.ok) {
        throw new Error("Network response was not ok")
      }

      const data = await response.json()
      setResponseData(data)
    } catch (error) {
      if (error.name === "AbortError") {
        console.log("Request canceled by user")
      } else {
        setError(error)
      }
    } finally {
      setLoading(false)
    }
  }

  const cancelRequest = () => {
    if (controller) {
      controller.abort()
    }
  }

  return (
    <div>
      <button onClick={fetchData} disabled={loading}>
        {loading ? "Loading..." : "Fetch Data"}
      </button>
      <button onClick={cancelRequest} disabled={!loading}>
        Cancel Request
      </button>
      {error && <div>Error: {error.message}</div>}
      {responseData && <div>Data: {JSON.stringify(responseData)}</div>}
    </div>
  )
}

export default RequestComponent

在 solidjs 中的写法,可以参考 diu 老师的 GitHub - anse-app/chatgpt-demo: Minimal web UI for ChatGPT.

import { Index, Show, createEffect, createSignal, onCleanup, onMount } from 'solid-js'
import { useThrottleFn } from 'solidjs-use'
import { generateSignature } from '@/utils/auth'
import IconClear from './icons/Clear'
import MessageItem from './MessageItem'
import SystemRoleSettings from './SystemRoleSettings'
import ErrorMessageItem from './ErrorMessageItem'
import type { ChatMessage, ErrorMessage } from '@/types'

export default () => {
  const [controller, setController] = createSignal<AbortController>(null)


  const requestWithLatestMessage = async() => {
    setLoading(true)
    setCurrentAssistantMessage('')
    setCurrentError(null)
    const storagePassword = localStorage.getItem('pass')
    try {
      const controller = new AbortController()
      setController(controller)
      const requestMessageList = messageList().slice(-maxHistoryMessages)
      if (currentSystemRoleSettings()) {
        requestMessageList.unshift({
          role: 'system',
          content: currentSystemRoleSettings(),
        })
      }
      const timestamp = Date.now()
      const response = await fetch('/api/generate', {
        method: 'POST',
        body: JSON.stringify({
          messages: requestMessageList,
          time: timestamp,
          pass: storagePassword,
          sign: await generateSignature({
            t: timestamp,
            m: requestMessageList?.[requestMessageList.length - 1]?.content || '',
          }),
          temperature: temperature(),
        }),
        signal: controller.signal,
      })
      if (!response.ok) {
        const error = await response.json()
        console.error(error.error)
        setCurrentError(error.error)
        throw new Error('Request failed')
      }
      const data = response.body
      if (!data)
        throw new Error('No data')

      const reader = data.getReader()
      const decoder = new TextDecoder('utf-8')
      let done = false

      while (!done) {
        const { value, done: readerDone } = await reader.read()
        if (value) {
          const char = decoder.decode(value)
          if (char === '\n' && currentAssistantMessage().endsWith('\n'))
            continue

          if (char)
            setCurrentAssistantMessage(currentAssistantMessage() + char)

          isStick() && instantToBottom()
        }
        done = readerDone
      }
    } catch (e) {
      console.error(e)
      setLoading(false)
      setController(null)
      return
    }
    archiveCurrentMessage()
    isStick() && instantToBottom()
  }

 const stopStreamFetch = () => {
  if (controller()) {
   controller().abort()
   ...
  }
 }


  return (
    ...
  )
}