"停止生成"这个按钮,看着简单,我第一版做得是错的------点了停止,前端是不显示了,但请求还在后台跑,后端还在生成,token 照烧。后来认真用 AbortController 重做了一遍,把正确姿势和坑记下来。
错误示范:只 cancel reader
我最早是这么写的:
scss
// 错误!
reader.cancel()
reader.cancel() 确实停止了前端读流,但底层的 HTTP 请求不一定真的断,后端可能还在往一个没人读的连接里写。看着停了,其实没省。
正确做法:AbortController
把 signal 传给 fetch,中断时调 abort(),整个请求(包括底层连接)才真断:
php
const controller = new AbortController()
const res = await fetch('/api/chat', {
signal: controller.signal,
method: 'POST',
body: JSON.stringify({ messages }),
})
// 用户点停止:
controller.abort()
abort() 会让 fetch 抛一个 AbortError,记得 catch 住,别让它当成真错误弹报错给用户:
kotlin
catch (e) {
if (e.name === 'AbortError') return // 用户主动停的,正常
showError(e)
}
坑一:abort 后状态没清干净
中断后,那条"正在生成"的消息可能停在半句话。我得决定:是保留半句(用户可能就想要这点),还是整条删掉。我选了保留,但在末尾标个"(已停止)",不然用户以为是完整回答。
坑二:abort 和新请求打架
用户停了上一条,马上又发新的。如果没把旧 controller 置空,新请求复用了旧的 signal(已经 aborted),新请求一发就直接被 abort,整个对话框"卡死"。我吃过这亏。每次发新消息都 new 一个全新的 controller:
csharp
let controller
function send() {
controller = new AbortController() // 每次都新建
...
}
坑三:组件卸载也该 abort
用户在生成中途切走了页面/关了对话框,组件卸载时也得 abort(),不然请求成了野请求,回调还可能往已销毁的组件里 set state 报警告。在卸载钩子里统一收尾。
没做到位的
abort 我只断了前端,后端是否真的因为连接断开就停止计费,得看后端实现。理想是前端 abort 时再显式发个取消信号给后端,双保险。这块我跟后端还没对齐,暂时只信连接断开这一层。
模型用的讯飞 MaaS,现成 API,连接断了那边的计费逻辑省心,前端把中断这套做严谨就行。你们 abort 还遇到过啥坑,评论区交流。