如何在Next.js ChatGPT示例中添加取消按钮终止流式请求循环
在Next.js AI ChatGPT示例中终止流式请求的正确方法
你之前尝试用cancelled状态变量终止while循环无效,核心原因是React状态更新的闭包特性:循环运行在组件某次渲染的闭包中,引用的是该次渲染时的cancelled初始值,后续状态更新不会同步到这个闭包里的变量,因此循环无法感知到状态变化。
正确的实现方式是使用AbortController直接中断底层的流式网络请求,具体步骤如下:
1. 初始化AbortController引用
用useRef保存AbortController实例,避免组件重渲染时丢失引用:
import { useRef, useState } from 'react'; export default function ChatComponent() { // 用于控制请求取消的控制器 const abortControllerRef = useRef(new AbortController()); const [isCancelled, setIsCancelled] = useState(false); // 其他状态(如聊天消息、加载状态等) const [messages, setMessages] = useState([]);
2. 发起请求时关联Signal
在调用fetch发起流式请求时,将控制器的signal传入请求配置,让请求和控制器绑定:
const sendMessage = async (inputMessage) => { // 发起新请求前,先取消未完成的旧请求 abortControllerRef.current.abort(); // 创建新的控制器(一个控制器只能用一次) abortControllerRef.current = new AbortController(); setIsCancelled(false); try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [...messages, { role: 'user', content: inputMessage }] }), // 关联取消信号 signal: abortControllerRef.current.signal }); if (!response.ok) { throw new Error('请求失败'); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let done = false; let newMessageContent = ''; // 处理流式响应 while (!done) { const { value, done: doneReading } = await reader.read(); done = doneReading; if (value) { const chunk = decoder.decode(value); // 拼接流式返回的内容 newMessageContent += chunk; // 更新UI setMessages(prev => [ ...prev.slice(0, -1), { role: 'assistant', content: newMessageContent } ]); } } } catch (error) { if (error.name === 'AbortError') { // 请求被取消后的处理,比如清空未完成的消息 setMessages(prev => prev.slice(0, -1)); console.log('请求已取消'); } else { // 其他错误处理 console.error('请求出错:', error); } } };
3. 实现取消按钮
点击按钮时调用控制器的abort()方法,同时更新状态:
<button onClick={() => { abortControllerRef.current.abort(); setIsCancelled(true); }} disabled={isCancelled} > 取消生成 </button>
关键说明
- AbortController会直接中断底层的网络连接,让
reader.read()立即抛出AbortError,从而终止while循环,这比单纯依赖状态变量更可靠。 - 每次发起新请求前要创建新的AbortController实例,因为一个控制器只能触发一次取消操作。
内容的提问来源于stack exchange,提问作者Peter Kellner
相关产品推荐
相关产品推荐

