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

相关推荐
@大迁世界8 小时前
Firefox 本地模式:localhost 再见了 ?
前端·架构
米米商聊10 小时前
同名附件为什么被 Map 覆盖?重复键与一对多索引
前端·javascript
echome88811 小时前
Python 异常处理:try、else、finally 的正确搭配
开发语言·前端·python·异常处理·编程技巧
时间的情敌11 小时前
OpenLayers 实战|Vue3 + OpenLayers 完整工程案例(加载天地图、要素、弹窗、绘制、WFS)
前端·openlayers
陈随易11 小时前
下载神器!Motrix 停更三年后重写回归
前端·后端·程序员
Moshow郑锴12 小时前
Ansible 批量部署 Node.js 服务:ZIP 直解与共享目录分发两种方案对比
node.js·ansible
前端的日常12 小时前
一个人+AI做情侣食谱小程序,0 费用的服务器
前端·后端
计算机魔术师12 小时前
谷歌搜索广告踩刹车,AI广告成为救命稻草
前端
总要学点什么12 小时前
第81篇:数据可视化-PM2.5分布——让圆点的胖瘦替每座城市报出浓度
前端·信息可视化·echarts·cesium·空间分析
IT_陈寒12 小时前
Vue的v-for里用了个临时变量,页面直接挂了
前端·人工智能·后端