React Server Components(RSC)是React团队推出的一项重要特性,它允许组件在服务端渲染,只把必要的数据和交互逻辑发送到客户端,大幅减少客户端的JavaScript体积,提升页面性能。今天来详细聊聊React Server Components的配置,从基础概念到高级用法,包括它的原理、配置方法、最佳实践,以及常见的坑和解决方案。

先说说背景。React Server Components,最早是在2020年底由React团队提出的,当时还是一个实验性的特性。它的核心思想,是把React组件分成两类:服务端组件(Server Components)和客户端组件(Client Components)。服务端组件在服务端渲染,不需要发送JavaScript到客户端;客户端组件在客户端渲染,需要发送JavaScript到客户端,支持交互。

通过这种方式,可以大幅减少发送到客户端的JavaScript体积,因为大部分组件(尤其是那些只负责展示数据、没有交互的组件)都可以作为服务端组件,在服务端渲染好之后,直接把HTML发送到客户端,不需要发送JavaScript。只有那些需要交互的组件(比如按钮、表单、动画等),才需要作为客户端组件,发送JavaScript到客户端。

这对于提升页面性能,尤其是首屏加载速度,有非常大的帮助。因为JavaScript体积小了,下载和解析的时间就短了,页面就能更快地交互。

虽然React Server Components在2020年还是实验性的,但它代表了React未来的发展方向,也已经在Next.js等框架中得到了应用。今天,就来详细聊聊它的配置和用法。

一、React Server Components的基本原理

在深入配置之前,先说说React Server Components的基本原理,帮助大家理解它是怎么工作的。

1. 服务端组件和客户端组件

React Server Components的核心,是把组件分成两类:服务端组件和客户端组件。

  • 服务端组件(Server Components):在服务端渲染,不需要发送JavaScript到客户端。服务端组件可以直接访问服务端的资源,比如数据库、文件系统、后端API等,不需要通过网络请求获取数据。服务端组件渲染之后,会生成一种特殊的序列化格式(不是普通的HTML,而是一种中间格式),发送到客户端,客户端再把它渲染成HTML。
  • 客户端组件(Client Components):在客户端渲染,需要发送JavaScript到客户端。客户端组件支持交互,比如点击事件、状态管理、动画等。客户端组件和我们平时写的React组件是一样的。

默认情况下,所有组件都是服务端组件。如果需要一个组件是客户端组件,需要在文件的顶部加上'use client'指令。

比如:

// 这是一个客户端组件
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

而服务端组件,不需要加任何指令,默认就是:

// 这是一个服务端组件
import { db } from './db';

