React Server Components手把手教学

生活的乐趣取决于生活都本身,而不是取决于工作或地点

大家好,我是柒八九

前言

在上一篇介绍React 18 如何提升应用性能文章中提到了很多关于React性能优化的方式,例如(Suspence),从底层实现的角度来看,都是基于React Server Component(简称RSC)来做文章.

2020年末,React团队引入了Zero-Bundle-Size React Server Components概念。自那以后,React开发者社区一直在尝试并学习如何应用这种前瞻性的方法。

React改变了我们构建用户界面的思维方式。而使用RSC的新模型更加结构化、方便、可维护,并提供了更好的用户体验。

最新版本的Next.js 13已经采用了以服务器组件思维方式 ,并将其作为默认选项 。作为React开发者,我们必须适应这种新的思维模式,以充分发挥其在构建应用程序方面的优势。

RSC很好的将服务器端渲染客户端JavaScript的交互性无缝地融合在一起。

所以,我们有必要用一篇文章来介绍它.(文章有点长,请耐心观看,并且内容有点内核,配合收藏观看更佳)

你能所学到的知识点

  1. 前置知识点
  2. React :客户端UI库
  3. React 应用的通病
  4. SSR 和 Suspense 解决的痛点
  5. 客户端-服务器模型
  6. RSC的红与黑
  7. 如何同时使用客户端组件和服务器组件
  8. RSC的优点

好了,天不早了,干点正事哇。


1. 前置知识点

网络瀑布效应

网络瀑布效应(Network Waterfall Effect)是一个用于描述在计算机网络中出现的性能问题的概念。它通常用来说明在复杂的网络环境中,一个小问题的出现可能会逐渐扩大,导致整个网络的性能下降。(类似多米诺骨牌一样)

网络瀑布效应的核心思想是,网络中的各个组件和节点之间相互依赖,一个组件的问题可能会影响到其他组件,从而引发连锁反应

这种效应通常在大规模、分布式的网络中更为显著,因为网络中的节点众多,问题的传播速度和范围都会加大。

这意味着后续的获取请求仅在前一个获取请求被解析或完成后才会被初始化。


水合(Hydration)

在计算机科学领域,水合Hydration)通常指的是将数据或状态从一种格式或状态转换为另一种格式或状态的过程

React/Vue 水合

ReactVue的水合流程大差不差(反正都是各自SSR流程中的一部分,只是具体API不同,原理都是一样的),所以我们只按其中一种介绍,另外一种或者说其他更多的前端框架,你只需要换个名字就可以了. (按图索骥,照猫画虎会哇).

"React 水合"(React Hydration)是指将服务器端渲染Server-Side RenderingSSR)得到的 HTML 结构与客户端的 JavaScript 表现层相结合的过程。

React 水合是在客户端渲染(Client-Side RenderingCSR)和服务器端渲染之间的一个关键步骤,确保在将服务器渲染的 HTML 呈现给用户之后,React 组件能够在客户端接管并继续工作。

以下是 React 水合的详细步骤和背后的原理:

  1. 服务器端渲染(SSR):服务器端使用 React 渲染组件,生成一段包含完整组件结构的 HTML

    • 这段 HTML 可以包含组件的初始状态,这样在首次加载页面时,用户将看到已经有内容呈现在页面上,而不需要等待客户端 JavaScript 加载和执行。
  2. 客户端 JavaScript 加载: 在浏览器中加载包含 React 应用逻辑的 JavaScript 文件。

    • 这些文件可能包括应用的组件、状态管理逻辑、事件处理等。
  3. 水合阶段: 一旦客户端 JavaScript 加载完成,React 将接管页面,开始水合过程。

    • 这意味着 React 会检查服务器端渲染生成的 HTML,并将其与客户端 JavaScript 中的组件逻辑进行匹配。
  4. 组件恢复和事件绑定: 在水合阶段,React 会将服务器端渲染的 HTML 中的组件恢复到其初始状态,并建立与客户端 JavaScript 中的相应组件的联系。

    • 这包括建立事件绑定、状态同步等。
  5. 交互和动态更新: 一旦水合完成,React 组件就会变得可交互

    • 用户可以与页面进行互动,而客户端 JavaScript 负责处理事件、状态更改等
    • 此后,页面将继续响应用户操作,动态地更新内容,而无需再次从服务器获取完整的 HTML

