生活的乐趣取决于生活都本身,而不是取决于工作或地点
大家好,我是柒八九。
前言
在上一篇介绍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的交互性
无缝地融合在一起。
所以,我们有必要用一篇文章来介绍它.(文章有点长,请耐心观看,并且内容有点内核,配合收藏观看更佳)
你能所学到的知识点
- 前置知识点
- React :客户端UI库
- React 应用的通病
- SSR 和 Suspense 解决的痛点
- 客户端-服务器模型
- RSC的红与黑
- 如何同时使用客户端组件和服务器组件
- RSC的优点
好了,天不早了,干点正事哇。
1. 前置知识点
网络瀑布效应
网络瀑布效应(Network Waterfall Effect
)是一个用于描述在计算机网络
中出现的性能问题的概念。它通常用来说明在复杂的网络环境中,一个小问题的出现可能会逐渐扩大,导致整个网络的性能下降。(类似多米诺骨牌一样)
网络瀑布效应的核心思想是,网络中的各个组件和节点之间相互依赖,一个组件的问题可能会影响到其他组件,从而引发连锁反应。
这种效应通常在大规模、分布式
的网络中更为显著,因为网络中的节点众多,问题的传播速度和范围都会加大。
这意味着后续的获取请求仅在前一个获取请求被解析或完成后才会被初始化。
水合(Hydration
)
在计算机科学领域,水合
(Hydration
)通常指的是将数据或状态从一种格式或状态转换为另一种格式或状态的过程。
React/Vue 水合
React
和Vue
的水合流程大差不差(反正都是各自SSR
流程中的一部分,只是具体API
不同,原理都是一样的),所以我们只按其中一种介绍,另外一种或者说其他更多的前端框架,你只需要换个名字就可以了. (按图索骥
,照猫画虎
会哇).
"React 水合"(
React Hydration
)是指将服务器端渲染
(Server-Side Rendering
,SSR
)得到的HTML 结构
与客户端的JavaScript 表现层
相结合的过程。
React 水合
是在客户端渲染(Client-Side Rendering
,CSR
)和服务器端渲染之间的一个关键步骤,确保在将服务器渲染的 HTML
呈现给用户之后,React
组件能够在客户端接管并继续工作。
以下是 React 水合
的详细步骤和背后的原理:
-
服务器端渲染(SSR): 在
服务器端
使用React
渲染组件,生成一段包含完整组件结构的HTML
。- 这段
HTML
可以包含组件的初始状态,这样在首次加载页面时,用户将看到已经有内容呈现在页面上,而不需要等待客户端JavaScript
加载和执行。
- 这段
-
客户端 JavaScript 加载: 在浏览器中加载包含
React
应用逻辑的JavaScript
文件。- 这些文件可能包括应用的组件、状态管理逻辑、事件处理等。
-
水合阶段: 一旦客户端
JavaScript
加载完成,React 将接管页面,开始水合过程。- 这意味着
React
会检查服务器端渲染生成的HTML
,并将其与客户端JavaScript
中的组件逻辑进行匹配。
- 这意味着
-
组件恢复和事件绑定: 在水合阶段,
React
会将服务器端渲染的HTML
中的组件恢复到其初始状态,并建立与客户端JavaScript
中的相应组件的联系。- 这包括建立事件绑定、状态同步等。
-
交互和动态更新: 一旦水合完成,
React
组件就会变得可交互。- 用户可以与页面进行互动,而客户端
JavaScript
负责处理事件、状态更改等 - 此后,页面将继续响应用户操作,动态地更新内容,而无需再次从服务器获取完整的
HTML
。
- 用户可以与页面进行互动,而客户端
React 水合
的优势在于它结合了服务器端渲染
和客户端渲染
的优点,提供了更好的性能和用户体验。
通过在首次加载时提供一部分已渲染的内容 ,用户可以更快地看到页面,并与之互动。然后,客户端
JavaScript
接管页面,继续处理后续的交互和动态更新。
Next 简单使用教程
Next.js
是一个基于 React
的框架,用于构建具有服务器端渲染(SSR
)和静态网站生成(SSG
)功能的应用程序。Next.js
提供了一个内置的路由系统 ,称为 Next.js App Router
,用于管理应用程序的路由和页面导航。
下面是一个简单的介绍和代码示例,展示如何使用 Next.js App Router
:
-
安装 Next.js: 首先,你需要在项目中安装
Next.js
。你可以使用npm
或yarn
进行安装。bashnpm install next react react-dom # 或 yarn add next react react-dom
-
创建页面: 在
Next.js
中,页面是位于pages
目录下的React
组件。每个页面对应一个 URL 路由。在项目根目录下创建
pages
目录,并在其中创建一个名为index.js
的文件,作为默认页面:jsx// pages/index.js function HomePage() { return ( <div> <h1>前端柒八九</h1> </div> ); } export default HomePage;
-
导航: 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;
-
启动开发服务器: 在终端中运行以下命令以启动
Next.js
开发服务器。bashnpm 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>
)
}
我们有两个组件,ComponentA
和 ComponentB
,它们作为子组件
传递给一个 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.
当然,我们可以通过使用加载指示器
或闪烁效果
来改善体验,告诉用户稍后会有一些内容出现。但是,这个效果(自认为)是一种掩耳盗铃的方式.
网络瀑布流
另一个问题是,子组件(ComponentA
和 ComponentB
)甚至在 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
以及链接的JavaScript
、CSS
和其他资产,如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.js
或Express中
才会看到这种代码
- 在常规的开发中,我们只有在
- 然后我们查询数据库并获取数据,以便将其传递给我们的JSX进行渲染。
- 注意,
控制台日志
会在服务器控制台上记录,而不是在我们的浏览器控制台上。
另外,我们完全摆脱了状态管理(useState
)和副作用管理(useEffect
)。
使用RSC
,我们可能不需要使用useEffect
(老死不相往来
的那种)。
6. RSC的红与黑
以下是关于RSC
可以做和不能做的事情的列表。尽管服务器组件可能看起来很高级,但并不意味着我们可以在任何地方都使用它们。
可以做的事情:
- 使用
async/await
与仅限于服务器的数据源,如数据库
、内部服务
、文件系统
等进行数据获取。 - 渲染其他服务器组件、本地元素(如
div
、span
等)或客户端组件(普通的 React 组件)。
不能做的事情:
- 无法使用
React
提供的钩子,比如useState
、useReducer
、useEffect
等,因为服务器组件是在服务器上渲染的。 - 不能使用
浏览器 API
,比如本地存储等(不过在服务器上可以进行polyfill
)。 - 不能使用依赖于仅限于浏览器 API(例如本地存储)或依赖于状态或效果的自定义钩子的任何实用函数。
7. 如何同时使用客户端组件和服务器组件
我们的应用程序可以是服务器组件和客户端组件的组合。
服务器组件
可以导入并渲染客户端组件,但客户端组件不能在其中渲染服务器组件。如果我们想在客户端组件中使用服务器组件,我们可以将其作为props
传递并以这种方式使用。
最好将服务器组件放在组件层次结构的根部 ,并将客户端组件推向组件树的叶子。
数据获取可以在服务器组件的顶部进行,并可以按照React
允许的方式进行传递。用户交互(事件处理程序)和访问浏览器API可以在客户端组件中的叶子级别进行处理。
客户端组件
无法导入服务器组件,但反过来是可以的。在服务器组件
内部导入客户端组件或服务器组件都是可以的。而且,服务器组件可以将另一个服务器组件作为子组件传递给客户端组件,例如:
javascript
const ServerComponentA = () => {
return (
<ClientComponent>
<ServerComponentB />
</ClientComponent>
)
}
在上面的示例中,我们将一个名为 ServerComponentB
的服务器组件作为子组件传递给了客户端组件。
让我们总结一下:
- 可以在服务器组件内部导入客户端组件。
- 不能在客户端组件内部导入服务器组件。
- 可以将一个服务器组件作为子组件传递给服务器组件内的客户端组件。
RSC vs SSR
RSC
和SSR
两者的名字都包含了Server
这个词,但相似之处仅限于此。
通过SSR
,我们将原始HTML
从服务器发送到客户端,然后所有客户端的JavaScript
都被下载。React
开始水合
化过程,将HTML
转换为可交互的React
组件。在SSR
中,组件不会留在服务器上。
而使用RSC
,组件会留在服务器上,并且可以访问服务器基础设施,而无需进行任何网络往返。
SSR
用于加快应用程序的初始页面加载速度 。我们可以在应用程序中同时使用SSR
和RSC
,而不会出现任何问题。
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} />;
}
}
在上面的示例中,我们有两个组件 NewPhotoRenderer
和 OldPhotoRenderer
(两者都是客户端组件),它们是有条件地进行渲染的。
假设 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.js
和React
服务器组件时,数据获取和 UI 渲染可以在同一个组件中完成。此外,服务器操作(Server Actions)为用户提供了在页面上的JavaScript
加载之前与服务器端数据进行交互的方式。
9. 如何使用Next.js和MongoDB构建课程列表页面
现在让我们用Next.js
构建一个使用RSC
的应用程序。
因此,我们现在将构建一个课程列表页面,以展示我们如何在Next.js
中创建服务器组件,以及它与客户端组件的不同之处。
请注意,我们不会在这里深入学习
Next.js
或MongoDB
。我们只是将这个应用程序作为一个示例,来教我们RSC
的工作原理以及它们与客户端组件的区别。
首先,让我们将课程数据添加到数据存储中。对于这个应用程序,我使用了MongoDB
。下面的图像显示添加了三个课程的三个文档。
接下来,我们将创建一个实用函数来建立与MongoDB
的连接。这是一个通用的代码,我们可以在任何基于JavaScript
的项目中使用它,以使用Mongoose
和MongoDB 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
钩子,如useState
、useReducer
和useEffect
。 React
服务器组件可以导入并渲染客户端组件,但反之则不成立。但我们可以将服务器组件作为props
传递给客户端组件。RSC
并不意味着取代客户端组件。健康的应用程序同时使用服务器组件来进行动态数据获取以及客户端组件来实现丰富的交互性。挑战在于确定何时使用每种组件。
后记
分享是一种态度。
参考资料:
- understanding-react-server-components
- react-server-components-for-beginners/
- how-to-use-react-server-components/
- what-even-are-react-server-components/
全文完,既然看到这里了,如果觉得不错,随手点个赞和"在看"吧。