export default async function PostList() {
  const posts = await db.posts.findMany(); // 直接访问数据库
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

2. 服务端组件的优势

服务端组件有以下几个优势:

  • 零JavaScript体积:服务端组件不需要发送JavaScript到客户端,所以可以大幅减少客户端的JavaScript体积。比如,一个用于渲染Markdown的组件,可能依赖了一个很大的Markdown解析库,如果作为服务端组件,这个库只需要在服务端安装,不需要发送到客户端,客户端的JavaScript体积就小了很多。
  • 直接访问服务端资源:服务端组件可以直接访问数据库、文件系统、后端API等服务端资源,不需要通过网络请求获取数据。这不仅简化了代码,还提升了性能,因为不需要在客户端发起网络请求,减少了延迟。
  • 更好的安全性:服务端组件在服务端运行,敏感的代码和数据(比如API密钥、数据库连接信息等)不会暴露到客户端,安全性更好。
  • 更好的缓存:服务端组件的渲染结果可以被缓存,比如CDN缓存、服务端缓存等,下次请求的时候,可以直接返回缓存的结果,不需要重新渲染,提升了性能。

3. 服务端组件的限制

当然,服务端组件也有一些限制:

  • 不能使用浏览器API:服务端组件在服务端运行,不能使用浏览器的API,比如windowdocumentlocalStorage等。如果需要使用这些API,必须使用客户端组件。
  • 不能使用交互事件:服务端组件不能使用onClickonChange等交互事件,因为这些事件需要在客户端处理。如果需要交互,必须使用客户端组件。
  • 不能使用状态和生命周期:服务端组件不能使用useStateuseEffectuseReducer等状态和生命周期相关的Hook,因为这些都是客户端的概念。服务端组件是无状态的,每次渲染都是全新的。
  • 不能直接导入客户端组件的某些特性:服务端组件可以导入客户端组件,但不能把服务端组件的函数作为props传给客户端组件,因为函数不能被序列化。可以传递序列化的数据,比如字符串、数字、对象、数组等。

了解了这些基本原理,再来说说配置。

二、基础配置:从零开始搭建React Server Components

要使用React Server Components,需要一些基础的配置。目前,React Server Components主要在Next.js中得到了支持,所以我们以Next.js为例,来介绍配置方法。

1. 创建Next.js项目

首先,创建一个新的Next.js项目。Next.js 13及以上版本,默认支持React Server Components,使用App Router。

npx create-next-app@latest my-app
cd my-app

在创建过程中,会问你一些问题,比如是否使用TypeScript、是否使用ESLint、是否使用Tailwind CSS、是否使用src/目录、是否使用App Router等。建议都选Yes,尤其是App Router,因为React Server Components需要App Router的支持。

创建完成之后,项目的结构大概是这样的:

my-app/
  src/
    app/
      layout.jsx
      page.jsx
      globals.css
  package.json
  next.config.js

其中,app目录是App Router的根目录,里面的文件会自动映射成路由。默认情况下,app目录下的所有组件,都是服务端组件。

2. 编写第一个服务端组件

app/page.jsx中,默认的代码是一个客户端组件(因为用了useState等),我们可以把它改成一个服务端组件,看看效果。

// src/app/page.jsx
// 这是一个服务端组件,不需要加 'use client'

async function getPosts() {
  // 模拟从数据库获取数据
  // 在真实项目中,这里可以直接访问数据库
  return [
    { id: 1, title: '第一篇文章', content: '这是第一篇文章的内容' },
    { id: 2, title: '第二篇文章', content: '这是第二篇文章的内容' },
    { id: 3, title: '第三篇文章', content: '这是第三篇文章的内容' },
  ];
}

export default async function HomePage() {
  const posts = await getPosts(); // 在服务端获取数据
  
  return (
    <main>
      <h1>文章列表</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.content}</p>
          </li>
        ))}
      </ul>
    </main>
  );
}

这就是一个最简单的服务端组件。它在服务端获取数据,然后渲染成HTML,发送到客户端。客户端不需要下载这个组件的JavaScript,只需要接收渲染好的HTML。

你可以运行npm run dev,然后打开浏览器,查看页面。你会发现,页面正常显示,而且查看页面源代码,可以看到内容已经在HTML中了,这就是服务端渲染的效果。

3. 编写第一个客户端组件

如果需要交互,比如点击按钮、输入表单等,就需要使用客户端组件。客户端组件需要在文件顶部加上'use client'指令。

比如,创建一个计数器组件:

// src/components/Counter.jsx
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>
        加一
      </button>
    </div>
  );
}

然后,在服务端组件中导入这个客户端组件:

// src/app/page.jsx
import Counter from '../components/Counter';

async function getPosts() {
  // ... 同上
}

export default async function HomePage() {
  const posts = await getPosts();
  
  return (
    <main>
      <h1>文章列表</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.content}</p>
          </li>
        ))}
      </ul>
      
      {/* 在服务端组件中使用客户端组件 */}
      <Counter />
    </main>
  );
}

这样,页面中既有服务端渲染的文章列表,也有客户端渲染的计数器组件。文章列表不需要JavaScript,计数器组件需要JavaScript,但只有计数器的JavaScript会被发送到客户端,文章列表的JavaScript不会,所以整体的JavaScript体积还是很小的。

