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

相关推荐
发现一只大呆瓜11 分钟前
虚拟列表:支持“向上加载”的历史消息(Vue 3 & React 双版本)
前端·javascript·面试
css趣多多28 分钟前
ctx 上下文对象控制新增 / 编辑表单显示隐藏的逻辑
前端
_codemonster35 分钟前
Vue的三种使用方式对比
前端·javascript·vue.js
寻找奶酪的mouse36 分钟前
30岁技术人对职业和生活的思考
前端·后端·年终总结
梦想很大很大43 分钟前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
We་ct1 小时前
LeetCode 56. 合并区间:区间重叠问题的核心解法与代码解析
前端·算法·leetcode·typescript
张3蜂1 小时前
深入理解 Python 的 frozenset:为什么要有“不可变集合”?
前端·python·spring
无小道1 小时前
Qt——事件简单介绍
开发语言·前端·qt
广州华水科技1 小时前
GNSS与单北斗变形监测技术的应用现状分析与未来发展方向
前端
code_YuJun1 小时前
corepack 作用
前端