banner
innei

innei

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

伺服器操作與可串流的使用者介面

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">重新驗證此頁面佈局</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">從 Server Action 渲染 ReactNode</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>載入中</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()
        setNode(node)
      }}
    >
      <button type="submit">從 Server Action 渲染 ReactNode</button>
    </form>
  )
}

image

我們可以看到,當我們點擊按鈕時,頁面渲染了一個 Suspense 組件,展示了載入中。隨後,等待異步組件加載完成,展示了 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>打字機完成。</p>
      </div>,
    )
  })()

  return <Suspense fallback={<div>載入中</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

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。