React Server Component 从理念到原理
大厂技术 高级前端 精选文章
点击上方 全站前端精选,关注公众号
回复1,加入高级前段交流
React Server Component
(后文简称RSC
)是React
近几年最重要的特性。虽然他对React
未来发展至关重要,但由于:
-
仍属实验特性
-
配置比较繁琐,且局限较多
所以虽然体验Demo[1]已经发布3年了,但仍属于「知道的人多,用过的人少」。
本文会从以下几个角度介绍RSC
:
-
RSC
是用来做啥的? -
RSC
和其他服务端渲染方案(SSR
、SSG
)的区别 -
RSC
的工作原理
希望读者读完本文后对RSC
的应用场景有清晰的认识。
本文参考了how-react-server-components-work[2]
什么是RSC
对于一个React
组件,可能包含两种类型的状态:
-
前端交互用的状态,比如加载按钮的显/隐状态
-
后端请求回的数据,比如下面代码中的
data
状态用于保存后端数据:
function App() {
const [data, update] = useState(null);
useEffect(() => {
fetch(url).then(res => update(res.json()))
}, [])
return <Ctn data={data}/>;
}
「前端交互用的状态」放在前端很合适,但「后端请求回的数据」逻辑链路如果放在前端则比较繁琐,整个链路类似如下:
-
前端请求并加载
React
业务逻辑代码 -
应用执行渲染流程
-
App
组件mount
,执行useEffect
,请求后端数据 -
后端数据返回,
App
组件的子组件消费数据
如果我们根据「状态类型」将组件分类,比如:
-
「只包含交互相关状态」的组件,叫客户端组件(
React Client Component
,简写RCC
) -
「只从数据源获取数据」的组件,叫服务端组件(
React Server Component
,简写RSC
)
按照这种逻辑划分,上述代码中:
-
App
组件只包含数据,显然属于SSR
-
App
组件的子组件Ctn
消费data
,如果他内部包含交互逻辑,应该属于RCC
将上述代码改写为:
function App() {
// 从数据库获取数据
const data = getDataFromDB();
return <Ctn data={data}/>;
}
其中:
-
App
组件在后端运行,可以直接从数据源(这里是数据库)获取数据 -
Ctn
组件在前端运行,消费数据
改造后「前端交互用的状态」逻辑链路不变,而「后端请求回的数据」逻辑链路却变短很多:
-
后端从数据源获取数据,将
RSC
数据返回给前端 -
前端请求并加载业务逻辑代码(来自步骤0)
-
应用执行渲染流程(此时
App
组件已经包含数据) -
App
组件的子组件消费数据
这就是RSC
的理念,一句话概括就是 —— 根据状态类型,划分组件类型,RCC
在前端运行,RSC
在后端运行。
与SSR、SSG的区别
同样涉及到前端框架的后端运行,RSC
与SSR
、SSG
有什么区别呢?
首先,SSG
是后端「编译时方案」。使用SSG
的业务,后端代码在编译时会生成HTML
(通常会被上传CDN
)。当前端发起请求后,后端(或CDN
)始终会返回编译生成的HTML
。
RSC
与SSR
则都是后端「运行时方案」。也就是说,他们都是前端发起请求后,后端对请求的实时响应。根据请求参数不同,可以作出不同响应。
同为后端运行时方案,RSC
与SSR
的区别主要体现在输出产物:
-
类似于
SSG
,SSR
的输出产物是HTML
,浏览器可以直接解析 -
RSC
会流式输出一种「类JSON」的数据结构,由前端的React
相关插件解析
既然输出产物不同,那么他们的应用场景也是不同的。
比如,在需要考虑SEO
(即需要后端直接输出HTML
)时,SSR
与SSG
可以胜任(都是输出HTML
),而RSC
则不行(流式输出)。
同时,由于实现不同,同一个应用中可以同时存在SSG
、SSR
以及RSC
。
RSC的限制
「RSC规范」是如何区分RSC
与RCC
的呢?根据规范定义:
-
带有
.server.js(x)
后缀的文件导出的是RSC
-
带有
.client.js(x)
后缀的文件导出的是RCC
-
没有带
server
或client
后缀的文件导出的是通用组件
所以,我们上述例子可以导出为2个文件:
// app.server.jsx
function App() {
// 从数据库获取数据
const data = getDataFromDB();
return <Ctn data={data}/>;
}
// ctn.client.jsx
function Ctn({data}) {
// ...省略逻辑
}
对于任意应用,按照「RSC规范」拆分组件后,能得到类似如下的组件树,其中RSC
和RCC
可能交替出现:
但是需要注意:RCC
中是不允许import
RSC
的。也就是说,如下写法是不支持的:
// ClientCpn.client.jsx
import ServerCpn from './ServerCpn.server'
export default function ClientCpn() {
return (
<div>
<ServerCpn />
</div>
)
}
这是因为,如果一个组件是RCC
,他运行的环境就是前端,那么他的子孙组件的运行环境也是前端,但RSC
是需要在后端运行的。
那么上述RSC
和RCC
交替出现是如何实现的呢?
答案是:通过children
。
改写下ClientCpn.client.jsx
:
// ClientCpn.client.jsx
export default function ClientCpn({children}) {
return (
<div>{children}</div>
)
}
在OuterServerCpn.server.jsx
中引入ClientCpn
与ServerCpn
:
// OuterServerCpn.server.jsx
import ClientCpn from './ClientCpn.client'
import ServerCpn from './ServerCpn.server'
export default function OuterServerCpn() {
return (
<ClientCpn>
<ServerCpn />
</ClientCpn>
)
}
组件结构如下:
解释下这段代码,首先OuterServerCpn
是RSC
,则他运行的环境是后端。他引入的ServerCpn
组件运行环境也是后端。
ClientCpn
组件虽然运行环境在前端,但是等他运行时,他拿到的children props
是后端已经执行完逻辑(已经获得数据)的ServerCpn
组件。
RSC协议详解
我们可以将RSC
看作一种rpc
(Remote Procedure Call
,远程过程调用)协议的实现。数据传输的两端分别是「React后端运行时」与「React前端运行时」。
一款rpc
协议最基本的组成包括三部分:
-
数据的序列化与反序列化
-
id
映射 -
传输协议
以上面的OuterServerCpn.server.jsx
举例:
// OuterServerCpn.server.jsx
import ClientCpn from './ClientCpn.client'
import ServerCpn from './ServerCpn.server'
export default function OuterServerCpn() {
return (
<ClientCpn>
<ServerCpn />
</ClientCpn>
)
}
// ClientCpn.client.jsx
export default function({children}) {
return <div>{children}</div>;
}
// ServerCpn.server.jsx
export default function() {
return <div>服务端组件</div>;
}
这段组件代码转化为RSC
数据后如下(不用在意数据细节,后文会解释):
M1:{"id":"./src/ClientCpn.client.js","chunks":["client1"],"name":""}
J0:["$","div",null,{"className":"main","children":["$","@1",null,{"children":["$","div",null,{"children":"服务端组件"}]}]}]
接下来我们从上述三个角度分析这段数据结构的含义。
数据的序列化与反序列化
RSC
是一种「按行分隔」的数据结构(方便按行流式传输),每行的格式为:
[标记][id]: JSON数据
其中:
-
「标记」代表这行的数据类型,比如
J
代表「组件树」,M
代表「一个RCC的引用」,S
代表Suspense
-
id
代表这行数据对应的id
-
JSON
数据保存了这行具体的数据
RSC
的序列化与反序列化其实就是JSON
的序列化与反序列化。反序列化后的数据再根据「标记」不同做不同处理。
比如,对于上述代码中第二行数据:
J0:["$","div",null,{"className":"main","children":["$","@1",null,{"children":["$","div",null,{"children":"服务端组件"}]}]}]
可以理解为,这行数据描述了一棵组件树(标记J
),id
为0
,组件树对应数据为:
[
"$","div",null,{
"className":"main","children":[
"$","@1",null,{
"children":["$","div",null,{
"children":"服务端组件"}]
}
]
}
]
当前端反序列化这行数据后,会根据上述JSON
数据渲染组件树。
id映射
所谓「id映射」,是指 对于同一个数据,如何在rpc
协议传输的两端对应上?
在「RSC协议」的语境下,是指 对于同一个组件,经由RSC
在React
前后端运行时之间传递,是如何对应上的。
还是考虑上面的例子,回顾下第二行RSC
对应的数据:
[
"$","div",null,{
"className":"main","children":[
"$","@1",null,{
"children":["$","div",null,{
"children":"服务端组件"}]
}
]
}
]
这段数据结构有些类似JSX
的返回值,把他与组件层级放到一张图里对比下:
可以发现,这些信息已经足够前端渲染<OuterServerCpn/>
、<ServerCpn/>
组件了,但是<ClientCpn/>
对应的数据@1
是什么意思呢?
这需要结合第一行RSC
的数据来分析:
M1:{"id":"./src/ClientCpn.client.js","chunks":["client1"],"name":""}
M
标记代表这行数据是「一个RCC的引用」,id
为1,数据为:
{
"id":"./src/ClientCpn.client.js",
"chunks":["client1"],
"name":""
}
第二行中的@1
就是指「引用id为1的RCC」,根据第一行RSC
提供的信息,React
前端运行时知道id
为1的RCC
包含一个名为client1
的chunk
,路径为"./src/ClientCpn.client.js"
。
于是React
前端运行时会向这个路径发起JSONP
请求,请求回<ClientCpn/>
组件对应代码:
如果应用包裹了<Suspense/>
,那么请求过程中会显示fallback
效果。
可以看到,通过协议中的:
-
M[id]
,定义id
对应的「RCC数据」 -
@[id]
,引用id
对应的「RCC数据」
就能将同一个RCC
在React
前后端运行时对应上。
那么,为什么RCC
不像RSC
一样直接返回数据,而是返回引用id
呢?
主要是因为RCC
中可能包含前端交互逻辑,而有些逻辑是不能通过「RSC协议」序列化的(底层是JSON
序列化)。
比如下面的onClick props
是一个函数,函数是不能通过JSON
序列化的:
<button onClick={() => console.log('hello')}>你好</button>
这里我们再梳理下「RSC协议」中「id映射」的完整过程:
-
业务开发时通过
.server | client
后缀区分组件类型 -
后端代码编译时,所有
RCC
(即.client
后缀文件)会编译出独立文件(这一步是react-server-dom-webpack[3]插件做的,对于Vite
,也有人提了Vite插件的实现 PR[4]) -
React
后端返回给前端的RSC
数据中包含了组件树(J标记
)等按行表示的数据 -
React
前端根据J标记
对应数据渲染组件树,遇到「引用RCC」(形如M[id]
)时,根据id
发起JSONP
请求 -
请求返回该
RCC
对应组件代码,请求过程的pending
状态由<Suspense/>
展示
传输协议
RSC
数据是以什么格式在前后端间传递呢?
不同于一些rpc
协议会基于TCP
或UDP
实现,「RSC协议」直接基于「HTTP协议」实现,其Content-Type
为text/x-component
。
总结
本文从理念、原理角度讲解了RSC
,过程中回答了几个问题。
Q
:RSC
和其他服务端渲染方案有什么区别?
A
:RSC
是服务端运行时的方案,采用流式传输。
Q
:为什么需要区分RSC
与RCC
(通过文件后缀)?
A
:因为RSC
需要在后端获取数据后流式传输给前端,而RCC
在后端编译时编译成独立文件,前端渲染时再以JSONP
的形式请求该文件
Q
:为什么RCC
中不能import RSC
?
A
:因为他们的运行环境不同(前者在前端,后者在后端)
由于配置繁琐,并不推荐在现有React
项目中使用RSC
。想体验RSC
的同学,可以使用Next.js
并开启App Router
:
在这种情况下,组件默认为RSC
。
参考资料
体验Demo: https://github.com/reactjs/server-components-demo
[2]how-react-server-components-work: https://www.plasmic.app/blog/how-react-server-components-work
[3]react-server-dom-webpack: https://www.npmjs.com/package/react-server-dom-webpack
[4]Vite插件的实现 PR: https://github.com/facebook/react/pull/26926
前端 社群
下方加 Nealyang 好友回复「 加群」即可。
如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:
1. 点个「在看」,让更多人也能看到这篇文章