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

相关推荐
Kagol43 分钟前
TinyVue 支持 Skills 啦!现在你可以让 AI 使用 TinyVue 组件搭建项目
前端·agent·ai编程
柳杉1 小时前
从零打造 AI 全球趋势监测大屏
前端·javascript·aigc
simple_lau1 小时前
Cursor配置MasterGo MCP:一键读取设计稿生成高还原度前端代码
前端·javascript·vue.js
睡不着先生1 小时前
如何设计一个真正可扩展的表单生成器?
前端·javascript·vue.js
天蓝色的鱼鱼1 小时前
模块化与组件化:90%的前端开发者都没搞懂的本质区别
前端·架构·代码规范
明君879971 小时前
Flutter 如何给图片添加多行文字水印
前端·flutter
leolee181 小时前
Redux Toolkit 实战使用指南
前端·react.js·redux
bluceli2 小时前
React Hooks最佳实践:写出优雅高效的组件代码
前端·react.js
IT_陈寒2 小时前
JavaScript代码效率提升50%?这5个优化技巧你必须知道!
前端·人工智能·后端
IT_陈寒2 小时前
Java开发必知的5个性能优化黑科技,提升50%效率不是梦!
前端·人工智能·后端