React 水合的优势在于它结合了服务器端渲染客户端渲染的优点,提供了更好的性能和用户体验。

通过在首次加载时提供一部分已渲染的内容 ,用户可以更快地看到页面,并与之互动。然后,客户端 JavaScript 接管页面,继续处理后续的交互和动态更新。


Next 简单使用教程

Next.js 是一个基于 React 的框架,用于构建具有服务器端渲染(SSR)和静态网站生成(SSG)功能的应用程序。Next.js 提供了一个内置的路由系统 ,称为 Next.js App Router,用于管理应用程序的路由和页面导航。

下面是一个简单的介绍和代码示例,展示如何使用 Next.js App Router

  1. 安装 Next.js: 首先,你需要在项目中安装 Next.js。你可以使用 npmyarn 进行安装。

    bash 复制代码
    npm install next react react-dom
    # 或
    yarn add next react react-dom
  2. 创建页面:Next.js 中,页面是位于 pages 目录下的 React 组件。每个页面对应一个 URL 路由

    在项目根目录下创建 pages 目录,并在其中创建一个名为 index.js 的文件,作为默认页面:

    jsx 复制代码
    // pages/index.js
    function HomePage() {
      return (
        <div>
          <h1>前端柒八九</h1>
        </div>
      );
    }
    
    export default HomePage;
  3. 导航: Next.js App Router 提供了 Link 组件来实现内部页面之间的导航。

    jsx 复制代码
    // pages/index.js
    import Link from 'next/link';
    
    function HomePage() {
      return (
        <div>
          <h1>前端柒八九</h1>
          <Link href="/about">
            <a>北宸南蓁</a>
          </Link>
        </div>
      );
    }
    
    export default HomePage;
    jsx 复制代码
    // pages/about.js
    function AboutPage() {
      return (
        <div>
          <h1>北宸南蓁</h1>
        </div>
      );
    }
    
    export default AboutPage;
  4. 启动开发服务器: 在终端中运行以下命令以启动 Next.js开发服务器。

    bash 复制代码
    npm run dev
    # 或
    yarn dev

    访问 http://localhost:3000 可以看到主页,点击 "北宸南蓁" 链接可以切换到关于页面。

Next.js App Router 简化了页面导航和路由管理,使开发者能够更轻松地创建多页面应用。使用 Link 组件可以实现内部页面之间的无刷新切换,而无需重新加载整个页面。这对于提供更好的用户体验非常有帮助。


2. React :客户端UI库

自诞生以来,React 一直是一个客户端UI库 。它是一个基于JavaScript的开源库,帮助Web和移动开发者构建采用组件化架构的应用程序。

React的哲学建议我们将整个设计拆分成更小、自包含的组件,称为组件(components)。

然后,这些组件可以拥有自己的私有数据,称为状态state),以及在其他组件之间传递数据的方式,称为属性props)。我们将这些组件分解为一个组件层次结构,定义状态,管理改变状态的效果,并决定数据的流动。

所有React组件都是JavaScript函数。

当应用程序在浏览器上加载时,我们下载组件代码并使用它们使应用程序正常运行。


3. 传统 React 应用的通病

React客户端组件在解决特定用例方面表现良好。但是,在有些场景下,它表现的差强人意.

现在让我们看一下我们可能会遇到的一些常见问题示例。

布局抖动

一个非常常见的用户体验问题是组件渲染时突然的布局变化。

jsx 复制代码
const App = () => {
    return (
        <Wrapper>
            <ComponentA />
            <ComponentB />
        </Wrapper>
    )
}

我们有两个组件,ComponentAComponentB,它们作为子组件传递给一个 Wrapper 组件。

每个组件的主体看起来类似于这样:

Wrapper 组件

jsx 复制代码
const Wrapper = ({children}) => {
  
  const [wrapperData, setWrapperData] = useState({});
  
  useEffect(() => {
    // 模拟异步接口
    getWrapperData().then(res => {
      setWrapperData(res.data);
    });
  }, []);
  
  // 只有异步接口,成功返回,才开始渲染子组件(通过children)
  return (
  	<>
      <h1>{wrapperData.name}</h1>
      <>
        {wrapperData.name && children}
      </>
    </>
  )
}

ComponentA组件

