学习 RSC(二):集成客户端组件与搜索功能

57 阅读4分钟

一、回顾:极简 RSC 框架

在上一篇,我们基于 react-server-dom-webpack 实现了一个最小可运行的 RSC 应用,这个项目结构很简单:

  • server.js:服务端应用,基于 Express 提供接口
  • client.js:客户端入口文件
  • build.js:给客户端文件打包
  • app.js:服务端组件入口,包含 与数据加载逻辑

我们甚至没有使用 use clientuse 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 替换流,触发新树渲染

五、流式交互流程解析

  1. 初次渲染
    • 浏览器:fetch('/rsc') → client payload → createFromReadableStream → React 树
  2. 用户输入并提交
    • 浏览器阻止默认提交,读取 中的 query
    • 再次 fetch('/rsc?query=xxx')
  3. 服务端渲染新树
    • 接收新的 query 参数
    • 执行 App({ searchParams }) → 调用 getMovies(query) → 等待
  4. 客户端更新
    • 新的 ReadableStream 通过 window.__updateTree 注入
    • React 从流中重建组件树并自动更新 UI

六、RSC 的局限与思考

如果是服务端组件,React Server Components 的通信机制(通过 fetch /rsc + createFromReadableStream)支持多次重新请求。但是你无法在客户端“先行更新 UI”,而只能等待后端完成渲染并重新 stream 新的组件树。

七、结语

本文通过在极简 RSC 框架中加入搜索功能,演示了 use client、react-server-dom-webpack/plugin 以及客户端流式更新的核心机制。希望能帮助你更直观地理解 RSC 的开发流程与前后端组件边界。