banner
innei

innei

写代码是因为爱,写到世界充满爱!
github
telegram
twitter

Server Action & Streamable UI

Note

此小节启发于 https://sdk.vercel.ai/docs/concepts/ai-rsc

以下代码在 Next.js 14.1.2-canary.3 中可以工作,其他版本或许会有改动

此文章首次发布于我正在编写的 聊点不一样的 Next.js 小册。欢迎支持。

在 LLM 项目中,总是能看到流式传输渲染的信息。

image

我们看一下请求。

image

发现其实这是一个流式传输的 RSC payload。也就是说 UI 的更新是由服务器的流式传输 RSC payload 驱动的。当流式传输的 RSC payload 读取到下一行就刷新 UI。

这节我们利用 RSC 简单实现一下流式渲染消息流。

Server Action#

开始之前,我们需要知道 Server Action 其实是一个 POST 请求,服务器会调用 Server Action 函数的引用,然后通过 HTTP 请求的方式流式返回执行结果。

在 Server Action 中,你必须要定义一个异步的方法,因为请求是异步的;第二你必须返回一个可以被序列化的数据,例如函数这类则不行。

我们常用 Server Action 刷新页面的数据,例如使用 revalidatePath

我们尝试一下。

```tsx filename="app/server-action/layout.tsx" import type { PropsWithChildren } from 'react'

export default async ({ children }: PropsWithChildren) => {
return (


Layout Render At: {Date.now()}

{children}

)
}

</Tab>
<Tab label="page.tsx">
```tsx filename="app/server-action/page.tsx"
'use client'

import { useState } from 'react'
import type { ReactNode } from 'react'

import { actionRevalidate } from './action'

export default () => {
  return (
    <div className="flex flex-col gap-4">
      <ServerActionRevalidate />
    </div>
  )
}

const ServerActionRevalidate = () => {
  return (
    <form
      action={async (e) => {
        await actionRevalidate()
      }}
    >
      <button type="submit">Revalidate this page layout</button>
    </form>
  )
}
```tsx filename="app/server-action/action.tsx" 'use server'

import { revalidatePath } from 'next/cache'

export const actionRevalidate = async () => {
revalidatePath('/server-action')
}

</Tab>
</Tabs>

![](https://nextjs-book.innei.in/images/4.gif)

当我们点击按钮时,页面重新渲染了,在页面没有重载的情况下,刷新了最新的服务器时间。

## 使用 Server Action 获取 Streamable UI


脑洞一下,如果我们在 Server Action 返回一个 ReactNode 类型会怎么样。

<Tabs>
<Tab label="page.tsx">
```tsx filename="app/server-action/page.tsx" {21}
'use client'

import { useState } from 'react'
import type { ReactNode } from 'react'

import { actionReturnReactNode } from './action'

export default () => {
  return (
    <div className="flex flex-col gap-4">
      <ServerActionRenderReactNode />
    </div>
  )
}

const ServerActionRenderReactNode = () => {
  const [node, setNode] = useState<ReactNode | null>(null)
  return (
    <form
      action={async (e) => {
        const node = await actionReturnReactNode()
        setNode(node)
      }}
    >
      <button type="submit">Render ReactNode From Server Action</button>
    </form>
  )
}
```tsx filename="app/server-action/action.tsx" 'use server'

export const actionReturnReactNode = async () => {
return

React Node

}

</Tab>
</Tabs>

![](https://nextjs-book.innei.in/images/5.gif)

我们可以看到,当我们点击按钮时,页面渲染了一个 React Node。这个 React Node 是由 Server Action 返回的。

我们知道在 App Router 中可以使用 Server Component。Server Component 是一个支持异步的无状态组件。异步组件的返回值其实是一个 `Promise<ReactNode>`,而 `ReactNode` 是一个可以被序列化的对象。

那么,利用 Supsense + 异步组件会有怎么样的结果呢。


<Tabs>
<Tab label="action.tsx">
  ```tsx filename="app/server-action/action.tsx" {3,7}
  export const actionReturnReactNodeSuspense = async () => {
    const Row = async () => {
      await sleep(300)
      return <div>React Node</div>
    }
    return (
      <Suspense fallback={<div>Loading</div>}>
        <Row />
      </Suspense>
    )
  }
  ```
</Tab>

<Tab label="page.tsx">
```tsx filename="app/server-action/page.tsx"
'use client'

