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

相关推荐
街尾杂货店&1 小时前
css word-spacing属性
前端·css
千叶寻-1 小时前
正则表达式
前端·javascript·后端·架构·正则表达式·node.js
光影少年6 小时前
angular生态及学习路线
前端·学习·angular.js
无尽夏_8 小时前
HTML5(前端基础)
前端·html·html5
Jagger_8 小时前
敏捷开发流程-精简版
前端·后端
FIN66689 小时前
昂瑞微冲刺科创板:创新驱动,引领射频芯片国产化新征程
前端·安全·前端框架·信息与通信·芯片
GISer_Jing9 小时前
ByteDance——jy真题
前端·javascript·面试
睡美人的小仙女1279 小时前
浏览器为何屏蔽本地文件路径?
前端
真的想不出名儿9 小时前
Vue 中 props 传递数据的坑
前端·javascript·vue.js
FIN66689 小时前
昂瑞微:深耕射频“芯”赛道以硬核实力冲刺科创板大门
前端·人工智能·科技·前端框架·信息与通信·智能