Next.js api 接口字符串 stream response
可用在需要请求接口需要较长时间才完成任务,将处理信息逐步向客户端输出,直到完全输出信息。
常见的业务场景:
- 文字 AI 的信息逐字出现
- 下载文件
这次为文本,字符串形式的。
开发
Next.js 创建一个测试用的 app api router 页面
app/test/stream-api/router.ts
export const nodeStreamToIterator = async function* (stream) {
for await (const chunk of stream) {
yield chunk
}
}
export const iteratorToStream = (iterator) => {
return new ReadableStream({
async pull(controller) {
const { value, done } = await iterator.next()
if (done) {
controller.close()
} else {
// conversion to Uint8Array is important here otherwise the stream is not readable
// @see https://github.com/vercel/next.js/issues/38736
controller.enqueue(value)
}
},
})
}
function sleep(time: number) {
return new Promise((resolve) => {
setTimeout(resolve, time)
})
}
const encoder = new TextEncoder()
async function* makeIterator() {
let length = 0
while (length < 60 * 10) {
yield encoder.encode(`<p>${length} ${new Date().toLocaleString()}</p>`)
await sleep(1000)
length += 1
}
}
export async function POST() {
return new Response(iteratorToStream(nodeStreamToIterator(makeIterator())), {
headers: { 'Content-Type': 'application/octet-stream' },
})
}
export async function GET() {
return new Response(iteratorToStream(nodeStreamToIterator(makeIterator())), {
headers: { 'Content-Type': 'text/html' },
})
}
大致逻辑:
- 使用 makeIterator 方法创建一个 while 循环,内部使用 await 进行 sleep 1 秒的等待。使用 encoder.encode 方法将字符串转为 Unit8Array
- 使用 nodeStreamToIterator 将 makeIterator 每次 yield 时的数据当做一个 stream 的 chunk
- 使用 iteratorToStream 方法将 nodeStreamToIterator 方法的 chunk 压入一个 ReadableStream 可读流内
- 使用 Response 对象将可读流逐步返回给客户端
使用浏览器打开测试用的 app api router。浏览器会间隔一秒的时间显示一个时间。直到前面的 length 达到 600,大约 10 分钟。里面的 await sleep 就约等于模拟程序需要执行的时间。
页面的缓慢出现,可以认为是“网速”慢,但是至少页面是缓慢出现信息。
同步创建一个 post 的接口,供下面的 fetch 例子请求使用。
使用 fetch 请求
一般使用 fetch 请求接口都会使用如下格式获取到数据。
const res = await fetch('api')
const data = await res.text()
里面有两个 await。
- fetch 前面的 await 为创建、发送一个 request 对象
- 第二个 await 为等待 response 彻底完成
这时候,我们可以不用等待第二个 await 彻底完成后再使用数据。可以修改成这样
const res = await fetch('api')
if (res.body) {
const reader = res.body.getReader()
while(true) {
const { done, value } = await reader.read()
...
if(done){
break
}
}
}
使用 while 循环获取 response 的“可读流”数据。当 done 为真时,跳出循环。
那么配合上面的 10 分钟不断输出字符串的例子,使用 fetch 请求 post 的接口。
'use client'
import React, { useState } from 'react'
import { useRequest } from 'ahooks'
const Page: React.FC = () => {
const [text, changeText] = useState<string[]>([])
useRequest(async () => {
const res = await fetch('/test/stream-api/', { method: 'post' })
if (res.body) {
const reader = res.body.getReader()
const decode = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
changeText((text) => {
return [...text, decode.decode(value)]
})
if (done) {
break
}
}
}
})
return (
<ul>
{text.map((text) => (
<li key={text}>{text}</li>
))}
</ul>
)
}
export default Page
这时候浏览器看到的效果与直接访问会一样。在每次 while 内的 await reader.read() 获取到流数据。使用 decode.decode(value) 将 Unit8Array 转为字符串。这里的字符串可以是一个 json 或者自定义字符串结构。再对这个字符串做对应的编解码,即可得到运行时所需的的数据了。
这个操作可以用在 node 调用 child_process.spawn 等返回“流”对象的操作。可以直接将“流”对象传输给客户端,供客户端进行消费。而不是在服务端等待流结束后再将获取到的数据再传输给客户端。
转载自:https://juejin.cn/post/7317815765218541620