import { useState } from 'react'
import type { ReactNode } from 'react'

import { actionReturnReactNodeSuspense } from './action'

export default () => {
  return (
    <div className="flex flex-col gap-4">
      <ServerActionRenderReactNode />
    </div>
  )
}

const ServerActionRenderReactNode = () => {
  const [node, setNode] = useState<ReactNode | null>(null)
  return (
    <form
      action={async (e) => {
        const node = await actionReturnReactNodeSuspense()  // [!code highlight]
        setNode(node)
      }}
    >
      <button type="submit">Render ReactNode From Server Action</button>
    </form>
  )
}

image

我们可以看到,当我们点击按钮时,页面渲染了一个 Suspense 组件,展示了 Loading。随后,等待异步组件加载完成,展示了 React Node。

那么,利用这个特征我们可以对这个方法进行简单的改造,比如我们可以实现一个打字机效果。

export const actionReturnReactNodeSuspenseStream = async () => {
  const createStreamableRow = () => {
    const { promise, reject, resolve } = createResolvablePromise()
    const Row = (async ({ next }: { next: Promise<any> }) => {
      const promise = await next
      if (promise.done) {
        return promise.value
      }

      return (
        <Suspense fallback={promise.value}>
          <Row next={promise.next} />
        </Suspense>
      )
    }) /* Our React typings don't support async components */ as unknown as React.FC<{
      next: Promise<any>
    }>

    return {
      row: <Row next={promise} />,
      reject,
      resolve,
    }
  }

  let { reject, resolve, row } = createStreamableRow()

  const update = (nextReactNode: ReactNode) => {
    const resolvable = createResolvablePromise()
    resolve({ value: nextReactNode, done: false, next: resolvable.promise })
    resolve = resolvable.resolve
    reject = resolvable.reject
  }

  const done = (finalNode: ReactNode) => {
    resolve({ value: finalNode, done: true, next: Promise.resolve() })
  }

  ;(async () => {
    for (let i = 0; i < typewriterText.length; i++) {
      await sleep(10)
      update(<div>{typewriterText.slice(0, i)}</div>)
    }
    done(
      <div>
        {typewriterText}

        <p>typewriter done.</p>
      </div>,
    )
  })()

  return <Suspense fallback={<div>Loading</div>}>{row}</Suspense>
}

上面的代码中,createStreamableRow 创建了一个被 Suspense 的 Row 组件,利用嵌套的 Promise,只要 当前的 promise 的 value 没有 done,内部的 Suspense 就一直不会被 resolve,那么我们就可以一直往里面替换新的 React Node。

update 中我们替换了原来已经被 resolve 的 promise,新的 promise 没有被 resolve,那么 Suspense 就 fallback 上一个 promise 的值。依次循环。直到 done === true 的条件跳出。

效果如下:

image

那么利用这种 Streamable UI,可以结合 AI function calling,在服务器端按需绘制出各种不同 UI 的组件。

Warning

由于这种流式传输驱动组件更新,服务器需要一直保持长连接,并且每一次驱动更新的 RSC payload 都是在上一次基础上的全量更新,所以在长文本的情况下,传输的数据量是非常大的,可能会增大带宽压力。

另外,在 Vercel 等 Serverless 平台上,保持长连接会占用大量的计算资源,最终你的账单可能会变得很不可控。

上述所有代码示例位于:demo/steamable-ui

参考:https://sdk.vercel.ai/docs/concepts/ai-rsc#create-an-airsc-instance-on-the-server

此文由 Mix Space 同步更新至 xLog
原始链接为 https://innei.in/posts/programming/nextjs-rsc-server-action-and-streamable-ui


Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.