jsx 复制代码
const ComponentA = () => {
  const [componentAData, setComponentAData] = useState({});
  
  useEffect(() => {
    getComponentAData().then(res => {
      setComponentAData(res.data);
    });
  }, []);
  
  return (
  	<>
      <h1>{componentAData.name}</h1>
    </>
  )
}

ComponentB组件

jsx 复制代码
const ComponentB = () => {
  const [componentBData, setComponentBData] = useState({});
  
  useEffect(() => {
    getComponentBData().then(res => {
      setComponentBData(res.data);
    });
  }, []);
  
  return (
  	<>
      <h1>{componentBData.name}</h1>
    </>
  )
}

每个组件都负责获取自己的数据 。因此,每个组件都不受其他组件数据的干扰。这种情况,貌似很玩美. (徐志胜语音包)

但是,如果遇到下面的情况,阁下该如何应对呢. 让我们慢慢道来.

假设从每个组件发起的 API 调用获取响应的时间如下:

  • <Wrapper /> 获取响应需要 1 秒
  • <ComponentB /> 获取响应需要 2 秒
  • <ComponentA /> 获取响应需要 3 秒

这里就会出现很匪夷所思的场景

  • Wrapper 在 1 秒后对用户可见。
  • 然后 ComponentB 在 2 秒后出现。
  • 经过 3 秒,ComponentA 出现。但是 ComponentA 的出现会将 ComponentB 推下去,就好像 ComponentA 突然冒出来一样。这不是很好的用户体验。

从网上找了一个类似的效果,大家可以不必要特意强调组件名称的异同.只看对应的效果即可.

这就是我们常说的页面抖动,而这个情况,又会产生布局位移。 导致网页视觉稳定性 很差. 如果想了解更过,可以查看我们之前写的CLS.

当然,我们可以通过使用加载指示器闪烁效果来改善体验,告诉用户稍后会有一些内容出现。但是,这个效果(自认为)是一种掩耳盗铃的方式.


网络瀑布流

另一个问题是,子组件(ComponentAComponentB)甚至在 Wrapper 组件从其所发起的 API 调用获取响应之前都没有被渲染出来,这导致了一个瀑布效应(Waterfall)。连续的数据获取总是会引入瀑布效应

在我们的示例中,只有在 Wrapper 组件中的 API 调用获取响应之后,其他两个组件才会被渲染出来。


可维护性问题

我们如何解决瀑布问题呢?

现在假设我们的任何组件都不进行任何网络调用。我们一次性使用 单个API调用fetchAllDetails()获取所有组件的详细信息,包括父组件在内。

之后,我们将所需的信息作为props传递给每个组件。这种处理方式,或多或少 可以减少瀑布问题

jsx 复制代码
const App = () => {
    // 假设,该网络调用在合适的地方进行调用(`useEffect`等)
    const info = fetchAllDetails();
    
    return(
    	<Wrapper ino={info.wrapperInfo} >
        <ComponentA ino={info.AInfo} />
        <ComponentB ino={info.BInfo} />
      </Wrapper>     
    )
}

这种方法并没有什么问题。但是,API 的响应与我们的组件之间耦合度很高 。这可能会导致一些可维护性问题。

假设有一天,善变小可爱 产品决定放弃ComponentA功能。那么我们可以简单地去掉上面代码中的ComponentA组件。这没问题!我们还希望从 API 响应中删除 AInfo,因为我们不想处理组件未使用的数据。毕竟,如果没有 ComponentA,那就不需要 AInfo


性能成本

我们将要讨论的最后一个问题领域是性能成本

上图形象的描绘了JavaScript对客户端带来的负担

React组件是客户端JavaScript函数。它们是我们的React应用程序的构建块。当我们在客户端加载应用程序时,组件会下载到客户端,React会执行必要的操作来为我们渲染它们。

但是这会带来两个重要问题:

首先,当用户发送请求时,应用程序会下载HTML以及链接的JavaScriptCSS和其他资产,如Image

在客户端(浏览器上),React开始执行其魔法,并进行HTML结构的水合(hydrates)。它解析HTML,将事件侦听器附加到DOM,并从存储中获取数据。因此,该站点变成了一个完全操作的React应用程序。

但问题是,客户端上会发生很多事情。我们最终会将所有这些代码都下载到客户端。

通常情况下,我们需要将外部库(Node模块)作为项目的依赖项。所有这些依赖项都会在客户端上下载,使其变得更加臃肿。


SSR 和 Suspense 解决的痛点

