一、回顾:极简 RSC 框架
在上一篇,我们基于 react-server-dom-webpack
实现了一个最小可运行的 RSC 应用,这个项目结构很简单:
- server.js:服务端应用,基于 Express 提供接口
- client.js:客户端入口文件
- build.js:给客户端文件打包
- app.js:服务端组件入口,包含 与数据加载逻辑
我们甚至没有使用 use client
、use server
这样的指令。
二、交互需求:为何要引入 Client Components
现在我们想给 movies 列表增加一个搜索过滤的功能,基于我们现有的功能是无法实现的,因为客户端获取的是服务端流数据,换句话说,app.js
是在服务端执行,MovieList
在服务端获取数据。
实际上,服务端通过 renderToPipeableStream 将组件树序列化为 RSC Payload,并通过 /rsc 接口以流式方式返回;客户端通过 fetch('/rsc') + createFromReadableStream 获取组件树并渲染到 DOM React,还没有客户端和服务端的交互。
但是通过 /rsc 获取服务端流数据,就是交互。
所以要想获得新的 MovieList
,就要重新请求 /rsc。
要实现搜索过滤,我们需要在客户端捕获用户输入,并触发新的 RSC 流请求。核心在于:
- 服务端渲染的组件(Server Components)只能生成静态的、与请求参数绑定的 UI。
- 状态(state) 与 事件处理 只能在浏览器端执行。
因此,我们需要将搜索表单及其逻辑标记为 客户端组件(Client Components),并将其打包进浏览器 bundle 中。在服务端序列化时,RSC 渲染器会将这些客户端组件替换为“客户端引用(client references)”,使得在 Payload 中存在对应的模块 ID。
三、关键技术:use client
与 Webpack 插件
use client
指令
在 search.js 顶部添加:
'use client';
该指令告诉 react-server-dom-webpack/plugin:该模块及其依赖需包含在客户端 bundle 中,并在服务端渲染时生成 client reference。
react-server-dom-webpack/plugin
在 build.js 中配置插件:
const ReactServerWebpackPlugin = require('react-server-dom-webpack/plugin');
plugins: [
new ReactServerWebpackPlugin({ isServer: false })
],
- 扫描 项目中所有添加了 use client 的模块
- 打包 它们及其依赖到客户端 bundle
- 生成 react-client-manifest.json,映射模块 ID 与 chunk 文件
服务端在调用 renderToPipeableStream(tree, clientManifest) 时,基于该 manifest 将所有 client references 序列化到 Payload 中。
四、实现要点
服务端入口 server.js
const clientManifest = require('./dist/react-client-manifest.json');
app.get('/rsc', (req, res) => {
res.setHeader('Content-Type', 'text/x-component');
const tree = React.createElement(App, {
searchParams: new URLSearchParams(req.query),
});
const rscStream = ReactServerDOMWebpackServer.renderToPipeableStream(
tree,
clientManifest
);
rscStream.pipe(res);
});
服务端接口,我们导入生成的 react-client-manifest.json,并将其作为 renderToPipeableStream API 的第二个参数传递。
- 注入 clientManifest 以支持 client references
- 每次访问 /rsc?query=xxx 时,服务端均重新渲染包含新 query 的组件树。
服务端组件 app.js
export function App({ searchParams }) {
const query = searchParams.get('query') ?? ''
const moviesPromise = getMovies(query);
return (
<div className='flex min-h-[100dvh] flex-col bg-gray-900 text-gray-200'>
<main className='container mx-auto max-w-lg flex-grow px-4 py-8'>
<Search query={query} />
<Suspense fallback={<div className='text-center'>Loading...</div>}>
<MovieList moviesPromise={moviesPromise} />
</Suspense>
</main>
</div>
);
}
服务端组件中增加 Search 搜索过滤组件。
客户端组件 search.js
'use client';
export function Search({ query }) {
const handleSearch = async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const newQuery = formData.get('query')?.toString() ?? '';
const response = await fetch(`/rsc?query=${newQuery}`);
window.__updateTree?.(response.body);
};
return (
<form onSubmit={handleSearch}>
<input
type='search'
placeholder='Search movies...'
className='w-full rounded-md border border-gray-700 bg-gray-800 px-8 py-2 text-gray-300 placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none'
name='query'
defaultValue={query}
/>
</form>
);
}
Search 组件被标志为 客户端组件
,使用 use client
指令。
其中搜索处理函数中调用了全局方法 window.__updateTree
,传入接口返回数据。这个方法后面会介绍到。
客户端入口 client.js
function App() {
const [tree, setTree] = useState(initialReactTreePromise);
useEffect(() => {
window.__updateTree = (stream) => {
const reactTreePromise = ReactServerDOMWebpackClient.createFromReadableStream(stream);
setTree(reactTreePromise);
};
return () => {
window.__updateTree = undefined;
};
}, []);
return use(tree);
}
客户端入口组件增加了 useEffect,定义了全局方法 window.__updateTree
,读取服务端 stream,客户端渲染进行更新。
- 首次加载:
initialTree
发起/rsc
请求并渲染 - 再次搜索:
window.__updateTree
替换流,触发新树渲染
五、流式交互流程解析
- 初次渲染
- 浏览器:fetch('/rsc') → client payload → createFromReadableStream → React 树
- 用户输入并提交
- 浏览器阻止默认提交,读取 中的 query
- 再次 fetch('/rsc?query=xxx')
- 服务端渲染新树
- 接收新的 query 参数
- 执行 App({ searchParams }) → 调用 getMovies(query) → 等待
- 客户端更新
- 新的 ReadableStream 通过 window.__updateTree 注入
- React 从流中重建组件树并自动更新 UI
六、RSC 的局限与思考
如果是服务端组件,React Server Components 的通信机制(通过 fetch /rsc + createFromReadableStream)支持多次重新请求。但是你无法在客户端“先行更新 UI”,而只能等待后端完成渲染并重新 stream 新的组件树。
七、结语
本文通过在极简 RSC 框架中加入搜索功能,演示了 use client、react-server-dom-webpack/plugin 以及客户端流式更新的核心机制。希望能帮助你更直观地理解 RSC 的开发流程与前后端组件边界。