该渲染由 Shiro API 生成,可能存在排版问题,最佳体验请前往:https://innei.in/posts/programming/nextjs-rsc-server-action-and-streamable-ui
这个内容只能在原文中查看哦
此文章首次发布于我正在编写的 聊点不一样的 Next.js 小册。欢迎支持。
在 LLM 项目中,总是能看到流式传输渲染的信息。
我们看一下请求。
发现其实这是一个流式传输的 RSC payload。也就是说 UI 的更新是由服务器的流式传输 RSC payload 驱动的。当流式传输的 RSC payload 读取到下一行就刷新 UI。
这节我们利用 RSC 简单实现一下流式渲染消息流。
开始之前,我们需要知道 Server Action 其实是一个 POST 请求,服务器会调用 Server Action 函数的引用,然后通过 HTTP 请求的方式流式返回执行结果。
在 Server Action 中,你必须要定义一个异步的方法,因为请求是异步的;第二你必须返回一个可以被序列化的数据,例如函数这类则不行。
我们常用 Server Action 刷新页面的数据,例如使用 revalidatePath
。
我们尝试一下。
import type { PropsWithChildren } from 'react'
export default async ({ children }: PropsWithChildren) => {
return (
<div className="m-auto mt-12 max-w-[800px]">
<div>Layout Render At: {Date.now()}</div>
{children}
</div>
)
}
'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>
)
}
'use server'
import { revalidatePath } from 'next/cache'
export const actionRevalidate = async () => {
revalidatePath('/server-action')
}
当我们点击按钮时,页面重新渲染了,在页面没有重载的情况下,刷新了最新的服务器时间。
脑洞一下,如果我们在 Server Action 返回一个 ReactNode 类型会怎么样。
'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>
)
}
'use server'
export const actionReturnReactNode = async () => {
return <div>React Node</div>
}
我们可以看到,当我们点击按钮时,页面渲染了一个 React Node。这个 React Node 是由 Server Action 返回的。
我们知道在 App Router 中可以使用 Server Component。Server Component 是一个支持异步的无状态组件。异步组件的返回值其实是一个 Promise<ReactNode>
,而 ReactNode
是一个可以被序列化的对象。
那么,利用 Supsense + 异步组件会有怎么样的结果呢。
export const actionReturnReactNodeSuspense = async () => {
const Row = async () => {
await sleep(300)
return <div>React Node</div>
}
return (
<Suspense fallback={<div>Loading</div>}>
<Row />
</Suspense>
)
}
'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>
)
}
我们可以看到,当我们点击按钮时,页面渲染了一个 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
的条件跳出。
效果如下:
那么利用这种 Streamable UI,可以结合 AI function calling,在服务器端按需绘制出各种不同 UI 的组件。
这个内容只能在原文中查看哦
上述所有代码示例位于:demo/steamable-ui
参考:https://sdk.vercel.ai/docs/concepts/ai-rsc#create-an-airsc-instance-on-the-server