为了更好地理解对 RSC 的需求,首先需要理解对服务器端渲染(SSR)和 Suspense 的需求。

SSR 关注初始页面加载,将预渲染的 HTML 发送到客户端,然后在它被下载的 JavaScript 注入后,才会表现为典型的 React 应用程序行为。SSR仅发生一次:在直接导航到页面时

仅仅使用 SSR,用户可以更快地获取 HTML,但必须在all or nothing的瀑布流之前等待,然后才能与 JavaScript 进行交互:

  • 必须从服务器获取所有数据,然后才能显示其中的任何内容。
  • 必须从服务器下载所有 JavaScript,然后才能将客户端注入其中。
  • 必须在客户端上完成所有的注入,然后才能与任何内容进行交互。

为了解决这个问题,React 创建了 Suspense,它允许在服务器端进行 HTML 流式传输,并在客户端上进行选择性的注入。通过将组件包装在 <Suspense> 中,我们可以告诉服务器将该组件的渲染和注入降低优先级,让其他组件在不受较重组件阻塞的情况下加载

当我们在 <Suspense> 中有多个组件时,React 会按照我们编写的顺序从上往下处理树状结构,使我们的应用程序能够进行最优化的流式传输。然而,如果用户尝试与某个特定组件进行交互,该组件将优先于其他组件。

这大大改善了情况,但仍然存在一些问题:

  • 在显示任何组件之前,必须从服务器获取整个页面的数据。唯一的方法是在 useEffect() 钩子中在客户端进行数据获取,这比服务器端获取需要更长的往返时间,并且仅在组件渲染和注入后才发生。

  • 所有页面的 JavaScript 最终都会被下载,即使它以异步方式流式传输到浏览器。随着应用程序的复杂性增加,用户下载的代码量也会增加。

  • 尽管优化了注入,用户仍然无法与组件进行交互,直到客户端的 JavaScript 被下载并且为该组件实现。

  • 大部分 JavaScript 计算负荷仍然位于客户端,可能在各种不同类型的设备上运行。


通过上面的各种举证和分析,我们或多或少的知道,React在平时开发中遇到的一些令人深恶痛绝 的问题. 其实React官方也知道这些问题,所以提出了RSC.

但在我们谈论这些之前,让我们更多地了解一下客户端服务器


4. 客户端-服务器模型

在本文中,我们已经多次使用了"客户端""服务器"这两个术语。让我们高屋建瓴的解释它们之间的关系

  • 客户端:在应用程序方面,客户端是在最终用户端执行任务的系统。客户端包括我们的台式电脑、笔记本电脑、移动设备、浏览器等。

  • 服务器:字如其人,服务器为客户端提供服务。它可以与数据存储或数据库共存,以便快速访问数据。

  • 请求:请求是客户端用于向服务器请求服务的通信方式。

  • 响应:响应也是服务器用于将服务(数据/信息)发送回客户端的通信方式。

如果想了解更多关于网络相关的东西,可以参考之前写的网络篇


在服务器组件出现之前,我们编写的所有 React 代码都是在客户端(浏览器)上进行渲染的。因此,为了与在服务器上进行渲染的服务器组件区分开来,从现在开始,我们将常规的 React 组件(其中使用状态、effect、仅限于浏览器的 API 等)称为客户端组件(Client Components)。

React Client Components

传统上React组件存在于客户端。当它们与服务器交互时,它们发送请求并等待响应返回。在接收到响应后,客户端触发下一组操作。

如果请求的服务成功完成,客户端组件将根据UI采取相应操作,并显示成功消息。如果出现错误,客户端组件会向用户报告错误信息。

当它引起网络瀑布问题时,客户端组件的响应被延迟,从而导致糟糕的用户体验。


React Server Components

我们可以将React组件迁移到服务器上.也就是说我们可以将它们与后台数据一起放置.

让我们现在来了解一下RSC。这些新的组件可以更快地获取数据 ,因为它们位于服务器上。它们可以访问我们的服务器基础设施,如文件系统数据存储,而无需通过网络进行任何往返

对于React开发者来说,这是一个完整的范式转变,因为现在我们必须从服务器组件的角度来思考

使用RSC,我们可以将数据获取逻辑移至服务器(使我们的组件无需网络调用即可获取数据),并在服务器上准备好它。返回到客户端的数据是一个精心构造的组件,其中包含了所有的数据。

