大家好,我卡颂。
React Server Component
(后文简称 RSC
)是React
近几年最重要的个性。尽管他对 React
将来倒退至关重要,但因为:
- 仍属试验个性
- 配置比拟繁琐,且局限较多
所以尽管体验 Demo 曾经公布 3 年了,但仍属于 晓得的人多,用过的人少。
本文会从以下几个角度介绍RSC
:
RSC
是用来做啥的?RSC
和其余服务端渲染计划(SSR
、SSG
)的区别RSC
的工作原理
心愿读者读完本文后对 RSC
的利用场景有清晰的意识。
本文参考了 how-react-server-components-work
欢送退出人类高质量前端交换群,带飞
什么是 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 插件做的,对于Vite
,也有人提了 Vite 插件的实现 PR) 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
。