参考链接: stackoverflow.com/questions/6…
实现:前端实现打字机式输出,请求结束识别,可中断数据传输。后端返回的数据需为流数据,响应头 Content-Type
为 text/event-stream
。
以下方案均可在最新版 Chrome 上使用。
方案一 fetch
借助 ReadableStream
API (MDN docs here)。
const postData = {foo: 'bar'}
const url = '<http://test.com>'
let controller = new AbortController()
const loadData = async () => {
try {
const response = await fetch(url, {
method: 'post',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(postData),
signal: controller.signal
})
const reader = (response as any).body.getReader()
let data = ''
while (true) {
const {done, value} = await reader.read()
if (done) {
setLoading(false)
break
}
data += new TextDecoder().decode(value)
setData(data)
}
} catch {
message.error('请求失败')
}
}
loadData()
// 中断接收
// controller.abort()
每次只返回并读取一点数据。
方案二 使用 XHR
const postData = {foo: 'bar'}
const url = '<http://test.com>'
var xhr = new XMLHttpRequest()
xhr.open('POST', url, true)
xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8')
xhr.onprogress = function () {
setData(xhr.responseText)
}
xhr.onloadend = function () {
setLoading(false)
}
xhr.onerror = function () {
message.error('请求失败')
}
xhr.send(JSON.stringify(postData))
// 中断接收
// xhr.abort()
xhr.responseText
每次都返回当前全部信息,包括前面已返回的。
方案三 axios——基于方案2
const postData = {foo: 'bar'}
const url = '<http://test.com>'
let controller = new AbortController()
axios({
method: 'post',
url,
data: postData,
responseType: 'stream',
onDownloadProgress: function (progressEvent: AxiosProgressEvent) {
setData(progressEvent.event.currentTarget.responseText)
},
signal: controller.signal
}).then(() => {
setLoading(false)
}).catch(() => {
message.error('请求失败')
})
// 中断接收
// controller.abort()
如果是 Get 请求,可以使用 EventSource
数据处理
看到评论有伙伴提到了数据处理,再补充一下。我们收到的流数据是一段段的字符串。一般情况下,开始传递的时候会先发送一段 data:\n\n\n\n/
表示要开始传递了,接下来每行文本的格式也是以 data:
开头,中间是具体传递的内容,然后以 \n
或 \n\n
结束。具体的事件流格式可以参考这篇文章。
所以我们要获取到准确的信息需要对每次返回的文本进行截取处理。比如这样:
const content = progressEvent.event.currentTarget.responseText
.replace(/^data:\n\n\n\n/, '')
?.split('data:')
?.map((item: string) => item.replace(/\n\n$/, ''))
?.join('')
这种截取方式适用于每次收到的文本都包含前面信息的,也就是如果我们要传递这么一段文字 “我的心却在怦怦跳,因为我对一切都充满了期望:丛林里的花,咆哮的野兽。”,第一次接收到的是 data:我的心却在怦怦跳,\n\n
,第二次接收到的就是 data:我的心却在怦怦跳,因为我对一切都充满了期望:\n\n
,以此类推,最后接收到的是整段文字。但还有一种方式是,每次接收到的内容不包含上次的,这种方式下第二次接收到的就是 data:因为我对一切都充满了期望:\n\n
。这点需要注意一下。
然后,如果我们想要传递 JSON 数据,收到的也只能是 JSON 字符串,需要用 JSON.parse
转换成 JSON 对象格式。所以,JSON 数据不太方便流式输出,因为只能等全部数据全部传完,才可使用 JSON.parse
进行转换。当然,不怕麻烦的话,也可以在过程中通过一些字符匹配获取内容。