4. 服务端组件和客户端组件的混合使用

在实际项目中,服务端组件和客户端组件通常是混合使用的。一般的原则是:尽量使用服务端组件,只有在需要交互的时候,才使用客户端组件。

比如,一个博客文章页面:

  • 整个页面的布局(头部、侧边栏、底部),可以是服务端组件
  • 文章内容的渲染,可以是服务端组件,直接从数据库获取文章内容,渲染成HTML
  • 文章的点赞按钮、评论区、分享按钮等需要交互的部分,可以是客户端组件

这样,大部分内容都是服务端渲染的,只有少量需要交互的部分是客户端渲染的,既保证了性能,又保证了交互性。

需要注意的是,客户端组件中导入的所有组件,都会自动变成客户端组件,即使它们没有加'use client'。因为客户端组件的JavaScript需要发送到客户端,它导入的组件也需要一起发送。所以,'use client'指令,实际上是定义了一个客户端组件的边界,这个边界以内的所有组件,都是客户端组件。

三、高级配置:深入React Server Components

了解了基础配置,再来说说一些高级的配置和用法。

1. 数据获取

服务端组件最大的优势之一,就是可以直接在组件中获取数据,不需要使用useEffect在客户端发起网络请求。

在服务端组件中,可以直接使用async/await来获取数据:

async function getPost(id) {
  // 直接从数据库获取数据
  const post = await db.post.findUnique({ where: { id } });
  return post;
}