这意味着使用RSC,我们可以编写如下的代码:

jsx 复制代码
import { dbConnect } from '@/services/mongo'

import { addCourseToDB } from './actions/add-course'

import CourseList from './components/CourseList'

export default async function App() {

  // 建立 MongoDB 链接
  await dbConnect();
  
  // 从数据库(db)中获取对应的数据信息
  const allCourses = await courses.find();
  
  // 数据校验(查看是否成功和数据格式)
  console.log({allCourses})

  return (
    <main>
      <div>
        <CourseList allCourses={allCourses} />  
      </div>
    </main>
  )
}

从上面的代码中我们可以注意到一些写法上的变化

  • 组件的类型是async,因为它将处理异步调用。
  • 我们从组件本身连接到数据库(MongoDB)。
    • 在常规的开发中,我们只有在Node.jsExpress中才会看到这种代码
  • 然后我们查询数据库并获取数据,以便将其传递给我们的JSX进行渲染。
  • 注意,控制台日志会在服务器控制台上记录,而不是在我们的浏览器控制台上。

另外,我们完全摆脱了状态管理(useState)和副作用管理(useEffect)。

使用RSC,我们可能不需要使用useEffect老死不相往来的那种)。


6. RSC的红与黑

以下是关于RSC可以做和不能做的事情的列表。尽管服务器组件可能看起来很高级,但并不意味着我们可以在任何地方都使用它们。

可以做的事情:

  • 使用 async/await 与仅限于服务器的数据源,如数据库内部服务文件系统等进行数据获取。
  • 渲染其他服务器组件、本地元素(如 divspan 等)或客户端组件(普通的 React 组件)。

不能做的事情:

  • 无法使用 React 提供的钩子,比如 useStateuseReduceruseEffect 等,因为服务器组件是在服务器上渲染的。
  • 不能使用浏览器 API,比如本地存储等(不过在服务器上可以进行 polyfill)。
  • 不能使用依赖于仅限于浏览器 API(例如本地存储)或依赖于状态或效果的自定义钩子的任何实用函数。

7. 如何同时使用客户端组件和服务器组件

我们的应用程序可以是服务器组件和客户端组件的组合。

服务器组件可以导入并渲染客户端组件,但客户端组件不能在其中渲染服务器组件。如果我们想在客户端组件中使用服务器组件,我们可以将其作为props传递并以这种方式使用。

最好将服务器组件放在组件层次结构的根部 ,并将客户端组件推向组件树的叶子

数据获取可以在服务器组件的顶部进行,并可以按照React允许的方式进行传递。用户交互(事件处理程序)和访问浏览器API可以在客户端组件中的叶子级别进行处理。

客户端组件无法导入服务器组件,但反过来是可以的。在服务器组件内部导入客户端组件或服务器组件都是可以的。而且,服务器组件可以将另一个服务器组件作为子组件传递给客户端组件,例如:

javascript 复制代码
const ServerComponentA = () => {
    return (
        <ClientComponent>
            <ServerComponentB />
        </ClientComponent>
    )
}

在上面的示例中,我们将一个名为 ServerComponentB 的服务器组件作为子组件传递给了客户端组件。

让我们总结一下:

  • 可以在服务器组件内部导入客户端组件。
  • 不能在客户端组件内部导入服务器组件。
  • 可以将一个服务器组件作为子组件传递给服务器组件内的客户端组件。

RSC vs SSR

RSCSSR两者的名字都包含了Server这个词,但相似之处仅限于此。

通过SSR,我们将原始HTML从服务器发送到客户端,然后所有客户端的JavaScript都被下载。React开始水合化过程,将HTML转换为可交互的React组件。在SSR中,组件不会留在服务器上

而使用RSC组件会留在服务器上,并且可以访问服务器基础设施,而无需进行任何网络往返。

SSR用于加快应用程序的初始页面加载速度 。我们可以在应用程序中同时使用SSRRSC,而不会出现任何问题。


8. RSC的优点

零捆绑包大小的组件

使用库对开发人员很有帮助,但它会增加捆绑包的大小,可能会影响应用程序性能。

应用程序的许多部分并不是交互式的,也不需要完全的数据一致性。例如,详细信息页面通常显示有关产品、用户或其他实体的信息,不需要根据用户交互来更新。

RSC允许开发人员在服务器上渲染静态内容。我们可以自由地在服务器组件中使用第三方包,而不会对捆绑包大小产生任何影响。

