您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

React Server Component 从理念到原理

时间:2023-06-15 12:54:27  来源:微信公众号  作者:魔术师卡颂
SSG是后端「编译时方案」。使用SSG的业务,后端代码在编译时会生成html(通常会被上传CDN)。当前端发起请求后,后端(或CDN)始终会返回编译生成的HTML。

大家好,我卡颂。

React Server Component(后文简称RSC)是React近几年最重要的特性。虽然他对React未来发展至关重要,但由于:

  • 仍属实验特性。
  • 配置比较繁琐,且局限较多。

所以虽然体验Demo[1]已经发布3年了,但仍属于「知道的人多,用过的人少」。

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

  1. RSC是用来做啥的?
  2. RSC和其他服务端渲染方案(SSR、SSG)的区别
  3. 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}/>;
}

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

  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的区别

同样涉及到前端框架的后端运行,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规范」拆分组件后,能得到类似如下的组件树,其中RSCRCC可能交替出现:

图片

但是需要注意: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映射」的完整过程:

  1. 业务开发时通过.server | client后缀区分组件类型。
  2. 后端代码编译时,所有RCC(即.client后缀文件)会编译出独立文件(这一步是react-server-dom-webpack[3]插件做的,对于Vite,也有人提了Vite插件的实现 PR[4])。
  3. React后端返回给前端的RSC数据中包含了组件树(J标记)等按行表示的数据。
  4. React前端根据J标记对应数据渲染组件树,遇到「引用RCC」(形如M[id])时,根据id发起JSONP请求。
  5. 请求返回该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。

参考资料

[1]体验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



Tags:React   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
从0实现React18
要从零开始实现React 18,需要理解React的核心概念和一些主要特性。以下是一个简要的步骤:1. 了解React的基本概念: 组件: React应用的基本构建块。组件可以是函数组件(Functional...【详细内容】
2024-01-22  Search: React  点击:(47)  评论:(0)  加入收藏
React的核心概念
React是一个开源JavaScript库,用于构建用户界面。它由Facebook开发并维护,已成为构建Web和移动应用程序的流行选择。React的主要特点是组件化架构,它使开发人员能够将应用程序...【详细内容】
2024-01-09  Search: React  点击:(103)  评论:(0)  加入收藏
浅析五种 React 组件设计模式
作为一名 React 开发者,你可能会面临下面几个问题: 如何构建一个高复用度性的组件,使其适应不同的业务场景? 如何构建一个具有简单 API的组件,使其易于使用? 如何构建一个在 UI 和...【详细内容】
2024-01-09  Search: React  点击:(83)  评论:(0)  加入收藏
React与Vue性能对比:两大前端框架的性能
React和Vue是当今最流行的两个前端框架,它们在性能方面都有着出色的表现。React的加载速度:初次加载:由于React使用了虚拟DOM(Virtual DOM)技术,它可以通过比较虚拟DOM树与实际DOM...【详细内容】
2024-01-05  Search: React  点击:(107)  评论:(0)  加入收藏
Vanilla Design,新一代 React UI 库
这几天做需求,一堆 UI 库实在是不知道选哪个,各种角色的同事争论不休;还总有新轮子冒出来,所以我来插一脚,并借此来领悟写代码的哲学:The best way to write secure and reliable...【详细内容】
2024-01-04  Search: React  点击:(89)  评论:(0)  加入收藏
vue3中 ref和 reactive的区别 ?
最近有朋友在面试过程中经常被问到这么一个问题,vue3 中的ref 和 reactive的区别在哪里,为什么 要定义两个API 一个 api不能实现 响应式更新吗??带着这个疑问 ,我们 接下来进行逐...【详细内容】
2024-01-03  Search: React  点击:(38)  评论:(0)  加入收藏
React18 与 Vue3 全方面对比
1. 编程风格 & 视图风格1.1 编程风格 React 语法少、难度大;Vue 语法多,难度小例如指令:Vue<input v-model="username"/><ul> <li v-for="(item,index) in list" :key="inde...【详细内容】
2024-01-03  Search: React  点击:(72)  评论:(0)  加入收藏
使用React微前端的完整指南
译者 | 李睿审校 | 重楼事实表明,前端开发伴随着许多挑战。而寻找简化开发过程和加快任务执行的方法是每个开发团队的目标。在开发大型复杂产品时,让开发团队成员在任务上进行...【详细内容】
2023-12-26  Search: React  点击:(90)  评论:(0)  加入收藏
什么是React的错误边界(Error Boundary)?
React的错误边界(ErrorBoundary)是一种React组件,用于捕获并处理其子组件树中任何位置的JavaScript错误。它允许开发人员在应用程序中定义错误边界,以便在发生错误时显示备用UI...【详细内容】
2023-12-21  Search: React  点击:(125)  评论:(0)  加入收藏
如何设计更优雅的 React 组件?
在日常开发中,团队中每个人组织代码的方式不尽相同。下面我们就从代码结构的角度来看看如何组织一个更加优雅的 React 组件!1. 导入依赖项我们通常会在组件文件顶部导入组件所...【详细内容】
2023-12-21  Search: React  点击:(101)  评论:(0)  加入收藏
▌简易百科推荐
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(6)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(13)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(9)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(11)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(9)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
Kubernetes 究竟有没有 LTS?
从一个有趣的问题引出很多人都在关注的 Kubernetes LTS 的问题。有趣的问题2019 年,一个名为 apiserver LoopbackClient Server cert expired after 1 year[1] 的 issue 中提...【详细内容】
2024-03-15  云原生散修  微信公众号  Tags:Kubernetes   点击:(6)  评论:(0)  加入收藏
站内最新
站内热门
站内头条