Next.js api 接口字符串 stream response

可用在需要请求接口需要较长时间才完成任务,将处理信息逐步向客户端输出,直到完全输出信息。

常见的业务场景:

  1. 文字 AI 的信息逐字出现
  2. 下载文件

这次为文本,字符串形式的。

开发

Next.js 创建一个测试用的 app api router 页面

app/test/stream-api/router.ts

javascript 复制代码
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' },
  })
}

大致逻辑:

  1. 使用 makeIterator 方法创建一个 while 循环,内部使用 await 进行 sleep 1 秒的等待。使用 encoder.encode 方法将字符串转为 Unit8Array
  2. 使用 nodeStreamToIterator 将 makeIterator 每次 yield 时的数据当做一个 stream 的 chunk
  3. 使用 iteratorToStream 方法将 nodeStreamToIterator 方法的 chunk 压入一个 ReadableStream 可读流内
  4. 使用 Response 对象将可读流逐步返回给客户端

使用浏览器打开测试用的 app api router。浏览器会间隔一秒的时间显示一个时间。直到前面的 length 达到 600,大约 10 分钟。里面的 await sleep 就约等于模拟程序需要执行的时间。

页面的缓慢出现,可以认为是"网速"慢,但是至少页面是缓慢出现信息。

同步创建一个 post 的接口,供下面的 fetch 例子请求使用。

使用 fetch 请求

一般使用 fetch 请求接口都会使用如下格式获取到数据。

vbnet 复制代码
const res = await fetch('api')
const data = await res.text()

里面有两个 await。

  1. fetch 前面的 await 为创建、发送一个 request 对象
  2. 第二个 await 为等待 response 彻底完成

这时候,我们可以不用等待第二个 await 彻底完成后再使用数据。可以修改成这样

csharp 复制代码
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 的接口。

typescript 复制代码
'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 等返回"流"对象的操作。可以直接将"流"对象传输给客户端,供客户端进行消费。而不是在服务端等待流结束后再将获取到的数据再传输给客户端。

相关推荐
Mintopia4 分钟前
像素的进化史诗:计算机图形学与屏幕的千年之恋
前端·javascript·计算机图形学
Mintopia7 分钟前
Three.js 中三角形到四边形的顶点变换:一场几何的华丽变身
前端·javascript·three.js
归于尽22 分钟前
async/await 从入门到精通,解锁异步编程的优雅密码
前端·javascript
陈随易22 分钟前
Kimi k2不行?一个小技巧,大幅提高一次成型的概率
前端·后端·程序员
猩猩程序员28 分钟前
Rust 动态类型与类型反射详解
前端
杨进军30 分钟前
React 实现节点删除
前端·react.js·前端框架
yanlele1 小时前
【实践篇】【01】我用做了一个插件, 点击复制, 获取当前文章为 Markdown 文档
前端·javascript·浏览器
爱编程的喵1 小时前
React useContext 深度解析:告别组件间通信的噩梦
前端·react.js
望获linux2 小时前
【实时Linux实战系列】多核同步与锁相(Clock Sync)技术
linux·前端·javascript·chrome·操作系统·嵌入式软件·软件
魂祈梦2 小时前
rsbuild的环境变量
前端