常规组件

javascript 复制代码
import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)

function NoteWithMarkdown({text}) {
  const html = sanitizeHtml(marked(text));
  return (/* render */);
}

如果我们将上面的示例渲染为RSC,我们可以使用完全相同的代码来实现我们的功能,但避免将其发送到客户端 - 这将节省超过 240K 的代码(未压缩)。

Server Component (零捆绑包大小)

javascript 复制代码
import marked from 'marked'; // 零捆绑包
import sanitizeHtml from 'sanitize-html'; // 零捆绑包

function NoteWithMarkdown({text}) {
  // ....
}

简而言之,如果我们在服务器组件内使用任何第三方库,该库将不会包含在客户端的捆绑包中。这将减小 JavaScript 捆绑包的大小。

换句话说,通过服务器组件,初始页面加载更快,更精简。基本的客户端运行时是可缓存的,并且大小是可预测的,不会随着应用程序的增长而增加。额外的面向用户的 JavaScript 主要是在我们的应用程序通过客户端组件需要更多的客户端交互时添加的。

如果我们在任何客户端组件内部使用该库,那么就如我们所想,该库将包含在客户端捆绑包中,并将被浏览器下载以进行解析和执行。


全权访问后端数据

正如前面所讨论的,服务器组件可以利用直接的后端访问来使用数据库、内部(微)服务和其他仅限于后端的数据源。

jsx 复制代码
import db from 'db';

async function Note({id}) {
  const note = await db.notes.get(id);
  return <NoteWithMarkdown note={note} />;
}

在上面的代码片段中,我们将 note 传递给了 NoteWithMarkdown 组件。我们可以直接从数据库中获取这个note.

如果我们仔细查看代码,我们会发现我们没有进行任何获取 API 调用来获取 note。相反,我们只是在 Note 组件内直接执行了 DB 查询(通常我们在服务器端代码中执行 DB 查询)。这是可能的,因为这是一个服务器组件,它在服务器上进行渲染。

让我们再看一个例子,其中我们可以从服务器的服务器组件中访问文件系统

jsx 复制代码
import fs from 'fs';

async function Note({id}) {
  const note = JSON.parse(await fs.readFile(`${id}.json`));
  return <NoteWithMarkdown note={note} />;
}

正如我们在上面的代码中所看到的,我们使用了 fs 模块(文件系统的缩写)来读取服务器上存在的文件。


自动代码分割

服务器组件将所有对客户端组件的导入视为潜在的代码分割点。

有如下的SRC

jsx 复制代码
import OldPhotoRenderer from './OldPhotoRenderer.js';
import NewPhotoRenderer from './NewPhotoRenderer.js';

function Photo(props) {
  // 根据业务进行组件的渲染
  if (FeatureFlags.useNewPhotoRenderer) {
    return <NewPhotoRenderer {...props} />;
  } else {
    return <OldPhotoRenderer {...props} />;
  }
}

在上面的示例中,我们有两个组件 NewPhotoRendererOldPhotoRenderer(两者都是客户端组件),它们是有条件地进行渲染的。

假设 if (FeatureFlags.useNewPhotoRenderer) 值为 True,那么用户将会看到 NewPhotoRenderer 组件。只有该组件会被发送到客户端(或浏览器)。OldPhotoRenderer 将被懒加载(也就是说,它不会立即被发送到客户端)。因此,只有与用户可见的组件相关的 JavaScript 是需要的。


没有瀑布效应

正如前面讨论过的,连续的数据获取会引入瀑布效应。我们希望找到一种方法来避免从客户端到服务器的连续往返延迟(也就是说,我们必须等待一个请求完成,而请求可能需要一些时间来完成,因为它必须从客户端传输到服务器)。

jsx 复制代码
async function Note(props) {
  // NOTE: 在渲染期间加载,在服务器上进行低延迟数据访问
  const note = await db.notes.get(props.id);
  if (note == null) {
    // 处理note 未被获取的逻辑
  }
  return (/* 根据note 渲染相关页面*/);
}

服务器组件通过将连续的往返请求移到服务器上,使应用程序能够实现这一目标(即不再有从客户端到服务器的获取调用)。

问题实际上并不是往返请求本身,而是这些请求是从客户端到服务器的。通过将这个逻辑移到服务器上,我们减少了请求的延迟,提高了性能。