export default async function PostPage({ params }) {
  const post = await getPost(params.id);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

这种方式,比在客户端用useEffect获取数据,性能好很多,因为:

  • 数据在服务端获取,不需要通过网络请求从客户端发到服务端,减少了延迟
  • 数据获取和页面渲染在同一个请求中完成,不需要先渲染页面,再发起请求,再更新页面
  • 数据直接渲染在HTML中,对SEO友好

还可以使用React的cache函数,来缓存数据获取的结果,避免重复获取:

import { cache } from 'react';

const getPost = cache(async (id) => {
  const post = await db.post.findUnique({ where: { id } });
  return post;
});

这样,在同一个请求中,多次调用getPost(id),只会真正执行一次,后面的调用会返回缓存的结果。

2. 动态渲染和静态渲染

服务端组件支持两种渲染模式:动态渲染和静态渲染。

  • 静态渲染(Static Rendering):在构建时渲染,渲染结果会被缓存,所有用户访问的都是同一个渲染结果。适用于内容不经常变化的页面,比如博客文章、帮助文档等。
  • 动态渲染(Dynamic Rendering):在每个请求时渲染,每个用户访问的都是实时渲染的结果。适用于内容经常变化、或者需要根据用户信息渲染的页面,比如用户主页、实时数据仪表盘等。

默认情况下,Next.js会根据页面是否使用了动态函数(比如cookies()headers()searchParams等),自动决定使用静态渲染还是动态渲染。如果页面使用了这些动态函数,就会使用动态渲染;否则,使用静态渲染。

也可以手动指定渲染模式,使用dynamic变量:

// 强制动态渲染
export const dynamic = 'force-dynamic';

// 强制静态渲染
export const dynamic = 'force-static';

还可以配置缓存时间,使用revalidate变量:

// 每60秒重新生成一次页面
export const revalidate = 60;

这就是增量静态再生(ISR),结合了静态渲染和动态渲染的优点,页面在构建时静态生成,然后每隔一段时间重新生成,保证内容的新鲜度,同时又有很好的性能。

3. 流式渲染(Streaming)

服务端组件支持流式渲染,也就是页面可以一部分一部分地发送到客户端,而不是等整个页面都渲染完了再发送。这样,用户可以更快地看到页面的内容,不需要等整个页面都加载完。

在Next.js中,可以使用loading.jsx文件,来定义页面加载时的骨架屏:

// src/app/loading.jsx
export default function Loading() {
  return (
    <div>
      <p>加载中...</p>
    </div>
  );
}

当页面正在渲染的时候,会先显示这个加载组件,等页面渲染完成之后,再替换成实际的内容。

还可以使用React的Suspense组件,来实现更细粒度的流式渲染:

import { Suspense } from 'react';
import PostList from './PostList';
import Sidebar from './Sidebar';

export default function HomePage() {
  return (
    <div>
      <Suspense fallback={<p>加载文章列表中...</p>}>
        <PostList />
      </Suspense>
      
      <Suspense fallback={<p>加载侧边栏中...</p>}>
        <Sidebar />
      </Suspense>
    </div>
  );
}

这样,PostListSidebar可以并行渲染,哪个先渲染完,就先显示哪个,不需要等另一个。用户可以更快地看到部分内容,提升了用户体验。

4. 服务端组件和客户端组件之间的数据传递

服务端组件和客户端组件之间,可以通过props传递数据,但需要注意,传递的数据必须是可序列化的,也就是可以被转换成JSON的。

可以传递的数据类型包括:

  • 基本类型:字符串、数字、布尔值、null、undefined
  • 复合类型:对象、数组(只要里面的元素都是可序列化的)
  • Promise(React Server Components支持传递Promise,客户端组件可以await)

不能传递的数据类型包括:

  • 函数(包括回调函数)
  • 类的实例
  • Symbol
  • 其他不可序列化的对象

如果需要在客户端组件中调用服务端的逻辑,可以使用Server Actions(服务端动作),这是React Server Components的另一个重要特性,允许在客户端组件中调用服务端的函数。

比如:

// src/app/actions.js
'use server';

export async function addPost(formData) {
  const title = formData.get('title');
  const content = formData.get('content');
  
  // 在服务端保存文章
  await db.post.create({ data: { title, content } });
  
  // 重新验证页面
  revalidatePath('/posts');
}

然后,在客户端组件中使用:

'use client';

import { addPost } from './actions';

export default function PostForm() {
  return (
    <form action={addPost}>
      <input type="text" name="title" placeholder="标题" />
      <textarea name="content" placeholder="内容" />
      <button type="submit">提交</button>
    </form>
  );
}

这样,表单提交的时候,会调用服务端的addPost函数,在服务端保存文章,不需要写API接口,非常方便。

5. 错误处理

服务端组件的错误处理,可以使用error.jsx文件,来定义错误边界:

// src/app/error.jsx
'use client'; // 错误边界必须是客户端组件

import { useEffect } from 'react';

export default function Error({ error, reset }) {
  useEffect(() => {
    console.error(error);
  }, [error]);
  
  return (
    <div>
      <h2>出错了!</h2>
      <p>{error.message}</p>
      <button onClick={reset}>重试</button>
    </div>
  );
}

当页面渲染出错的时候,会显示这个错误组件,而不是整个页面崩溃。用户还可以点击"重试"按钮,重新渲染页面。

还可以使用not-found.jsx文件,来处理404错误:

// src/app/not-found.jsx
export default function NotFound() {
  return (
    <div>
      <h2>页面不存在</h2>
      <p>你访问的页面不存在,请检查URL是否正确。</p>
    </div>
  );
}

在服务端组件中,可以调用notFound()函数,来触发404页面:

import { notFound } from 'next/navigation';

export default async function PostPage({ params }) {
  const post = await getPost(params.id);
  
  if (!post) {
    notFound(); // 触发404页面
  }
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

四、最佳实践和常见的坑

最后,说说使用React Server Components的一些最佳实践,以及常见的坑和解决方案。

1. 尽量使用服务端组件,只有在需要交互的时候才用客户端组件

这是最重要的一条原则。服务端组件有很多优势,比如零JavaScript体积、直接访问服务端资源、更好的安全性等。所以,应该尽量使用服务端组件,只有在需要交互(比如点击事件、状态管理、动画等)的时候,才使用客户端组件。

很多人一开始使用React Server Components的时候,习惯把所有组件都加上'use client',变成客户端组件,这样就失去了React Server Components的意义。应该反过来,默认都是服务端组件,只有确实需要交互的组件,才加上'use client'

2. 把客户端组件尽量放在组件树的叶子节点

因为客户端组件中导入的所有组件,都会自动变成客户端组件,所以,应该把客户端组件尽量放在组件树的叶子节点,也就是尽量靠下的位置。这样,客户端组件的边界就比较小,只有少量组件需要发送JavaScript到客户端。

比如,一个页面,头部、侧边栏、底部都是服务端组件,只有一个点赞按钮是客户端组件,那就把点赞按钮作为一个独立的客户端组件,放在叶子节点,而不是把整个页面都变成客户端组件。

3. 不要在服务端组件中使用浏览器API和交互事件

这是一个常见的坑。服务端组件在服务端运行,不能使用浏览器的API,比如windowdocumentlocalStorage等,也不能使用onClickonChange等交互事件。如果在服务端组件中使用了这些,会报错。

如果需要使用这些,必须把组件改成客户端组件,加上'use client'

4. 注意服务端组件和客户端组件之间的数据传递

服务端组件和客户端组件之间传递的数据,必须是可序列化的。不能传递函数、类的实例等不可序列化的数据。

如果需要在客户端组件中调用服务端的逻辑,可以使用Server Actions,而不是把函数作为props传递。

5. 注意CSS和样式的处理

在服务端组件中,可以使用CSS Modules、Tailwind CSS等方式来写样式,这些都可以正常工作。但如果需要使用CSS-in-JS(比如styled-components、emotion等),需要注意,因为这些库通常需要在客户端运行,可能不支持服务端组件。

如果需要使用CSS-in-JS,建议使用支持服务端组件的库,或者把使用CSS-in-JS的组件改成客户端组件。

6. 注意第三方库的兼容性

很多第三方库,可能不支持服务端组件,因为它们使用了浏览器API,或者使用了useStateuseEffect等客户端Hook。如果在服务端组件中导入这些库,会报错。

解决方案是,把使用这些库的组件改成客户端组件,加上'use client'。或者,使用动态导入,只在客户端加载这些库:

import dynamic from 'next/dynamic';

const Chart = dynamic(() => import('react-chartjs-2'), {
  ssr: false, // 不在服务端渲染
});

7. 做好性能监控和优化

使用React Server Components,虽然可以大幅减少客户端的JavaScript体积,但也可能引入一些新的性能问题,比如服务端渲染时间过长、流式渲染的顺序不合理等。

所以,要做好性能监控,关注以下指标:

  • 服务端渲染时间(TTFB,Time To First Byte)
  • 首屏内容渲染时间(FCP,First Contentful Paint)
  • 最大内容渲染时间(LCP,Largest Contentful Paint)
  • 可交互时间(TTI,Time To Interactive)
  • 客户端JavaScript体积

如果发现性能问题,及时优化,比如使用缓存、使用静态渲染、优化数据获取、合理使用流式渲染等。

五、写在最后

React Server Components,是React近年来最重要的特性之一,它改变了我们编写React应用的方式,让我们可以在服务端渲染大部分内容,只把必要的交互逻辑发送到客户端,大幅提升了页面性能。

虽然在2020年,React Server Components还是实验性的,但它代表了React未来的发展方向,也已经在Next.js等框架中得到了应用和验证。相信在不久的将来,React Server Components会成为React应用的标准开发方式。

今天,我们从基础概念到高级用法,详细介绍了React Server Components的配置和使用,包括它的原理、服务端组件和客户端组件的区别、数据获取、动态渲染和静态渲染、流式渲染、错误处理,以及最佳实践和常见的坑。

希望这篇文章,能帮助大家更好地理解和使用React Server Components,写出性能更好、体验更佳的React应用。

如果有什么问题或者不同的看法,欢迎在评论区交流。