乐趣区

关于前端:React-Server-Component-从理念到原理

大家好,我卡颂。

React Server Component(后文简称 RSC)是React 近几年最重要的个性。尽管他对 React 将来倒退至关重要,但因为:

  • 仍属试验个性
  • 配置比拟繁琐,且局限较多

所以尽管体验 Demo 曾经公布 3 年了,但仍属于 晓得的人多,用过的人少

本文会从以下几个角度介绍RSC

  1. RSC是用来做啥的?
  2. RSC和其余服务端渲染计划(SSRSSG)的区别
  3. 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}/>;
}

前端交互用的状态 放在前端很适合,但 后端申请回的数据 逻辑链路如果放在前端则比拟繁琐,整个链路相似如下:

  1. 前端申请并加载 React 业务逻辑代码
  2. 利用执行渲染流程
  3. App组件mount,执行useEffect,申请后端数据
  4. 后端数据返回,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组件在前端运行,生产数据

革新后 前端交互用的状态 逻辑链路不变,而 后端申请回的数据 逻辑链路却变短很多:

  1. 后端从数据源获取数据,将 RSC 数据返回给前端
  2. 前端申请并加载业务逻辑代码(来自步骤 0)
  3. 利用执行渲染流程(此时 App 组件曾经蕴含数据)
  4. App组件的子组件生产数据

这就是 RSC 的理念,一句话概括就是 —— 依据状态类型,划分组件类型,RCC在前端运行,RSC在后端运行。

与 SSR、SSG 的区别

同样波及到前端框架的后端运行,RSCSSRSSG 有什么区别呢?

首先,SSG是后端 编译时计划 。应用SSG 的业务,后端代码在编译时会生成HTML(通常会被上传CDN)。以后端发动申请后,后端(或CDN)始终会返回编译生成的HTML

RSCSSR 则都是后端 运行时计划。也就是说,他们都是前端发动申请后,后端对申请的实时响应。依据申请参数不同,能够作出不同响应。

同为后端运行时计划,RSCSSR 的区别次要体现在输入产物:

  • 相似于 SSGSSR 的输入产物是HTML,浏览器能够间接解析
  • RSC会流式输入一种 类 JSON的数据结构,由前端的 React 相干插件解析

既然输入产物不同,那么他们的利用场景也是不同的。

比方,在须要思考 SEO(即须要后端间接输入HTML)时,SSRSSG能够胜任(都是输入 HTML),而RSC 则不行(流式输入)。

同时,因为实现不同,同一个利用中能够同时存在 SSGSSR 以及RSC

RSC 的限度

RSC 标准 是如何辨别 RSCRCC的呢?依据标准定义:

  • 带有 .server.js(x) 后缀的文件导出的是RSC
  • 带有 .client.js(x) 后缀的文件导出的是RCC
  • 没有带 serverclient后缀的文件导出的是通用组件

所以,咱们上述例子能够导出为 2 个文件:

// app.server.jsx
function App() {
  // 从数据库获取数据
  const data = getDataFromDB();
  return <Ctn data={data}/>;
}

// ctn.client.jsx
function Ctn({data}) {// ... 省略逻辑}

对于任意利用,依照 RSC 标准 拆分组件后,能失去相似如下的组件树,其中 RSCRCC可能交替呈现:

然而须要留神:RCC中是不容许 import RSC 的。也就是说,如下写法是不反对的:

// ClientCpn.client.jsx

import ServerCpn from './ServerCpn.server'
export default function ClientCpn() {
  return (
    <div>
      <ServerCpn />
    </div>
  )
}

这是因为,如果一个组件是 RCC,他运行的环境就是前端,那么他的子孙组件的运行环境也是前端,但RSC 是须要在后端运行的。

那么上述 RSCRCC交替呈现是如何实现的呢?

答案是:通过children

改写下ClientCpn.client.jsx

// ClientCpn.client.jsx

export default function ClientCpn({children}) {
  return (<div>{children}</div>
  )
}

OuterServerCpn.server.jsx 中引入 ClientCpnServerCpn

// OuterServerCpn.server.jsx
import ClientCpn from './ClientCpn.client'
import ServerCpn from './ServerCpn.server'
export default function OuterServerCpn() {
  return (
    <ClientCpn>
      <ServerCpn />
    </ClientCpn>
  )
}

组件构造如下:

解释下这段代码,首先 OuterServerCpnRSC,则他运行的环境是后端。他引入的 ServerCpn 组件运行环境也是后端。

ClientCpn组件尽管运行环境在前端,然而等他运行时,他拿到的 children props 是后端曾经执行完逻辑(曾经取得数据)的 ServerCpn 组件。

RSC 协定详解

咱们能够将 RSC 看作一种 rpcRemote 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),id0,组件树对应数据为:

