TanStack Start 中文文档
服务器与执行

从服务器函数流式传输数据

得益于 AI 应用的兴起,从服务器流式传输(Streaming)数据变得非常流行。幸运的是,在 TanStack Start 中这很容易做到,而且更好的是:流式传输的数据是带类型的

从服务器函数流式传输数据最流行的两种方式是使用 ReadableStream 或异步生成器(async generators)。

你可以在从服务器函数流式传输数据示例中看到这两种方式的实现。

译者注:为什么流式传输重要?

传统模式下,服务器要等全部数据生成完才能一次性发给客户端。流式传输让服务器可以边生成边发送——对 AI 场景尤其关键:用户能看到 token 一个接一个地出现,而不是等待很久后突然全部蹦出来。Start 的服务器函数对流式数据做了类型支持,客户端收到的每个数据块都有正确的 TypeScript 类型。

类型化的 ReadableStream

下面是一个类型安全地把消息数组流式传输给客户端的服务器函数示例:

type Message = {
  content: string
}

/**
  This server function returns a `ReadableStream`
  that streams `Message` chunks to the client.
*/
const streamingResponseFn = createServerFn().handler(async () => {
  // These are the messages that you want to send as chunks to the client
  const messages: Message[] = generateMessages()

  // This `ReadableStream` is typed, so each
  // will be of type `Message`.
  const stream = new ReadableStream<Message>({
    async start(controller) {
      for (const message of messages) {
        // Send the message
        controller.enqueue(message)
      }
      controller.close()
    },
  })

  return stream
})

当你从客户端消费这个流时,流式传输的数据块会有正确的类型:

const [message, setMessage] = useState('')

const getTypedReadableStreamResponse = useCallback(async () => {
  const response = await streamingResponseFn()

  if (!response) {
    return
  }

  const reader = response.getReader()
  let done = false
  while (!done) {
    const { value, done: doneReading } = await reader.read()
    done = doneReading
    if (value) {
      // Notice how we know the value of `chunk` (`Message | undefined`)
      // here, because it's coming from the typed `ReadableStream`
      const chunk = value.content
      setMessage((prev) => prev + chunk)
    }
  }
}, [])

服务器函数中的异步生成器

一种更简洁、效果相同的方式是使用异步生成器函数:

const streamingWithAnAsyncGeneratorFn = createServerFn().handler(
  async function* () {
    const messages: Message[] = generateMessages()
    for (const msg of messages) {
      await sleep(500)
      // The streamed chunks are still typed as `Message`
      yield msg
    }
  },
)

客户端代码也会更精简:

const getResponseFromTheAsyncGenerator = useCallback(async () => {
  for await (const msg of await streamingWithAnAsyncGeneratorFn()) {
    const chunk = msg.content
    setMessages((prev) => prev + chunk)
  }
}, [])

译者注:两种方式如何选?

两者在类型支持上完全等价。ReadableStream 更底层、更灵活,适合需要精细控制读取节奏(比如手动 backpressure)的场景;异步生成器则用 yield 把逻辑写得最简洁,可读性最好,是大多数情况下的首选。

On this page