服务器组件与客户端代码完全兼容

服务器组件与客户端代码完全兼容,这意味着客户端组件和服务器组件可以在同一个 React 树中进行渲染。通过将大部分应用程序代码移到服务器上,服务器组件有助于防止客户端数据获取的瀑布效应,快速解决服务器端的数据依赖关系。

在传统的客户端渲染中,组件使用 React Suspense"暂停"其渲染过程(并显示回退状态),同时等待异步工作完成。通过服务器组件,数据获取和渲染都在服务器上进行,因此 Suspense 也会在服务器端管理等待期,从而缩短总的往返时间,加快回退和完成页面的渲染。

需要注意的是,客户端组件在初始加载时仍然进行服务器端渲染(SSR)。服务器组件模型并不取代 SSR 或 Suspense,而是与它们一起工作,根据需要为用户提供应用程序的所有部分

在使用 Next.jsReact 服务器组件时,数据获取和 UI 渲染可以在同一个组件中完成。此外,服务器操作(Server Actions)为用户提供了在页面上的 JavaScript 加载之前与服务器端数据进行交互的方式。


9. 如何使用Next.js和MongoDB构建课程列表页面

现在让我们用Next.js构建一个使用RSC的应用程序。

因此,我们现在将构建一个课程列表页面,以展示我们如何在Next.js中创建服务器组件,以及它与客户端组件的不同之处。

请注意,我们不会在这里深入学习Next.jsMongoDB。我们只是将这个应用程序作为一个示例,来教我们RSC的工作原理以及它们与客户端组件的区别。

首先,让我们将课程数据添加到数据存储中。对于这个应用程序,我使用了MongoDB。下面的图像显示添加了三个课程的三个文档。

接下来,我们将创建一个实用函数来建立与MongoDB的连接。这是一个通用的代码,我们可以在任何基于JavaScript的项目中使用它,以使用MongooseMongoDB URI连接到MongoDB

javascript 复制代码
import mongoose from "mongoose";

export async function dbConnect(): Promise<any> {
  try {
    const conn = await mongoose.connect(String(process.env.MONGO_DB_URI));
    console.log(`Database connected : ${conn.connection.host}`);
    return conn;
  } catch (err) {
    console.error(err);
  }
}

现在,我们需要创建与MongoDB文档相对应的模型(modal)。由于我们处理的是课程数据,这是与之对应的模型:

javascript 复制代码
import mongoose, { Schema } from "mongoose";

const schema = new Schema({
  name: {
      required: true,
      type: String
  },
  description: {
      required: true,
      type: String
  },
  cover: {
    required: true,
    type: String
  },
  rating: {
    required: true,
    type: Number
  },
  price: {
    required: true,
    type: Number
  },
  createdOn: {
    type: { type: Date, default: Date.now }
  },
  link: {
    required: true,
    type: String
  },
  type: {
    required: true,
    type: String
  },
  comments: {
    required: false,
    type: [{ body: String, date: Date }]
  }
});

export const courses = mongoose.models.course ?? mongoose.model("course", schema);

通过Next.js App Router,所有的组件默认都是服务器组件。这意味着它们位于靠近服务器的位置,并且可以访问我们的服务器生态系统。

下面的代码是一个常规的Next.js组件,但具有一个特殊功能:我们可以在组件中直接获取数据库连接,并直接查询数据,而无需经过任何状态和效果管理

从该组件中记录的任何内容都不会被记录到我们的浏览器控制台,因为这是一个服务器组件。我们可以在服务器控制台中查看日志(我们可以使用yarn dev命令启动服务器的终端)。

由于与数据库的交互是异步的 ,我们在进行调用时使用await关键字,并在组件上使用async关键字。在接收到响应后,我们将其作为属性传递给子组件。

jsx 复制代码
import { dbConnect } from '@/services/mongo'
import { courses } from '@/models/courseModel'
import { addCourseToDB } from './actions/add-course'

import AddCourse from './components/AddCourse'
import CourseList from './components/CourseList'

export default async function Home() {

  // 建立MongoDB链接
  await dbConnect();
  
  //获取所有的数据信息
  const allCourses = await courses.find().select(
  						["name", "cover", "rating"]);
  
  // 在服务器终端中打印显示数据
  console.log({allCourses})

  return (
    <main>
      <div>
        <h1>Courses</h1> 
        <AddCourse addCourseToDB={addCourseToDB} />
        <CourseList allCourses={allCourses} />  
      </div>
    </main>
  )
}

