在前端使用 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 (
    ...
  )
}