我想尽快正确地理解 React 服务器组件和应用路由器
因为在工作中使用React和Next.js,但是我对RSC、App Router和Suspense一无所知。。。真糟糕。。。所以我感到焦虑,同时开始学习并在Qiita上整理了一下。
如果阅读这篇文章,您将理解以下项目。
-
- React や Next.js の基礎知識
-
- React Server Components とは何か?
-
- React Server Components のレンダリングの流れ
-
- Suspense とは何か?
- App Router とは何か?
如果能对于任何与我有类似烦恼的人提供一点帮助,我会非常感激。
太长不看;简而言之。
-
- React は UI を簡単に構築するための JavaScript ライブラリ
-
- Next.js は React のフレームワーク
-
- React は以下の流れでレンダリング(CSR)を行う
レンダリングのトリガーを検知
ブラウザレンダリングする内容の決定
変更を DOM に適用
Next.js には以下の二つのモード(ルーティング方式)がある
Pages Router
App Router
Next.js の Pages Router では以下の 4 つのレンダリング方式を選択できる
SSR
SSG
ISR
CSR
React Server Components(RSC) とは、コンポーネントを「サーバー側でレンダリングされるコンポーネント」と「クライアント側でレンダリングされるコンポーネント」に分ける技術
Next.js の App Router では、デフォルトで作成したコンポーネントがサーバーコンポーネントになる
クライアントコンポーネントにするにはuse clientを記述する必要がある
RSC と SSR を組み合わせることで、初期表示を早めつつ、クライアント側に送信する JavaScript の量を抑えることができる
Suspense は、useState に頼らずに「ローディング中」を表現できる機能
Suspense と React Server Components を使用することで、サーバー側でもコンポーネント単位の非同期的なデータ取得が可能となる
关于 React 和 Next.js
首先,我简要解释一下React和Next.js。
React是一种用于轻松构建用户界面的JavaScript库。
通过使用“组件”这个概念来声明性地定义用户界面,可以轻松地构建屏幕结构。
然后,Next.js 是React的框架。
我们可以将其理解为「通过扩展React的功能,使其更易于使用」。
关于 React 的渲染
React 在使用 create-react-app 创建的初始状态下,会使用CSR(客户端渲染)进行渲染。
CSR 即是在浏览器中执行 JavaScript 以生成 DOM 并显示内容的方法。
在网页初始加载时,没有显示任何内容,在浏览器执行 JavaScript 之后才会首次显示页面。
更详细地解释,React 的渲染是通过以下步骤完成的。
-
- 检测渲染触发器
-
- 决定要在浏览器中渲染的内容
- 将更改应用于DOM
1. 检测渲染触发器
首先,检测触发渲染的引导动作。
在这里的触发器有以下两个。
-
- コンポーネントの初期レンダリング(画面の初期ロード)
- コンポーネントの状態(state)の更新
当React检测到任何一个触发器时,将开始渲染。
2. 决定浏览器渲染的内容
接下来,我们决定要在浏览器中渲染的内容。
这将按照以下的步骤进行。
-
- 调用目标组件
-
- 与之前组件的状态进行比较
- 确定要提交(浏览器渲染)的内容
在关于“调用目标组件”的第一次渲染中,调用根组件,并且在之后的渲染中,通过更新状态触发渲染的组件被调用。
然后,计算与先前组件状态的差异,并决定屏幕更新的内容(包括是否需要更新屏幕)。
当差异被检测到并且需要更新屏幕时,执行操作3。
换句话说,在React中,渲染指的是”调用待渲染组件,比较与上一次内容的差异,决定在浏览器中渲染(commit)什么”。
由于同样被称为“渲染”,很容易混淆,“浏览器渲染(绘制到屏幕上)”与其概念不同,请确保进行明确的区分。
每一步的顺序是,先进行”React 渲染”,然后进行”浏览器渲染”。
根据 React 的官方文档,为了区分 React 渲染和浏览器渲染,我们可以将浏览器渲染表示为 painting(绘制) 。
3. 将更改应用于 DOM。
如果在“浏览器渲染内容的确定”中有与之前状态不同的差异,那么将提交这些差异(将更改应用于 DOM)。
换句话说,我们对DOM树的结构进行修正。
然后,在 React 完成这三个处理之后,浏览器将把这些变更应用于屏幕(浏览器渲染/绘制)。
通过以上的步骤,React 将负责处理界面的显示和更新。
请务必阅读React官方提供的关于这三种流程的解释,它使用餐厅作为例子,非常容易理解。
关于Next.js的渲染
接下来,我们将对Next.js的渲染方式进行总结。
截至2023年9月,Next.js 有以下两种模式(路由方式)。
原本只有页面路由器(Pages Router),但最近添加了应用程序路由器(App Router)。
-
- Pages Router
- App Router
首先,我们将对 Pages 路由器的渲染进行总结。
关于Next.js的渲染(页面路由)
默认情况下,Next.js会对所有页面进行“预渲染”。
预渲染是指 Next.js 在客户端 JavaScript 中生成每个页面的 HTML,而不是事先生成。
据说通过这种预渲染方式,可以提升性能和改善搜索引擎优化(SEO)。
生成的HTML已与必要的JavaScript相关联,当页面被浏览器加载时,该JavaScript将被执行,使页面完全互动。(这被称为水合作用)
Next.js(页面路由)中有2种预渲染方式,可以根据情况进行选择使用。
-
- 静的生成(Static Generation)
HTML はビルド時に生成され、リクエストごとに再利用される
サーバサイドレンダリング(Server-side Rendering)
HTML はリクエストごとに生成される
水合作用是什么意思?
水合作用是指在服务器端渲染的HTML上执行与其相关联的JavaScript,将页面完成为最终状态的过程。
总结一下,大致就是这样的过程。
-
- (SSR等方式下)从服务器端返回HTML
- 执行被发送到客户端的JavaScript(注册事件监听器或添加互动操作)
从服务器接收到的初始 HTML 是一种没有交互功能的干燥 HTML,在客户端上添加所需的配置和功能,可以想象成给它添加了水分。
这主要是用于像 SSR 一样,在服务器端生成并返回 HTML 的情况。
关于Next.js的渲染类型(页面路由器)
在React的情况下,基本上是通过CSR进行渲染,但是在Next.js中,您可以选择多种渲染方式,如下所示。
-
- SSR
-
- SSG
-
- ISR
- CSR
我将对每个进行简要解释。
服务器端渲染 (SSR)
SSR 也被称为动态渲染。
每次请求都会生成页面的HTML的方式。
由于在浏览器中显示JavaScript执行之前,在服务器端生成的原始HTML(DOM),可以加快页面的初始显示速度。
据说这不仅可以改善用户体验,还对SEO有益。
要使用 Pages Router 进行服务器端渲染,需要导出一个名为 getServerSideProps 的异步函数。
SSG(静态网站生成)
如果使用SSG,页面的HTML将在构建时生成。
这个HTML可以在每个请求时重复使用,并且可以由CDN缓存。
在Next.js中,我们不仅可以生成静态HTML页面(即没有数据的页面),还可以使用getStaticProps和getStaticPaths在构建过程中获取和注册数据,并生成HTML页面。
SSG 的特点在于无需服务器每次请求时渲染页面,因此渲染非常快速。
因此,一般而言,建议使用SSG作为渲染方式。
增量静态再生 (ISR)
SSG 技术是预先生成页面,并为每个请求提供其静态副本的方法,而ISR 是对其进一步发展的方式。
通过使用ISR,即使静态页面已经预先生成,也可以在一定时间间隔内重新生成该页面。
借助这一手段,我们能够实现 SSG 的快速响应,并同时提供一定程度的实时性。
通过为Next.js的每个页面设置名为”revalidate”的参数来实现ISR的实现。
设定这个参数来指定再生成的间隔。例如,设置为revalidate: 60,页面每60秒重新生成一次。
换句话说,ISR 提供的功能是同时有效地提供静态内容并定期反映最新信息的机制。
CSR(客户端渲染)
CSR 是 React 的默认渲染方式。
在浏览器中执行 JavaScript 以生成 DOM 并显示内容。
在Next.js中,可以使用useEffect钩子等实现CSR。
RSC(React Server Components)是什么?
接下来,我们将解释Next.js的第二种模式,即App Router,紧随着Pages Router。
然而,要理解App Router,需要先理解其基础技术RSC(React Server Components)。
因此,首先我们将解释什么是“RSC”。
—-
请用中文将以下内容重新表达,只需要一个选项:
—
原本 React 只支持前端渲染(CSR),如先前所述。
然而,在CSR的情况下,由于需要向客户端发送所有组件的资源(JavaScript),因此担心客户端的性能会下降。
因此,在那里诞生了RSC(React Server Components)。
RSC 是一种将组件分为“服务器端渲染组件”和“客户端端渲染组件”的技术。
在 React 中,迄今为止只有“客户端组件”。然而,在 RSC 中,您可以选择将哪个组件专用于服务器,哪个组件专用于客户端。
据RSC称,通过将数据获取操作更接近于数据库的服务器端进行执行,可以减少发送给客户端的JavaScript组件和依赖包的大小(bundle大小),从而提高性能。
此外,RSC具有以下特点。
-
- サーバー側からより高速にデータ取得が可能になる(クライアントからのリクエスト量も減る)
-
- console.log はブラウザのコンソールではなく、サーバーのコンソールに情報を出力する
-
- onClick や onChange などのイベントリスナーは使用できない
-
- 状態管理(useState)と効果管理(useEffect)は使用できない
- サーバーコンポーネントはクライアントコンポーネントをインポートしてレンダリングできるが、クライアントコンポーネントはその中のサーバーコンポーネントをレンダリングできない
请注意,尽管使用RSC可以减小bundle大小,但并不是无条件地可以实现。请参考以下详细内容。
App Router 是什么?
App Router是Next.js 13中新增的一种新的路由器实现。
在 App Router 中,默认会应用 RSC(React 服务器组件)。也就是说,您创建的组件会默认在服务器端执行。
为了在客户端运行,请在组件的顶部定义”use client”。
尽可能将处理放在服务器端,以提高性能的意图可见。
基本上,可以说是将其作为服务器组件实现,并仅在必要的部分在客户端执行的基本思路。
关于SSR和App Router(RSC)的区别。
接下来,我们将通过插入图表来解释许多初学者常常混淆的“SSR和RSC的区别”。
SSR 将被渲染如下。
-
- 在服务器端进行整个渲染,并生成原始的HTML。
-
- 将生成的HTML映射到DOM上,在客户端进行显示(以加快初始载入速度)。
- 将打包好的JavaScript(组件)发送到客户端并进行灌水操作。
通过在服务器端生成HTML并将其反映到客户端上,以加快初始显示速度,这是其最显著的特点。
在RSC中,一方面会以以下方式进行渲染。
-
- 在服务器端渲染服务器组件。
-
- 将服务器组件的HTML和客户端组件的JavaScript发送到客户端。
-
- 渲染客户端组件。
- 将生成的HTML反映在DOM中,以在客户端显示。
以下是三个主要的区别。
-
- SSR の場合は初期表示が速い
-
- RSC の場合はサーバーとクライアントでそれぞれのコンポーネントがレンダリングされる
- SSR の方がクライアントに送信される JavaScript の量が多い
这个 SSR 和 RSC 并不是互不相干的技术,而是可以结合使用的。(事实上,结合使用的情况更为有效的时候较多)
当将SSR和RSC结合在一起时,处理流程如下:
-
- 在服务器端渲染服务器组件,
-
- 在服务器端渲染客户端组件(具有SSR特有行为),
-
- 将生成的服务器组件和客户端组件的HTML发送给客户端,以在DOM中反映并在客户端上显示(加快初始显示),
- 将JavaScript(客户端组件)发送给客户端,并执行渲染和hydration。
通过将SSR和RSC组合在一起,可以加快初始显示的同时减少发送到客户端的JavaScript代码量。
因此,通常情况下,与其单独使用RSC,更常见的是将RSC与SSR组合使用。
关于React服务器组件和数据获取的处理方案。
到目前为止,在解释中并没有特别意识到”从外部获取数据”这一点。
然而,在实际应用中,通常需要从外部(如数据库和API)获取数据并使用。
接下来,我将解释在这种情况下会发生什么。
以下用中文给出一种方式的改述:
—
最初から、客户端可以使用useState等方式来表示加载中的状态,这样可以实现组件级别的异步数据获取。
然而,在进行服务器渲染时,使用useState等无法在服务器端以组件为单位实现异步数据获取的问题。
使用 Next.js 的 getServerSideProps 能够成功获取数据,但是数据的获取方式存在“同步”的问题。
在这种情况下,可能会出现在向用户显示页面之前必须完成服务器上的所有数据获取,导致用户界面的初始显示延迟。
然而,随着悬疑和React服务器组件的出现,这种情况将会改变。
悬念是一种功能,它可以以声明式的方式表示“正在加载中”,而不依赖于useState等功能。
例如,准备以下组件。
import { Suspense } from "react";
import Loading from "./components/loading";
import { ServerComponent } from "./components/ServerComponent";
export default async function Home() {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
marginTop: "200px",
}}
>
<Suspense fallback={<Loading />}>
<ServerComponent />
</Suspense>
</div>
);
}
将此显示在屏幕上,结果如下所示。
你会发现它可以在不使用 useState 的情况下表达“加载中”。
此外,在React Server Components中,您可以使用Async/await(异步函数)。
通过使用具有这些特性的Suspense和React Server Components,在服务器端使用SSR时(不使用getSSP),我们现在可以实现组件级别的异步数据获取。
通过这种方法,用户在完全获取数据之前就可以看到屏幕上的内容。它比传统的SSR能够加快显示速度。
如果在服务器端渲染中进行数据获取,目前的惯例是使用 RSC 在服务器组件内使用 Suspense 表达加载中,并进行数据获取。
实际上亲自实践一下 RSC 和 App Router
接下来,我打算在运行应用程序的同时,检查它的行为。
由于操作简单,所以请务必亲自动手尝试一下。
假设前提是Node.js、npm和TypeScript都已经处于可使用的状态。
首先,在所需目录下执行以下命令来安装 Next.js。
npx create-next-app@latest .
请使用App Router,而不是Pages Router,并回答任何问题都可以按Enter键确认。
首先,我们将创建简单的服务器组件和客户端组件。
请分别创建以下文件。
import { ClientComponent } from "./components/ClientComponent";
import { ServerComponent } from "./components/ServerComponent";
export default async function Home() {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
marginTop: "200px",
}}
>
<ClientComponent />
<ServerComponent />
</div>
);
}
export async function ServerComponent() {
const boxStyle = {
width: "400px",
height: "300px",
backgroundColor: "#006400",
display: "flex",
justifyContent: "center",
alignItems: "center",
};
const textStyle = { color: "white", footSize: "larger", fontWeight: "bold" };
console.log("Server Componentを実行しています");
return (
<div style={boxStyle}>
<p style={textStyle}>Server Component</p>
</div>
);
}
"use client";
export function ClientComponent() {
const boxStyle = {
width: "400px",
height: "300px",
backgroundColor: "#ffff00",
display: "flex",
justifyContent: "center",
alignItems: "center",
};
const textStyle = { footSize: "larger", fontWeight: "bold", color: "black" };
console.log("Client Componentを実行しています");
return (
<div style={boxStyle}>
<p style={textStyle}>Client Component</p>
</div>
);
}
在App Router中,组件默认是服务器组件。
要将其变为客户端组件,您需要在页面的开头写入”use client;”。
在这里,我们分别创建了服务器组件和客户端组件,并在根组件(page.tsx)中加载了两者。
如果能做到这一点,请使用npm run dev来运行应用程序。
当您访问 localhost:3000 时,将会显示以下屏幕。
首先打开开发者工具的控制台。
然后可以看到只有客户端组件在客户端上执行(渲染)。
另外,在查看Network选项卡时,page.js的大小为39.2kB。
接下来,将”use client;”语句添加到所有组件中,如下所示。
"use client";
import { ClientComponent } from "./components/ClientComponent";
import { ServerComponent } from "./components/ServerComponent";
export default async function Home() {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
marginTop: "200px",
}}
>
<ClientComponent />
<ServerComponent />
</div>
);
}
"use client";
export async function ServerComponent() {
const boxStyle = {
width: "400px",
height: "300px",
backgroundColor: "#006400",
display: "flex",
justifyContent: "center",
alignItems: "center",
};
const textStyle = { color: "white", footSize: "larger", fontWeight: "bold" };
console.log("Server Componentを実行しています");
return (
<div style={boxStyle}>
<p style={textStyle}>Server Component</p>
</div>
);
}
"use client";
export function ClientComponent() {
const boxStyle = {
width: "400px",
height: "300px",
backgroundColor: "#ffff00",
display: "flex",
justifyContent: "center",
alignItems: "center",
};
const textStyle = { footSize: "larger", fontWeight: "bold", color: "black" };
console.log("Client Componentを実行しています");
return (
<div style={boxStyle}>
<p style={textStyle}>Client Component</p>
</div>
);
}
在这种情况下,再次显示屏幕。
然后,我们可以看到ServerComponent也在客户端上显示出来。
当查看网络选项卡时,可以看到 page.js 文件的大小为40.6KB。
我认为一旦将ServerComponent变为客户端组件,就可以发现其大小增加了。
这次反过来,让我们从所有的组件中删除use client,如下所示。
import { ClientComponent } from "./components/ClientComponent";
import { ServerComponent } from "./components/ServerComponent";
export default async function Home() {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
marginTop: "200px",
}}
>
<ClientComponent />
<ServerComponent />
</div>
);
}
export async function ServerComponent() {
const boxStyle = {
width: "400px",
height: "300px",
backgroundColor: "#006400",
display: "flex",
justifyContent: "center",
alignItems: "center",
};
const textStyle = { color: "white", footSize: "larger", fontWeight: "bold" };
console.log("Server Componentを実行しています");
return (
<div style={boxStyle}>
<p style={textStyle}>Server Component</p>
</div>
);
}
export function ClientComponent() {
const boxStyle = {
width: "400px",
height: "300px",
backgroundColor: "#ffff00",
display: "flex",
justifyContent: "center",
alignItems: "center",
};
const textStyle = { footSize: "larger", fontWeight: "bold", color: "black" };
console.log("Client Componentを実行しています");
return (
<div style={boxStyle}>
<p style={textStyle}>Client Component</p>
</div>
);
}
我去除了客户端,并将其改为服务器组件。
以这种状态再次显示屏幕。
控制台没有显示任何内容。
而且,当您查看网络选项卡时,您会发现 page.js 已经消失了。
這表示了所有元件的渲染都在伺服器端執行。
接下来,我们将使用Suspense在服务器组件中模拟异步数据获取。
请对每个项目进行以下更改。
import { Suspense } from "react";
import { ClientComponent } from "./components/ClientComponent";
import Loading from "./components/loading";
import { ServerComponent } from "./components/ServerComponent";
export default async function Home() {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
marginTop: "200px",
}}
>
<ClientComponent />
<Suspense fallback={<Loading />}>
<ServerComponent />
</Suspense>
</div>
);
}
const sleep = async (ms: number) => {
return new Promise((res) => setTimeout(res, ms));
};
export async function ServerComponent() {
console.log("ServerComponentを実行しています(sleepの前)");
// データの取得をイメージ
await sleep(3000);
const boxStyle = {
width: "400px",
height: "300px",
backgroundColor: "#006400",
display: "flex",
justifyContent: "center",
alignItems: "center",
};
const textStyle = { color: "white", footSize: "larger", fontWeight: "bold" };
console.log("Server Componentを実行しています(sleepの後)");
return (
<div style={boxStyle}>
<p style={textStyle}>Server Component</p>
</div>
);
}
"use client";
export function ClientComponent() {
const boxStyle = {
width: "400px",
height: "300px",
backgroundColor: "#ffff00",
display: "flex",
justifyContent: "center",
alignItems: "center",
};
const textStyle = { footSize: "larger", fontWeight: "bold", color: "black" };
console.log("Client Componentを実行しています");
return (
<div style={boxStyle}>
<p style={textStyle}>Client Component</p>
</div>
);
}
此外,还需要添加以下 loading.tsx。
export default function Loading() {
const boxStyle = {
width: "400px",
height: "300px",
backgroundColor: "#CACACA",
display: "flex",
justifyContent: "center",
alignItems: "center",
};
const textStyle = { color: "black", fontSize: "larger", fontWeight: "bold" };
return (
<div style={boxStyle}>
<p style={textStyle}>...Loading</p>
</div>
);
}
如果以这种状态显示屏幕,会出现以下的情况。
我认为您可以看到服务器在异步获取数据后进行了页面显示。
总结
最后我再总结一下内容。
-
- React は UI を簡単に構築するための JavaScript ライブラリ
-
- Next.js は React のフレームワーク
-
- React は以下の流れでレンダリング(CSR)を行う
レンダリングのトリガーを検知
ブラウザレンダリングする内容の決定
変更を DOM に適用
Next.js には以下の二つのモード(ルーティング方式)がある
Pages Router
App Router
Next.js の Pages Router では以下の 4 つのレンダリング方式を選択できる
SSR
SSG
ISR
CSR
React Server Components とは、コンポーネントを「サーバー側でレンダリングされるコンポーネント」と「クライアント側でレンダリングされるコンポーネント」に分ける技術
Next.js の App Router では、デフォルトで作成したコンポーネントがサーバーコンポーネントになる
クライアントコンポーネントにするにはuse clientを記述する必要がある
RSC と SSR を組み合わせることで、初期表示を早めつつ、クライアント側に送信する JavaScript の量を抑えることができる
Suspense は、useState に頼らずに「ローディング中」を表現できる機能
Suspense と React Server Components を使うことで、SSR を使用するサーバー側でもコンポーネント単位の非同期的なデータ取得が可能となる
总之
为了跟上前端技术的潮流,如React Server Components和App Router,我学习并在Qiita上总结了一下。
在编写示例代码的过程中,我能够加深对问题的理解,但仍然很难说我已经深入理解了。
我不仅仅是希望在个人实践中进行尝试,而且在工作实践中,如果有机会,我也希望积极尝试 React 服务器组件和应用程序路由。
希望这篇文章能对学习React Server Components和App Router有所帮助,请提供参考。
感谢您阅读至最后。
主要的参考资料
-
- 一言で理解するReact Server Components
-
- Next.js 公式ドキュメント
-
- What’s “Next” JS Meetup
-
- React Server Components の仕組み:詳細ガイド
-
- Nextjs で理解する React Server Components 徹底解説【React18】
-
- Understanding React Server Components
-
- Next.js から学ぶ Web レンダリング ~React 誕生以前から App Router with RSC までの流れ~
- Render and Commit