Home组件包含:

  • 一个标题
  • 一个组件(AddCourse),用于包装一个添加课程的按钮
  • 一个组件(CourseList),用于将课程显示为列表。

我们知道,服务器组件可以同时渲染客户端和服务器组件AddCourse组件需要用户交互,即用户需要点击按钮来添加课程。所以它不能是服务器组件.

因此,让我们为AddCourse创建一个客户端组件。通过Next.js App Router,默认情况下,所有组件都是服务器组件。

如果我们想创建一个客户端组件,我们必须在组件顶部(甚至在任何导入语句之前)使用名为use client的指令来明确创建一个客户端组件。

客户端组件- AddCourse

jsx 复制代码
'use client'

import { useState } from 'react';
import Modal from './Modal';
import AddCourseForm from "./AddCourseForm";

export default function AddCourse({
  addCourseToDB,
}: {
  addCourseToDB: (data: any) => Promise<void>
}) {
  const [showAddModal, setShowAddModal] = useState(false);
  const add = async(data: any) => {
    await addCourseToDB(data);
    setShowAddModal(false);
  }

  return (
    <>
      <button
        onClick={() => setShowAddModal(true)}
      >
        Add Course
      </button>
      <Modal 
        shouldShow={showAddModal} 
        body={
          <AddCourseForm 
            saveAction={add} 
            cancelAction={() => setShowAddModal(false)} />} />
    </>
  )
}

服务器组件 -CourseList

CourseList组件不需要任何事件处理程序,因此我们可以将其保持为服务器组件。

jsx 复制代码
import Image from 'next/image'
import Link from 'next/link'

export default function CourseList(courseList: any) {
  const allCourses = courseList.allCourses;
  return(
    <div>
      {
        allCourses.map((course: any) =>
        <Link key={course['_id']} href={`/courses/${course['_id']}`}>
          <div>
            <Image
              src={course.cover}
              width={200}
              height={200}
              alt={course.name}
            />
            <h2>{course.name}</h2>
            <p>{course.rating}</p>
          </div> 
        </Link> 
      )}
    </div>  
  )

}

我们打开浏览器开发工具的Sources选项卡,以确定客户端上下载了什么,服务器上留下了什么。我们在这里是看不到page.tsx文件或CourseList.tsx文件信息。这是因为这些是服务器组件,它们永远不会成为我们的客户端捆绑包的一部分

我们只会看到我们在应用程序中明确标记为客户端组件的组件。

Next.js App Router 中,所有获取的数据现在默认为静态数据,在构建时渲染。然而,这可以很容易地改变:Next.js 扩展了 fetch 选项对象,以提供缓存和重新验证规则的灵活性。

我们可以使用 {next: {revalidate: number}} 选项以设置的时间间隔或在后端更改发生时刷新静态数据(增量静态再生成),而 {cache: 'no-store'} 选项可以在动态数据的 fetch 请求中传递(服务器端渲染)。


总结

总结一下:

  • React服务器组件具有后端访问权限,无需进行任何网络往返。
  • 我们可以通过使用RSC来避免网络瀑布问题。
  • React服务器组件支持自动代码拆分,并通过零捆绑大小提高应用程序的性能。
  • 由于这些组件位于服务器端,它们无法访问客户端端的事件处理程序、状态和效果。这意味着我们不能使用任何事件处理程序或React钩子,如useStateuseReduceruseEffect
  • React服务器组件可以导入并渲染客户端组件,但反之则不成立。但我们可以将服务器组件作为props传递给客户端组件。
  • RSC并不意味着取代客户端组件。健康的应用程序同时使用服务器组件来进行动态数据获取以及客户端组件来实现丰富的交互性。挑战在于确定何时使用每种组件。

后记

分享是一种态度

参考资料:

  1. understanding-react-server-components
  2. react-server-components-for-beginners/
  3. how-to-use-react-server-components/
  4. what-even-are-react-server-components/

全文完,既然看到这里了,如果觉得不错,随手点个赞和"在看"吧。

相关推荐
也无晴也无风雨38 分钟前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang1 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
小牛itbull3 小时前
ReactPress:构建高效、灵活、可扩展的开源发布平台
react.js·开源·reactpress
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤5 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js