[
  "$","div",null,{
    "className":"main","children":[
      "$","@1",null,{
        "children":["$","div",null,{"children":"服务端组件"}]
        }
      ]
    }
]

以后端反序列化这行数据后,会根据上述 JSON 数据渲染组件树。

id 映射

所谓 id 映射,是指 对于同一个数据,如何在rpc 协定传输的两端对应上?

RSC 协定 的语境下,是指 对于同一个组件,经由 RSCReact前后端运行时之间传递,是如何对应上的。

还是思考下面的例子,回顾下第二行 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 蕴含一个名为 client1chunk,门路为"./src/ClientCpn.client.js"

于是 React 前端运行时会向这个门路发动 JSONP 申请,申请回 <ClientCpn/> 组件对应代码:

如果利用包裹了 <Suspense/>,那么申请过程中会显示fallback 成果。

能够看到,通过协定中的:

  • M[id],定义 id 对应的RCC 数据
  • @[id],援用 id 对应的RCC 数据

就能将同一个 RCCReact前后端运行时对应上。

那么,为什么 RCC 不像 RSC 一样间接返回数据,而是返回援用 id 呢?

次要是因为 RCC 中可能蕴含前端交互逻辑,而有些逻辑是不能通过 RSC 协定 序列化的(底层是 JSON 序列化)。

比方上面的 onClick props 是一个函数,函数是不能通过 JSON 序列化的:

<button onClick={() => console.log('hello')}> 你好 </button>

这里咱们再梳理下 RSC 协定id 映射 的残缺过程:

  1. 业务开发时通过 .server | client 后缀辨别组件类型
  2. 后端代码编译时,所有 RCC(即.client 后缀文件)会编译出独立文件(这一步是 react-server-dom-webpack 插件做的,对于Vite,也有人提了 Vite 插件的实现 PR)
  3. React后端返回给前端的 RSC 数据中蕴含了组件树( J 标记)等按行示意的数据
  4. React前端依据 J 标记 对应数据渲染组件树,遇到 援用 RCC(形如 M[id])时,依据id 发动 JSONP 申请
  5. 申请返回该 RCC 对应组件代码,申请过程的 pending 状态由 <Suspense/> 展现

传输协定

RSC数据是以什么格局在前后端间传递呢?

不同于一些 rpc 协定会基于 TCPUDP实现,RSC 协定 间接基于 HTTP 协定 实现,其 Content-Typetext/x-component

总结

本文从理念、原理角度解说了RSC,过程中答复了几个问题。

QRSC和其余服务端渲染计划有什么区别?

ARSC是服务端运行时的计划,采纳流式传输。

Q:为什么须要辨别 RSCRCC(通过文件后缀)?

A:因为 RSC 须要在后端获取数据后流式传输给前端,而 RCC 在后端编译时编译成独立文件,前端渲染时再以 JSONP 的模式申请该文件

Q:为什么 RCC 中不能import RSC

A:因为他们的运行环境不同(前者在前端,后者在后端)

因为配置繁琐,并不举荐在现有 React 我的项目中应用 RSC。想体验RSC 的同学,能够应用 Next.js 并开启App Router

在这种状况下,组件默认为RSC

退出移动版