前端开发框架和库
前端框架
React

如何看待 React Server Components?

关注者
870
被浏览
188,652

53 个回答

这个由 React 官方博客 发布于 今天(12-22)凌晨,他们是因为疫情都没法过圣诞了所以索性搞点有意思的?

这还仅仅是个研究性质的东西,官方文章甚至简洁到直接丢个 视频,当然 RFC 还算比较详细,时间充裕的可以完整看看,这里简单基于目前得到的信息总结下。

我们基于官方 演示 DEMO 部署了个 react-server-components.musicfe.dev ,可以对照着看。

解决什么问题

Dan 开门见山,丢出了我们业务开发中需要权衡三个点:体验(user experience)、可维护性(maintenance)、性能(performance),然后用一个例子来说明为什么这三个点很难权衡。

这是一个很常见的组件化组合,问题在于每个组件都需要不同的数据,但是就体验而言我们更希望这些组件的渲染尽量同时,而且如果关注性能的话,我们也会考虑并行的去 fetch 数据,于是我们通常会 fetch 逻辑放到顶层,然后通过 Props 或者 Context 传递下去。

这样会把可维护性变差,除了看起来恶心,每个组件从逻辑上就不那么解耦了,我们于是会考虑每个组件自己处理 fetch 逻辑。

这又会让体验变差,因为浏览器从服务端 fetch 数据是比较贵的 IO,抽象一下就是下面这样:

我们之所以需要从服务端 fetch 数据,是因为我们把所有渲染操作放到了客户端,那如果我们把部分渲染逻辑放服务端呢?

于是就有了 React Server Components!

跟 SSR 什么关系

React Server Components 这名字可能让人第一个想到是不是出了个官方的 SSR 方案?

官方视频很少提到 SSR,是因为这个跟 SSR 就不是一个东西,理论上也可以混用。

如果你仔细看我们部署 DEMO 的页面内容,会发现其实跟 CSR 没区别:

再点击侧面 Notes 组件时,会发现有异步请求,但是返回的内容是这样的:

当然,对 SEO 也没帮助。

从这里也很容易知道,框架启动了后端服务,通过 /react 异步地输出页面内容,只是这个页面内容不是 HTML,是一个自定义的结构。之所以不是直接输出 HTML 猜测是因为需要做些高级的事,比如维持组件状态。

什么场景下使用

虽然 Dan 一开头就说了痛点,不过感觉还是不够痛,可能他只是为了引出话题。

如果我们现有 Client Components 组件树是这样的话:

那么使用 Server Components 后就是这样:

是的,我们可以随意的混合使用两种组件。看到这里,你可能想到了,这不就是 Suspense + Lazy 的变种么?

异步从 CDN 拉取组件后由客户端渲染变成了异步从服务端拉取基本渲染好的组件

那你可能会说,看起来也没啥好处嘛?考虑以下情况:

  • 异步组件需要依赖比较多组件,这个 chunk 就会比较大
  • 渲染比较耗 CPU,客户端可能会白屏或者掉帧

这些情况换成 Server Components 问题就会很容易解决了!

不过这里少不了多出的服务器成本,就开发过程而言,相信未来在工具链完善的情况下,跟目前使用 Client Components 类似。

展望

个人还是很期待的,甚至有个大胆的想法,未来会不会出现一个公共服务,专门提供服务端组件,这或许是前端引入 BaaS 服务的又一个很好的点。

编辑于 2020-12-22 18:12

研究了一下源码,这跟SSR完全不是一个东西,大概说一下整体流程:


  1. Server先启动一个express服务,同时执行webpack编译,把src目录的组件编译成静态资源,但是稍有不同的是,因为会用到react-server-dom-webpack这个库,所以只会编译属于Client的组件
  2. 打开Client(浏览器),输入Server提供的"Server组件"地址,回车
  3. Server接收到请求,去拿对应的Server组件,并将该Server组件和所有Client组件,使用react-server-dom-webpack这个库组合在一起序列化一个特殊的格式,并返回给Client
J是Server组件实体,就是在Server执行React.createElement(Server组件)的JSON序列化结果
M是Client组件引用路径,仅仅是引用信息
S是Suspense
E是Error

4. Client拿到返回的Response,也是用react-server-dom-webpack这个库,去解析这个特殊的格式,如果遇到J那就反序列化JSON得到一个真正的组件,如果遇到M那就在运行时执行__webpack_require__来引用静态资源。

这些操作的实际现象是:J的反序列化直接在内存里执行,没有任何网络请求;M的引用就相当于“webpack动态import”,因此体现为Network里的一个请求;

这也就是为什么,Network里只有Client组件,而没有Server组件的原因。

当然,这也是为什么Server组件有那么多限制的原因,因为涉及到JSON序列化,Server组件必须足够简单,才能在序列化反序列化的过程中不丢失信息。


5. 得到一个完整的组件,包含反序列化的Server组件和动态import进来的Server组件,在Client进行渲染


所以,跟SSR的区别是什么?

  1. SSR是在Server进行渲染,然后把渲染得到的HTML返回给Client;而这个Server Component,所有组件,不管Server组件还是Client组件,都是在Client进行渲染。只不过Server组件会自带一些在Server端获取的数据放在props里,这样就不用在Client再进行请求了。
  2. SSR因为每个请求都是一个新的HTML,就相当于两个应用,你的应用都变了,那你原来应用里的状态肯定都丢了;但是Server Component不管你向Server请求多少次,都是同一个HTML,同一个应用,你的状态不会丢。

如何结合使用?

你会发现,使用Server Component是完全没法SEO的,因为Server返回的不是HTML。其实官方也提过,我们可以结合起来用:

  1. 我们的首页请求Server的/路径,这个路径对应的是SSR渲染
  2. Client拿到SSR返回HTML后,后续的组件,请求Server的/component路径,这个路径对应的是Server Component


如何评价这个东西?

  1. 它必须要有Server支持,幻想着让一个纯CSR应用去接入一个Server组件是不现实的(云组件没那么容易)。
  2. 它应该是渐进式的,可以在一个纯CSR应用之上,很快的叠加上Server组件的功能。
  3. React越来越像Framework了。
编辑于 2020-12-23 12:18

除了 React Server Component,还出了个 HTML Over The Wire

都是服务端渲染 partial 页面更新技术。就不要刷什么 PHP 了,PHP 做不了这个。我来快速捋一下这项技术的发展脉络

chapter 1: innerHTML

domElement.innerHTML = '<div>hello</div>'

DOM 提供了渲染后更新的能力。甚至可以直接替换一部分 html。这是一切 partial update 的起点。在小程序上做就非常困难。

chapter 2: ajax + innerHTML

服务端通过 ajax 提供一个返回部分 html 的接口,然后用 innerHTML 做替换。

这样客户端就只需写非常少量的 html 替换代码,而复用绝大多数的服务端渲染逻辑。

这个做法非常流行于 ruby on rails 这样的服务端渲染的社区。

为什么到这里仍然还不完美

我之前拿这个模式写过一个电商服务。其缺点在于三点

  • 需要先用 id 等方式定位到要刷新的区块,然后再去请求刷新:这需要写业务的时候很清楚哪些区块“订阅”了哪些数据。如果相关数据修改了,要记得刷他们。
  • 用户体验不佳:在服务端返回,html 被替换之前是没法看到进展的。如果网络慢了,用户就可能会焦虑。如果立即出 loading,用户也焦虑。
  • 纯客户端的交互没法搞:这种替换 html 的做法适合表单提交这样的场景。但是客户端搞个 tree 组件,accordian 组件就没法这么弄了。

chapter 3.1 引用“组件”的 html

既然可以动态替换 html,例如 <div>hello</div>。

那么为什么不能把 <div> 换成 <UserNote> 这样的自定义组件呢?

这种做法就是 wordpress 等 rich text editor 的做法。可以服务端渲染,也可以是用户编辑的内容。然后到客户端再做二次渲染。

chapter 3.2 把 html 增强为组件

这个是 HTML Over The Wire 中 stimulus 做的事情。把客户端已有的 html,通过 css selector 就地重建 dom 元素和 js object 的映射关系。和 react 先有 js object,再渲染 dom 元素正好反过来。解决的就是服务端渲染出来的 html 是死的,缺乏客户端交互能力的问题。

chapter 4 React Server Component

回顾上面我说的三点问题,React Server Component 的解法

  • 需要先用 id 等方式定位到要刷新的区块,然后再去请求刷新:React Server Component 不需要你手工用 id 找到 DOM 元素,然后去刷。而是这么写 github.com/reactjs/serv 和重渲染客户端组件是一样的体验,仍然秉承了单向数据流的理念。而 turbo frame 还是按 id 索引要更新区块的做法。
  • 用户体验不佳:用 comet long polling 技术,让服务端分次推送给客户端内容。服务端推一点,客户端就立马渲染一点。和浏览器渲染html一样,这样就缓解了用户焦虑。顺被还可以让服务端推个 <suspense> 过来,让客户端出 loading indicator
  • 纯客户端的交互没法搞:服务端推过来的更新不仅仅引用 <div> 这样的原生组件,也可以引用 <UserNote> 这样的 react 客户端组件

state considered harmful

在这条主发展路径上,也分叉出去一些其他的尝试。他们的特点都是想要“保持”state,把 HTTP 从无状态搞成有状态的。

detour 1:client retain server state

asp.net / tapestry / seaside 为典型代表的技术方案。在服务端渲染,然后把状态通过 html 传递给客户端,下次表单提交的时候再带回来。从而把 server 端搞成似乎有状态的样子。这个方案导致了 http 提交的时候要夹带大量的服务端state回去,降低了客户体验。

detour 2:rehydration

SSR 的 rehydration 就是 asp.net 的方案的一半。就是只把服务端状态带到客户端,不需要客户端再把状态带回服务端了。

Dan 明确指出了这次的 React Server Component 的特点是 Server Component 仅仅运行在服务端。而 SSR 的“同一个”组件,既要考虑自己运行客户端,又要运行在服务端。这就给组件造成了很大的复杂度。

只有当“同一个”组件需要同时运行时在服务端,又要在客户端复活,才会有复杂的 state rehydration 问题。如果只是在服务端运行,state 用完了就扔了。而服务端即便在服务端渲染出了客户端组件,也只是给了客户端组件的 props,这个时候客户端组件仍然未开始执行,自然没有 state 需要从服务端搬迁到客户端去。

detour 3:stateful connection

用 websocket 连接来保持一个 stateful 的服务端状态。elixir 的 LiveView 就是这样的技术(对比: Hotwire by Basecamp)。Meteor / Blazor 也是这样的技术。

这种技术的缺点是连接断开了,状态也就丢失了,这个页面只能完全刷新才能恢复正常。除非服务端做非常复杂的连接保持技术(欢迎打脸,这块确实不确信是不是有黑魔法)。在移动端网络下应用 stateful connection 就要三思了。

如何看待

React Server Component 把服务端渲染 partial 页面更新的技术推向了一个新的高度。React 确实再不断推动前端技术的边界。

编辑于 2020-12-25 09:30

补充一点,React Server Components(RSC,代号 Flight)和 SSR Streaming + Selective Hydration(代号 Fizz)以及它们的 runtime(node/browser),是相互独立正交的关系。

也就是说,以下皆可以实现(2^3 种可能):

  • RSC,没有用到 SSR
  • RSC,只有老式 SSR(renderToString,< 18)
  • RSC,配合 SSR Streaming 使用(pipeToNodeWritable,>= 18)
  • 无 RSC,SSR Streaming,跑在 web runtime 里面
  • ……

其中 RSC 着重解决了 partial hydration 和 runtime size 的问题;Fizz 解决了 streaming 以及 hydration order;可切换 runtime 带来了 infra 层面的更多可能性。总的来说 RSC 的发布标志着 React 不再满足于其 library 的定位,而试图延展到 framework 甚至更多。

另外官方此前发布的 demo 仅仅只是一个例子,并不意味着最终它的形态。

发布于 2021-11-04 11:36

如果一个组件是重逻辑而轻渲染的,将代码中的逻辑部分和渲染部分做进一步精细的拆分,那么通过这种方式可以让客户端变成一个渲染器+事件触发器,逻辑都放在服务端,算完了返回结果再渲染。

这样就可以一定程度上解放客户端的计算压力(给手机省电),以及逻辑那部分的代码就不需要下载到客户端了(瘦身)。

以现在的互联网环境来看,说起纯逻辑的部分其实通常也不会太复杂,不那么常见到因为逻辑抢占过多JS执行时间而引起的页面卡顿这种问题。甚至有时候其实我们是巴不得把计算都放在客户端,解放服务端的压力的(省钱),那这种情况下Server Components岂不是无卵用?

首先是业务逻辑的代码量问题,因为大家都知道的原因业务逻辑的分支都是常年增长的,随着迭代进行代码量只增不减,体积越来越大,而且三天两头上线对于缓存来说基本上是个灾难(而渲染部分依托于主流的View库和UI组件库,其实在工程方面反而可以做到比较可观的缓存有效率)。如果能把这些体积较大缓存命中率又低的部分传输省了其实还挺有帮助的。

然后把想法放开一点,server不一定是web server,也可能是web worker或者service worker,也可能是Electron里的主进程。过去的几年里我们通过函数式渲染或者MVVM这类办法慢慢把逻辑和渲染的代码拆开了,而现在又把它们发生的场所也解绑了,那客户端确实有机会只作为一个渲染器+事件触发器来使用了。

在搞SSR的时候,我们可以实现动态切换SSR和CSR,在闲时开启SSR来提高首屏体验,在忙时切到CSR来降低服务器压力。那么以后我们可能也能在闲时使用SSR+Server Components来提高首屏体验,而在忙时切到CSR + Web Worker Components(我瞎编的名字)来降低服务器压力。

所以说Suspense跟Concurrent是一步大棋(尽管我是一个Vue粉),对现阶段来说它对开发者是很大的心智负担,但如果真的乖乖地去把它那些禁忌都避了,按照那套规则去玩,也许就能面向未来编程了。当然这个过程我个人觉得来得不会太快,对于很多项目来说可以先问问项目能不能活到那时候。但对于技术的学习和调研来说很值得关注。

话说,这种模式做到极端大概就是M$的 Blazor Server ?

发布于 2020-12-25 16:27

2020 年底,React 公布了一个全新的特性:Server Components,当时它还处于调研和试验阶段,并没有正式发布,随着 React 18.0 版本的正式发布,Server Component 的脚步声也越来越近了,不出意外的话,应该会在今年的某个 React 18 的 minor 版本中正式发布。

Server Components 听起来好像并不那么激动人心,React 18 所发布的各种特性也似乎平平无奇,自从 Hooks 面世已经三年多过去了,React 似乎停滞了前进的脚步,只是在现有的基础上做些小修小补?

No。

Concurrent rendering(React 18 新带来的特性)是一种本质上的改变,它本身不像 Hooks 那样对开发体验有着近乎翻天覆地的变革,但是这种底层渲染能力/机制的调整,会带来非常非常多的可能性,例如: Suspense、OffScreen、Server Components

这些三种特性,目前都没有生产可用,但是等到未来他们正式发布并渐渐被大面积使用时,每一项特性都会带来非常显著的开发体验的提升。而如果让我从这些未来会出现的新特性中选一个最期待的,那毫不疑问会是 Server Component。

所以,Server Components 到底是什么?他会像当年的 Hooks 一样对整个 React 生态带来巨大的影响么?在我们回答这些问题之前,很有必要先解释一下 Server Components 是什么,又解决了什么问题。

注: 下文中的很多内容受 Dan 和 Lauren 的这份 演讲视频所启发,如果你想更深入的了解即将到来的 React Server Component,那么非常推荐这段视频 事实上,这篇文章并不是一份对 Server Components 的用法教学,也不会涵盖 Server Components 的每一处细节(甚至为了方便表述会有意地略过一些细节),因此,在读下文之前,最好是对 Server Components 已经有所了解

背景:前后端分离

“前后端分离”是当下主流的 web 研发模式,后端存储数据,并把对数据的操作(增删改查)封装成接口,通过后端服务提供给前端,前端应用发送请求(例如 http 请求或者 rpc 请求)去调用后端提供的接口,从而获取到数据或者是对数据进行修改。

这可能是十几年以来非常普遍的研发模式了,也因此,我们被区分成前端开发和后端开发,各自负责着“楚河汉界”的一侧。我们在各自那一侧都做了非常多的优化、创新、突破,在后端,我们有容器化、微服务、SSR,在前端,我们有 code spliting、前端路由、React Hooks。

但是对于 API 层,我们似乎这么多年以来都未曾有过关注,即便是有,也仅仅是停留于 API 传输性能(例如 grpc)、API 的存在形式(例如 Restful 和 GraphQL)、API 的工程化管理(例如 Postman)。

并非是想说 API 一个邪恶而糟糕的设计,但是自从 Restful 的概念被提出以来,已经 22 年过去了,我们是不是应该在现在重新思考一下:

  • 以网络请求作为前后端的分界是最优解吗?
  • 如果没有 API,我们该如何架构和开发 Web 应用?

症结所在

让我们再回到刚刚的那张图,考虑一下 API 在带来职责分工明晰之外,同时也带来了哪些问题。

请求瀑布流(Waterfall)

就像 Remix 首页上所展示的,基于 API 和嵌套路由的前端站点,在请求时会出现瀑布流的现象:

数据的之间可能是有前后的依赖关系,抑或是和组件强耦合在一起,需要等待组件的 bundle 加载完成之后才能发出请求,这些都导致了请求瀑布流现象的出现。

并发请求

后端希望实现小而美的接口,每个接口有独立的职责,例如:

  • getUser 获取用户信息
  • getSongs?page=12 获取歌曲列表
  • getNotifactions 获取通知列表
  • getFavoirateSongs 获取收藏的歌曲
  • getNewSongs 获取新发布的歌曲
  • getRecommendSong 获取今日推荐的歌曲及对应的文案
  • getSearchBarHotKeywords 获取热门的搜索词
  • getAdBanner 获取广告 banner 内容
  • getRecentSongs 获取最近听歌记录
  • getRecommendedPlayList 获取推荐的歌单列表
  • ……(实在太多了)

每一个接口,单独拿出来看都是合理的,但是放在一起,就会发现用户每次打开这样一个音乐 web app,都要发送至少十几个接口,对于一些稍微复杂一点的网页,首次加载就需要请求几十个接口也丝毫不奇怪。

每一个接口的请求,都会带来网络开销,甚至在有些环境下会有最大并发请求数量的限制(例如在支付宝客户端那的 rpc 请求),或许网络层的 automatic batching 可以解决这个问题,但是遗憾的是,在目前的技术体系内,这个问题并不好解决(这里没有写不能解决,是因为的确有一些可行的方案,例如 BFF、依赖网关来做接口聚合,但它们都引入的新的问题)。

前端包体积(Bundle size)

包体积已经是“现代”前端开发领域饱受诟病的一点了,动辄几百 k 的 js 文件,似乎已经背离了浏览器是用来“浏览”网页的初衷了。并不是说我们都要做一个浏览器原教旨主义者,但是如果网页能够在不损失用户体验和开发体验的前提下,恢复到非常轻量和快速的状态,难道不是一件好事么?

协作成本(沟通、逻辑感知和封闭)

在我个人看来,这是大型项目或需要长期维护的应用中最令人头疼的问题了。

假设我们现在有一个非常巨大的应用,需要有十几位开发者共同编写和维护,那如何分工?答案必然是先做模块化,我们把整个应用拆分成几个彼此尽量独立的模块,再由每个人或每几个人负责其中的一个模块。模块化带来的好处是边界清晰(看到一个需求就能判断出来涉及到哪个或哪些模块做哪些改动)、职责明确(每个人都有自己确定的职责)、减少沟通成本(由于模块内部的逻辑是封闭的,不需要外部感知,所以可以降低沟通成本)。

对于前两点,目前的前后端分离架构都还是及格的,但对于第三点,我觉得基于网络请求接口的协作模式,在很多情况下并没有有效地做到逻辑内部封闭、减少需要前后端之间来回沟通的信息量。

举个例子,对于这样的一个页面:

看起来非常简单,一些信息的展示,加上一个充值按钮,这就是我最开始所设想的。

然而,随着这个项目不断的推进,我发现,原本以为是纯静态的标题文案,实际上是需要后端控制的,根据当前用户的所属人群来动态判断文案内容;我发现,由于前端金额计算的可靠性问题,折扣和实际支付相关的内容都是需要在后端预处理之后展示在前端的;我发现,倒计时的参考时间是需要依靠后端返回的;我发现,按钮的文案、点击行为,是需要后端控制的,特别是按钮的点击行为,最终方案是后端返回一个枚举,前端根据这个值来 switch case 一下走不同的逻辑(例如下单、引导先进行注册和绑卡)……

为了阅读体验,我只是列举了其中随手想到的一小部分,如果总结一下,那就是,后端和前端并没有因为“前后端分离”而做到解藕,反倒是藕断丝连,剪不断理还乱。后端感知了过多的前端视图层逻辑,就像是发明了一套 DSL(Domain Specific Language),而前端则是要写一个针对这套 DSL 的解析器和渲染器。

回到我们刚刚提到的,模块化带来的好处。模块化能够降低沟通成本,有一个不可忽略前提,就是架构的合理性。模块化并非是降低沟通成本的本质原因,也并非所有的模块化实践都能带来沟通成本的降低。当前后端分离的实践成为一个僵硬的、死板的“规范”,那它还能真正起到多少降低沟通成本的作用?一个大大的问号。

Server Components

再次申明一下,下文是假设读者朋友已经对 Server Components 有所了解

基于网络请求的 API 模型,有一个大大的前提假设,就是前端应用和后端应用是两个独立的应用,但是为什么一定要是这样?

或许我们可以让后端应用直接渲染 HTML,用户操作时,重新渲染一遍页面?这其实就是在 Restful 时代之前的架构,有很多弊端,特别是可交互性差,不然也就不会出现后来 Restful 的盛行了。

那再或许,我们可以让前端的 React 组件,运行在后端?

这就是 React Server Components。

一图胜千言,在现在的前后端分离模式下,后端提供接口,前端的 React 组件调用接口。

而如果后端可以运行 React 组件,直接渲染 React 节点树到前端,就不需要所谓的 API 的概念了。

后端运行 React 组件并不是什么新鲜事,我们在 SSR(Server Side Rending)早就习以为常了,但是需要特别注明的一点是,在 SSR 中,后端是运行了 React 组件,生成了一份初始状态的 html,但这份 html 是没有可交互性的,它只是为了让用户能尽早看到页面而做的一种改良式的、修修补补一样的优化。

而 Server Components 所带来的,是我们可以把同一个项目中,一部分的组件作为 Server Components,另一部分组件,作为 Client Components,因此我们可以既享受到后端内部调用带来的便捷、可维护性,又能保证页面的可交互性几乎没有任何妥协。

如果你用过 PHP 或 Django,那你肯定非常熟悉这种模式:后端直接渲染 html 内容,浏览器只负责显示,用户点击按钮,那就重新请求、重新渲染页面,如果页面上需要一些复杂的动态交互,比如让用户可以把一个列表展开/收起,或者是点击某个按钮之后展示一个模态框,那可以借助于 jQuery 来实现。

PHP + bootstrap + jQuery,现在,Server Components 就像是这套范式的升级版,可以被称为一种全新的“全栈”开发模式。

因为是在后端环境下,这些 Server Components 可以使用全部的后端能力,不管是中间件,还是其他后端微服务的调用,甚至是 db 的访问(当然可以直接跑 SQL,但是更好的实践是通过一个数据中间层),都可以实现。这样一来,我们就可以直接把数据从源头获取,放到 React 组件的上下文中,那自然就不需要传统意义上的 API 了。

更准确的说,API 并未消失,我们其实也不会和 API 就此说再见,而是让它换了一种形式。有模块化的地方,就会有 API,Restful 的 http 网络请求固然是 API,但中间件暴露出来的方法,浏览器提供的 Date 对象,node 提供的文件读取函数,db 提供的 SQL,这些全都是 API。在这种新架构下,API 变成了后端里业务应用和上游服务之间的调用,变成了 Server Components 和 Client Components 之间的 props 传递,前者让 API 变得更加干净、更符合单一职责的原则,而后者让 API 变得自然到你几乎感知不到。

所以:

  • Server Components 允许我们不再按照 前端 - 后端 进行模块的拆分,而是依照 业务应用 - 底层服务 来进行更合理的模块拆分。从而可以理论上降低模块之间的沟通成本(因为目前还没有办法实践证明)。
  • 由于 Server Components 是在后端运行组件,直接通过网络传输给前端进行渲染,因此很多大体积的包(例如 markdown 渲染、html sanitize)都不需要在前端下载和运行,从而很大程度上降低包体积。
  • 由于底层 db 或上游服务的调用都是发生在后端内部的,因此即便出现并发请求,所带来开销也远远小于前端并发调用后端的 Restful API。
  • 同理,请求瀑布流的问题也会因为调用开销降低而消失或减轻。

想象

如果大胆想象一下的话,未来的研发模式可能这样的:

开发者将不会再区分前端和后端,而是区分为业务应用开发和上游服务开发。现在的后端开发将(真正地)不再需要关注视图逻辑,只聚焦于底层业务逻辑,为前端提供清晰好用、原子化的服务/接口;而现在的前端开发将会拓展到横跨前端和后端(代码运行环境上),负责的是在后端封装好的一个个原子化的底层能力上,构建视图层,而我们也需要一套全新的框架和基础设施,来适配 Server Components。

目前,Server Components 还没有正式发布,而即便正式发布之后,也还有长长的工程化落地的路要走,Server Components 增加了很多额外的限制,server、client、shared 的区分也可能会带来一些理解成本。缓存、性能、server 重新渲染时的增量更新策略、发布时的可灰度性和可回滚性、业务中边界情况的处理,还有很多的问题需要去解决,还有很多的未知尚未被验证。

编辑于 2022-05-07 10:34

昨天看了 demo,今天翻了翻源码。我们应该从几个维度来“看待” React Server Components:

  • 当前开发遇到了哪些痛点?
  • 这些痛点目前有哪几种解决方案?
  • Server Components 是怎么解决这些痛点的?
  • Server Components 是否比这些解决方案更优秀?
  • Server Components 是某一个或某几个解决方案的升级、颠覆还是互补?

而最常拿来和 Server Components 对比的就是传统的 PHP/ASP 技术和为框架而生的 SSR 技术。React Server Components 这个技术,听起来和 SSR 很像,而代码看起来则和 PHP 很像。很多人认为这是一种倒退,“前端好不容易爬到了山顶,却发现 PHP 已经等待多时”。其实恰恰相反,使用一种“和现有技术类似的”方式来解决某个开发痛点的做法,正是一种先进而优雅的方式。就好比 jsx 和 html 很相似,vue template 和 mustache 很相似,极低的减轻了开发者的学习成本。

虽然表面相似,但是却完全不同。

在 PHP/ASP 时代,页面都是由服务器来渲染。服务器接到请求后,查询数据库然后把数据“塞”到页面里面,最后把生成好的 html 发送给客户端。当用户点击链接后,继续重复上面的步骤。这样用户体验不是很好,每个操作几乎都要刷新页面,服务器处理完之后再返回新的页面。

我们可以概括为:

  • 痛点:用户体验太差(user experience)
  • 原因:页面总是刷新
  • 解决思路:让页面别刷新
  • 方案:使用 ajax

而 Angular/Vue/React 这种单页应用(SPA)则主要是客户端渲染。服务器接到请求后,把 index.html 以及 js/css/img 等发送给浏览器,浏览器负责渲染整个页面。后续用户操作和前面的 php/jquery 一样,通过 ajax 和后端交互。但是和 php 相比,第一次访问时只返回了什么内容都没有的 idnex.html 空页面,没法做 SEO。另一点就是页面需要等到 js/css 和接口都返回之后才能显示出来,首次访问会有白屏。

概括一下:

  • 痛点:首次访问白屏
  • 原因:首次访问只返回了 index.html 页面
  • 解决思路:首次访问时返回渲染完的页面
  • 方案:SSR

SSR 的方式是:首次访问的时候由服务器(通常是 Node.js)来渲染页面,然后把已经渲染好的 html 发送给浏览器。后续的用户操作依然通过 ajax 获取数据,然后在浏览器端渲染组件和页面。

那么能不能让首屏的体验更好呢?

  • 痛点:SSR 首屏还是太慢
  • 原因:服务端渲染是请求的接口太多,导致响应时间太长
  • 解决思路:分块渲染,把已经渲染好的部分尽早的发送到浏览器
  • 方案:bigPipe

我们根据这种思路重新审视一下 React Server Components 解决了什么问题。

在视频的开篇,Dan 就举了一个“鱼与熊掌不可兼得”的例子:

  • Good:用户体验
  • Cheap:可维护性
  • Fast:性能

例子很简单,页面中的三个组件应该如何请求数据?

  • 顶层组件通过 1 个接口 fetch 所有数据,这样请求的时候会变长。用户体验✅ 可维护性❎ 性能❎
  • 顶层组件通过 3 个接口并行 fetch 所有数据:用户体验✅ 可维护性❎ 性能✅
  • 每个组件自行 fetch 数据:用户体验❎ 可维护性✅ 性能✅

面对种种痛点,我们也都有自己的方案。比如:

  • 我们可以通过增加服务器配置来解决,或者优化后端代码来解决。解决思路是让接口响应变快。
  • 使用 graphql 按需查询后端数据。
  • ……

React 团队分析了导致痛点的原因:组件需要反复从服务器请求数据。而 React 团队给出的解决方案是:把组件放到服务器端,这样客户端和服务器端只需要往返一次。和 graphql 的思路很像,但是更加贴近 react 生态,也更加 frontend style。

我截取了视频中的 30 秒,很好的说明了这种理念。

graphql 看似美好,但是落地困难。我使用 Gatsby 开发过几个项目,也做过 React Native 的 Facebook 登录,graphql 是真香。但是我觉得影响 graphql 落地的最大障碍就是 “明明是解决的前端痛点,却非要改造后端”。而把 graphql 做 BFF 来用坑也不少。

而 React Server Components 则完全是按 React 的思维来解决这个问题。甚至可以说是按前端组件化的思维来解决这个问题,这种思想 Vue 也可以实现。

前端发起请求,服务器端组件可以查询 db,可以访问接口 api,…… 而且和 SSR 不同,服务器响应的不是 html,而是一个序列化的“指令”。客户端根据此“指令”集来渲染组件和页面。

(PS:服务器直接返回 html 片段的技术也有了,叫 pjax/turbolinks。我去年和一个团队交流,说他们在做基于 React 组件的 pjax,后来放弃了)

M1:{"id":"./src/SearchField.client.js","chunks":["client5"],"name":""}
M2:{"id":"./src/EditButton.client.js","chunks":["client1"],"name":""}
S3:"react.suspense"
J0:["$","div",null,{"className":"main","children":[["$","section",null,{"className":"col sidebar","children":[["$","section",null,{"className":"sidebar-header","children":[["$","img",null,{"className":"logo","src":"logo.svg","width":"22px","height":"20px","alt":"","role":"presentation"}],["$","strong",null,{"children":"React Notes"}]]}],["$","section",null,{"className":"sidebar-menu","role":"menubar","children":[["$","@1",null,{}],["$","@2",null,{"noteId":null,"children":"New"}]]}],["$","nav",null,{"children":["$","$3",null,{"fallback":["$","div",null,{"children":["$","ul",null,{"className":"notes-list skeleton-container","children":[["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}]]}]}],"children":"@4"}]}]]}],["$","section","94",{"className":"col note-viewer","children":["$","$3",null,{"fallback":["$","div",null,{"className":"note skeleton-container","role":"progressbar","aria-busy":"true","children":[["$","div",null,{"className":"note-header","children":[["$","div",null,{"className":"note-title skeleton","style":{"height":"3rem","width":"65%","marginInline":"12px 1em"}}],["$","div",null,{"className":"skeleton skeleton--button","style":{"width":"8em","height":"2.5em"}}]]}],["$","div",null,{"className":"note-preview","children":[["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}]]}]]}],"children":"@5"}]}]]}]
M6:{"id":"./src/SidebarNote.client.js","chunks":["client6"],"name":""}
J4:["$","ul",null,{"className":"notes-list","children":[["$","li","94",{"children":["$","@6",null,{"id":94,"title":"Untitled feng","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"feng test"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Untitled feng"}],["$","small",null,{"children":"6:21 AM"}]]}]}]}],["$","li","93",{"children":["$","@6",null,{"id":93,"title":"Untitled","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"Null"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Untitled"}],["$","small",null,{"children":"6:06 AM"}]]}]}]}],["$","li","92",{"children":["$","@6",null,{"id":92,"title":"TEST","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"DDD"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"TEST"}],["$","small",null,{"children":"6:06 AM"}]]}]}]}],["$","li","90",{"children":["$","@6",null,{"id":90,"title":"测试测试","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"测试测试测试测试测试测试测试测试测试测试测试测试测试测试"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"测试测试"}],["$","small",null,{"children":"5:49 AM"}]]}]}]}],["$","li","89",{"children":["$","@6",null,{"id":89,"title":"喵喵喵🐱","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":["$","i",null,{"children":"(No content)"}]}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"喵喵喵🐱"}],["$","small",null,{"children":"5:43 AM"}]]}]}]}],["$","li","88",{"children":["$","@6",null,{"id":88,"title":"Untitled","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"????"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Untitled"}],["$","small",null,{"children":"5:32 AM"}]]}]}]}],["$","li","87",{"children":["$","@6",null,{"id":87,"title":"test03","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"hahah hahah ahahha"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"test03"}],["$","small",null,{"children":"5:32 AM"}]]}]}]}],["$","li","86",{"children":["$","@6",null,{"id":86,"title":"求不要更新了, 老子学不动了","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":["$","i",null,{"children":"(No content)"}]}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"求不要更新了, 老子学不动了"}],["$","small",null,{"children":"5:30 AM"}]]}]}]}],["$","li","84",{"children":["$","@6",null,{"id":84,"title":"Untitled","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"</math><img src onerror=alert(1)> 1 2 3 4 5 <form> <math><mtext> </form><form> <mglyph> <style></math><img src onerror=alert(1)>"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Untitled"}],["$","small",null,{"children":"3:24 AM"}]]}]}]}],["$","li","83",{"children":["$","@6",null,{"id":83,"title":"Hi,I am Lilei","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"console.log(process)"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Hi,I am Lilei"}],["$","small",null,{"children":"5:07 AM"}]]}]}]}],["$","li","81",{"children":["$","@6",null,{"id":81,"title":"到此一游","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"123123123123"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"到此一游"}],["$","small",null,{"children":"3:23 AM"}]]}]}]}],["$","li","80",{"children":["$","@6",null,{"id":80,"title":"Untitled","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"asdasdad"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Untitled"}],["$","small",null,{"children":"2:32 AM"}]]}]}]}],["$","li","78",{"children":["$","@6",null,{"id":78,"title":"alert(123)","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"alert(123)"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"alert(123)"}],["$","small",null,{"children":"2:30 AM"}]]}]}]}],["$","li","77",{"children":["$","@6",null,{"id":77,"title":"; DROP TABLE *; --","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"; DROP TABLE *; --"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"; DROP TABLE *; --"}],["$","small",null,{"children":"2:30 AM"}]]}]}]}],["$","li","76",{"children":["$","@6",null,{"id":76,"title":"Untitled","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"STRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS TESTSTRESS..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Untitled"}],["$","small",null,{"children":"2:23 AM"}]]}]}]}],["$","li","75",{"children":["$","@6",null,{"id":75,"title":"🐂🍺","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"🐂🍺"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"🐂🍺"}],["$","small",null,{"children":"2:15 AM"}]]}]}]}],["$","li","74",{"children":["$","@6",null,{"id":74,"title":"1111","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"1111"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"1111"}],["$","small",null,{"children":"2:07 AM"}]]}]}]}],["$","li","73",{"children":["$","@6",null,{"id":73,"title":"我真帅","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":["$","i",null,{"children":"(No content)"}]}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"我真帅"}],["$","small",null,{"children":"1:59 AM"}]]}]}]}],["$","li","72",{"children":["$","@6",null,{"id":72,"title":"😲这还可以当讨论区吗","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"太顶了?? 可以的吧 不可以"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"😲这还可以当讨论区吗"}],["$","small",null,{"children":"5:46 AM"}]]}]}]}],["$","li","71",{"children":["$","@6",null,{"id":71,"title":"Untitled111","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":["$","i",null,{"children":"(No content)"}]}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Untitled111"}],["$","small",null,{"children":"1:59 AM"}]]}]}]}],["$","li","70",{"children":["$","@6",null,{"id":70,"title":"111","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"1111111"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"111"}],["$","small",null,{"children":"1:49 AM"}]]}]}]}],["$","li","69",{"children":["$","@6",null,{"id":69,"title":"Cheap, Good, Fast~ It's the best","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"yes"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Cheap, Good, Fast~ It's the best"}],["$","small",null,{"children":"2:07 AM"}]]}]}]}],["$","li","68",{"children":["$","@6",null,{"id":68,"title":"There is no silver bullet","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"没有银弹"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"There is no silver bullet"}],["$","small",null,{"children":"4:22 AM"}]]}]}]}],["$","li","66",{"children":["$","@6",null,{"id":66,"title":"Dan Abramov is the 🐐gfasdfasdfasdf","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"👍asdfasdfasdfasdfasdfasdfasdf那段时低年级"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Dan Abramov is the 🐐gfasdfasdfasdf"}],["$","small",null,{"children":"1:58 AM"}]]}]}]}],["$","li","65",{"children":["$","@6",null,{"id":65,"title":"ss","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":["$","i",null,{"children":"(No content)"}]}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"ss"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","64",{"children":["$","@6",null,{"id":64,"title":"Untitledsx","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"这个是一个默认页面"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Untitledsx"}],["$","small",null,{"children":"12:09 midnight"}]]}]}]}],["$","li","62",{"children":["$","@6",null,{"id":62,"title":"This is","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"#Surprisingly Slow"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"This is"}],["$","small",null,{"children":"4:20 AM"}]]}]}]}],["$","li","61",{"children":["$","@6",null,{"id":61,"title":"Yekshimesh","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"my name a borat!"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Yekshimesh"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","60",{"children":["$","@6",null,{"id":60,"title":"Elon Musk","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"https://twitter.com/elonmusk is the best"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Elon Musk"}],["$","small",null,{"children":"2:00 AM"}]]}]}]}],["$","li","59",{"children":["$","@6",null,{"id":59,"title":":-)","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":":)"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":":-)"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","57",{"children":["$","@6",null,{"id":57,"title":"Readme","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"React Server Components Demo What is this? When will I be able to use this? Setup DB Setup Step 1...."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Readme"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","56",{"children":["$","@6",null,{"id":56,"title":"HGV great","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":["$","i",null,{"children":"(No content)"}]}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"HGV great"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","55",{"children":["$","@6",null,{"id":55,"title":"HGV just tesing!","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"Hello, world!"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"HGV just tesing!"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","54",{"children":["$","@6",null,{"id":54,"title":"Hello","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"Hello??"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Hello"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","53",{"children":["$","@6",null,{"id":53,"title":"holyshit find another job","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"holyshit find another job"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"holyshit find another job"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","52",{"children":["$","@6",null,{"id":52,"title":"狗子们又要学新东西啦","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"狗子们又要学新东西啦 Really interesting."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"狗子们又要学新东西啦"}],["$","small",null,{"children":"1:22 AM"}]]}]}]}],["$","li","51",{"children":["$","@6",null,{"id":51,"title":"å†…å·å·æ­»ä½ ","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"å†…å·å·æ­»ä½ "}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"å†…å·å·æ­»ä½ "}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","50",{"children":["$","@6",null,{"id":50,"title":"HI","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"测试测试知乎小弟留"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"HI"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","49",{"children":["$","@6",null,{"id":49,"title":"又有新概念啦","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"又有新概念啦"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"又有新概念啦"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","48",{"children":["$","@6",null,{"id":48,"title":"From Vietnam","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"theuranuskjkj #fdfdf dfdsf dfdsfds 地方就乐山大佛 对方水电费"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"From Vietnam"}],["$","small",null,{"children":"3:49 AM"}]]}]}]}],["$","li","47",{"children":["$","@6",null,{"id":47,"title":"; DROP TABLE *; --","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"'); DROP TABLE *; --"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"; DROP TABLE *; --"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","46",{"children":["$","@6",null,{"id":46,"title":"Hello React Server Compoenents","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":["$","i",null,{"children":"(No content)"}]}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Hello React Server Compoenents"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","44",{"children":["$","@6",null,{"id":44,"title":"Why there are no loading indicators?","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"If I click on some notes, it freezes for a bit and only then transitions to the needed page. But..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Why there are no loading indicators?"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","42",{"children":["$","@6",null,{"id":42,"title":"Untitledasd","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":["$","i",null,{"children":"(No content)"}]}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Untitledasd"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","41",{"children":["$","@6",null,{"id":41,"title":"Youtube","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"Oh no! What have I done? FR Rechercher Image d'avatar 55:56 / 57:19 Data Fetching with React Server Components 17..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Youtube"}],["$","small",null,{"children":"2:49 AM"}]]}]}]}],["$","li","39",{"children":["$","@6",null,{"id":39,"title":"Dupa","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"z trupa hmmm this is nice"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Dupa"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","38",{"children":["$","@6",null,{"id":38,"title":"Untitledd","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"dddd"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Untitledd"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","37",{"children":["$","@6",null,{"id":37,"title":"New Note","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"it's a bit slow?"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"New Note"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","35",{"children":["$","@6",null,{"id":35,"title":"Untitled","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"Hello"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Untitled"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","34",{"children":["$","@6",null,{"id":34,"title":"q121Untitled","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"sadasdasd"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"q121Untitled"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","2",{"children":["$","@6",null,{"id":2,"title":"test","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"India"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"test"}],["$","small",null,{"children":"12/22/20"}]]}]}]}],["$","li","1",{"children":["$","@6",null,{"id":1,"title":"测试下","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":["$","i",null,{"children":"(No content)"}]}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"测试下"}],["$","small",null,{"children":"12/22/20"}]]}]}]}]]}]
J5:["$","div",null,{"className":"note","children":[["$","div",null,{"className":"note-header","children":[["$","h1",null,{"className":"note-title","children":"Untitled feng"}],["$","div",null,{"className":"note-menu","role":"menubar","children":[["$","small",null,{"className":"note-updated-at","role":"status","children":["Last updated on ","23 Dec 2020 at 6:21 AM"]}],["$","@2",null,{"noteId":94,"children":"Edit"}]]}]]}],["$","div",null,{"className":"note-preview","children":["$","div",null,{"className":"text-with-markdown","dangerouslySetInnerHTML":{"__html":"<p>feng test</p>\n"}}]}]]}]

所以本质上还是客户端渲染。视频中小姐姐还演示了一个例子:点击左侧内容时组件使用到了过渡效果,就如同普通的 React 程序一样,完全不受服务器端组件的影响。

Server Components 的这种思路还有很大的挖掘空间。比如可以开发 WebSocket Components,通过 WebSocket 向浏览器主动的实时推送“指令”。再比如可以开发 Native Components,在 App 内嵌的 H5 页面中向原生代码发请求,原生代码完成业务处理后返回给 H5 序列化的“指令”。

这种序列化的指令还可以被存储,可以被回放,可以被 mock,调试起来应该可以像 redux 一样具有时间旅行功能。

总之,对 Server Components 还是挺期待的。

编辑于 2020-12-23 16:44

就像 dan 再视频中说的一样, cheap, good, fast , 你通常只能保证两样, server components 是让某些场景中 client 端渲染压力变轻, 更加cheap,把这部分计算任务放到服务端去。能够比较直接的想到的就是适合一些内容为主的网站。比如,论坛网站把代码高亮渲染放到服务端去,可以减小页面闪动,优化用户体验。

以下为抖机灵:

  • "迟钝"的人还在如何看待, 聪明的人已经新建了一个 vue-server-component 的仓库。
  • 这技术的名字还是太低调了, 包装一下 ,不如叫 Cloud Render 如何?
发布于 2020-12-22 14:35

我看评论里有不少人说没看懂跟 SSR 区别,但其实两者完全不是一回事,这这几点不同:

  1. Server Components 跟过去的 SSR 相比,你在拉取后不会丢失客户端的状态;
  2. SSR 输出的是 HTML,Server Components 输出的是 chunks。

Server Components 的思路很有意思,把前端往业务层又推进了一步。如果整个后端做数据仓储提供中台支持的话,那么前端开发者是否可尝试独立支撑业务开发?我觉得相比 GraphQL 提供了更为统一的解决方案 —— 组件即服务。

但这对现有的基建是个不小的挑战,在习惯了 S3 存储静态资源,Serverless 开发轻量前端接口的开发模式以后,如何让 Server Components 跑起来,是个不小的挑战,也需要社区的大力支持。

————————

抖机灵:大厂的朋友们,还在等什么,公共的 Server Components 服务可以搞起来了[Doge]

编辑于 2020-12-22 23:26

大家好,我卡颂。

最近Next.js v14发布,发布会的各种梗图刷爆了国外前端社区。

Next.js的诸多特性(比如Server ActionApp Router),都是在RSCReact Server Component)基础上衍生出的。

从名字可以看出,RSCReact的特性。那么,该怎么理解RSCNext.js的关系呢?

欢迎围观朋友圈、加入 人类高质量前端交流群,带飞

React团队的宿愿

对于前端框架的开发范式,有三个重要衡量因素:

  1. 用户体验
  2. 维护成本
  3. 性能

但是,通常很难做到三者兼顾(具体原因本文不细究,感兴趣的同学可以看 data-fetching-with-react-server-components。

简单来说,在前端开发中,「IO瓶颈」是影响内容渲染速度的重要因素(可以简单理解为,前端需要等待请求返回数据后,再根据数据渲染内容,这期间延迟的时间就是「IO瓶颈」)。

但是,前端框架能够掌控的范围局限在前端,所以无法对「IO瓶颈」做出极致优化,只能在三个因素中做出取舍(比如考虑用户体验与性能时,代码维护成本就高)。

React团队为了同时兼顾三者,需要对服务端拥有更多掌控。这就是RSC诞生的初衷。

但是,大部分React的受众只是把React当作前端view库,并不会直接使用RSC相关功能,所以React团队选择和Next.js团队合作,落地RSC

此时我们发现,React有三类受众:

  1. 普通前端开发者,用稳定的React做业务开发
  2. 其他合作团队(比如Next.js团队),React团队为他们提供API支持
  3. 喜欢尝鲜的开发者/团队,愿意尝试那些可能出现在未来版本中的特性(通常还不稳定)

React团队针对这三类受众,制定了三条版本迭代路径:

  1. Latest路径
  2. Canary路径
  3. Experimental路径

我们正常通过npm i react下载的React包就是「Latest路径」的打包产物。

通过npm update react@canary可以替换为canary包,RSC相关的功能就属于canary包。

同理,通过npm update react@xperimental可以替换experimental包。

脱离Next.js使用RSC

Next.jsApp Router模式,所有组件默认为服务端组件(即在服务端render的组件),只有当组件所在文件顶部标记了'use client'指令时,该组件是客户端组件(即在前端render的组件)。

比如下面就是个客户端组件:

'use client'
import {useState} from 'react';

function Cpn() {
  const [num, update] = useState(0);
  // ...省略
}

实际上,这并不是Next.js自己的定义,而是RSC中的规范。在React文档中,我们可以看到'use client''use server'规范的定义,其中:

  • 'use client'用于标记客户端组件(在服务端,默认所有组件都是服务端组件,所以客户端组件需要专门标记)
  • 'use server'用于标记前端的某个函数为Server Action(可以在前端执行的服务端逻辑)

既然是规范,那就需要落地。在Next.js中,规范的落地都被收敛到Next.js框架内部实现了。如果要脱离Next.js使用RSC,就需要我们自己落地规范。

RSC规范的落地包括三部分:

  1. 服务端编译时
  2. 服务端运行时
  3. 客户端运行时

这三者都被收敛到 react-server-dom-webpack包中。

接下来我们简单讲下这三部分的作用。

服务端编译时

通过react-server-dom-webpack/plugin名字中的webpackplugin字样能看出,这是个webpack插件,配置类似如下:

const ReactServerWebpackPlugin = require("react-server-dom-webpack/plugin");

const config = {
  // ...省略其他配置
  plugins: [
    new ReactServerWebpackPlugin({ isServer: false }),
  ],
}

他的作用是识别项目中的'use client'指令,作用有些类似于「全自动React.lazy」

使用过React.lazy特性的同学会知道,当我们通过React.lazy懒加载组件时,dynamic import的组件会被打包工具(比如webpack)打包成独立的chunk。当前端需要该组件时,会通过Jsonp请求chunk文件。

比如下面代码中的./Cpn.jsx组件由于懒加载,会被打包成独立的chunk

import React from 'react';

const LayCpn = React.lazy(() => import('./Cpn.jsx'));

function App(props) {
  return <LayCpn {...props} />; 
}

React.lazy类似,当我们在组件所在文件的顶部标记'use client'时,并在服务端组件的子孙组件中使用到该组件,该组件代码也会打包成独立的chunk。由于这个过程是全自动的,所以可以称为「全自动React.lazy」

服务端运行时

上面讲到的编译产物都是「客户端组件对应chunk」,所以他们是不会在服务端运行时使用的。

服务端运行时的作用类似SSR,都是给定JSX输入,经过render后获得输出。比如,给定如下输入:

function App() {
  return <div>hello</div>;
}

对于SSR,会获得字符串'<div>hello</div>'的输出。

对于RSC规范,将输入传给react-server-dom-webpack/server导出的renderToPipeableStream方法,会获得如下序列化数据:

0:"$L1"
1:["$","div",null,{"children":"hello"}]

再让我们看一个稍微复杂点的例子:

我们有个组件Cpn,由于他包含客户端状态(使用了useState),所以只能作为客户端组件(顶部标记'use client'):

'use client'
import {useState} from 'react';

function Cpn() {
  const [num, update] = useState(0);
  // ...省略
}

现在,我们的服务端组件App返回值中包含了Cpn

function App() {
  return <div><Cpn/></div>;
}

经由renderToPipeableStream方法,会获得如下序列化数据:

0:"$L1"
2:I["./src/app/Test.jsx",["client0","client0.chunk.js"],"Test"]
1:["$","div",null,{"children":["$","$L2",null,{}]}]

可以发现,序列化数据中并不包含具体的客户端组件代码,而是组件代码对应的文件(client0.chunk.js),这个文件就是我们在「服务端编译时」打包产生的chunk文件。

客户端运行时

「服务端运行时」产生的「序列化数据」传递给前端时,react-server-dom-webpack又出场了,这次使用的是react-server-dom-webpack/client

这个包提供了几个方法,用于将「从不同数据源获取的序列化数据」转换为「合法的React Element」,比如:

  • createFromFetch:通过fetch方法获取序列化数据
  • createFromReadableStream:通过可读流获取序列化数据

对于上述序列化数据:

0:"$L1"
2:I["./src/app/Test.jsx",["client0","client0.chunk.js"],"Test"]
1:["$","div",null,{"children":["$","$L2",null,{}]}]

经由react-server-dom-webpack/client中方法的转换,会得到一个React.lazy组件,这样前端的React就能正常render这个组件了。

总结

RSC规范属于React特性,来自于React Canary。规范的落地可以通过react-server-dom-webpack包实现。

整个工作流程包括三个阶段:

  1. 服务端编译时,对应react-server-dom-webpack/plugin
  2. 服务端运行时,对应react-server-dom-webpack/server
  3. 客户端运行时,对应react-server-dom-webpack/client

Next.js中,RSC规范的落地被集成到框架内部,做到了开箱即用的RSC,并在此基础上衍生出更完善的功能(App Router)。

发布于 2023-11-20 16:08

先提一点比较重要的,我认为和过去页面直出不一样的地方,就是客户端的 state 是会在 sever component 渲染后还是会保留的,这点在视频里也强调了很多遍,感觉是引以为豪的一个点(确实也是

但是感觉有点走偏,毕竟提到的痛点其实没那么痛

编辑于 2020-12-23 10:15

刚好翻译了一篇服务端组件的文章。

原文链接: 玄魂:【翻译】React 服务端组件

本周,React 团队发布了零打包体积的 React 服务端组件[1](Server Components),旨在用服务端驱动的心理模型实现现代 UX。这与组件的服务器端渲染(SSR)有很大的不同,可能会导致客户端 JavaScript 打包体积大大减少 。
我对这项工作的方向相当兴奋,虽然它还没有准备用于生产环境中,但非常值得继续关注。我强烈建议大家阅读 RFC[2] 或观看 Dan 和 Lauren 的演讲[3],了解更多细节。

英文原文 : addyosmani.com/blog/rea

一、服务端渲染局限性
今天的客户端 JavaScript 的服务器端渲染可能是次优的。你的组件的 JavaScript 代码在服务器上被渲染成一个 HTML 字符串。这个 HTML 被传递给浏览器,这可能会导致快速的首次内容绘制(First Contentful Paint)或大内容绘制(Largest Contentful Paint)。
然而,JavaScript 代码仍然需要被下载并解析以实现交互性,这通常是通过水合(hydration)步骤实现的。服务器端渲染通常用于初始页面加载,所以在水合(hydration)后您不太可能再看到它被使用。
注意:虽然确实可以利用 SSR 构建一个仅使用服务端渲染(SSR)的React应用,完全避免在客户端上水合,但界面上的重交互性往往需要跳出 React。服务端组件启用的混合模型将允许在每个组件的基础上决定是否是 SSR/CSR,或者两者都是。
通过 React 服务端组件,我们的组件可以定期重新获取。当有新的数据时,可以在服务器上运行带有组件的应用程序,从而限制了需要发送给客户端的代码量。
[RFC]: 开发者需要不断地选择使用第三方的包。使用一个包来渲染markdown或格式化一个日期,对于我们开发者来说是很方便的,但是它增加了代码的大小,影响了用户的性能。


二、服务端组件
React 新的服务端组件与服务器端渲染相得益彰,实现了渲染成中间抽象格式,而不需要添加到 JavaScript 打包结果中。这既可以在不损失状态的情况下将服务器树与客户端树合并,又可以扩展到更多组件。
服务端组件不是 SSR 的替代品。当搭配在一起时,它们支持以中间格式快速渲染,然后由服务器端渲染基础架构将其渲染成 HTML,使早期的渲染更加快速。我们对服务端组件生成的客户端组件进行 SSR,类似于 SSR 与其他数据获取机制的使用方式。
然而这一次,JavaScript 的捆绑规模将大大缩小。早期的探索表明,服务端组件对于减少打包体积尤为有效(-18-29%),但一旦进一步的基础设施工作完成,React 团队将对实际的收益有一个更清晰的认识。
[RFC]: 如果我们把上面的例子迁移到服务端组件上,我们就可以为我们的功能使用完全相同的代码,但避免将其发送到客户端--节省了超过 240K 的代码(在未压缩的情况下)。


三、自动代码分割
通过使用代码分割(code split),只向用户提供他们需要的代码,这被认为是一种最佳实践。这允许你将你的应用程序分解成更小的捆绑包,需要更少的代码发送到客户端。在服务端组件之前,人们会手动使用 React.lazy() 来定义 "拆分点",或者依靠元框架设置的启发式,比如路由/页面来创建新的分块(chunk)。


代码分割的一些挑战是:

  • 在框架(如 Next.js)之外,你经常不得不手动解决这个优化问题,用动态导入代替导入语句。
  • 它可能会延迟应用程序开始加载组件的时间,影响用户体验。

服务端组件引入了自动代码分割,将客户端组件中的所有正常导入视为可能的代码分割点。它们还允许开发人员更早地(在服务器上)选择使用哪个组件,允许客户端在渲染过程中更早地获取它。


四、服务端组件会取代 Next.js 的 SSR 吗?
不,它们是完全不同的。随着研究和实验的继续,最初采用的服务端组件其实会通过 Next.js 等框架进行实验。
Dan Abramov 对 Next.js SSR 和服务端组件之间的区别已经做了很好的解释[4]:

  • 服务端组件的代码从来不会被传送到客户端。在许多使用 React 的 SSR 实现中,组件代码无论如何都会通过 JavaScript 包发送至客户端。这可能会延迟可交互时间。
  • 服务端组件可以从树的任何地方访问后端。当使用 Next.js 时,你习惯于通过 getServerProps() 来访问后端,它的局限性在于只能在顶层页面工作。某些 npm 组件无法做到这一点。
  • 服务端组件可能会被重新获取,同时在树内保持客户端状态。这是因为主要的传输机制比单纯的 HTML 要丰富得多,允许对服务器渲染的部分(例如搜索结果列表)进行重新请求,而不会破坏里面的状态(例如搜索输入文本、焦点、文本选择)。

服务端组件的一些早期集成工作将通过一个具有以下功能的 webpack 插件来完成:

  • 定位所有客户端组件
  • 创建 ID 之间的映射 => 分块 URLs。
  • 一个 Node.js 加载器,将对客户端组件的导入替换为对其的引用(即不出现在纯服务端组件的打包结果中)。
  • 有些工作需要更深层次的集成(例如与 Routing 等部件的集成),这就是为什么让它与 Next.js 这样的框架一起工作会很有价值。

正如 Dan 所指出的,这项工作的目标之一是让框架变得更好。
五、深入学习,并欢迎向 React 团队反馈
要了解更多关于这项新功能的信息,请观看 Dan 和 Lauren 的演讲[5],阅读 RFC[6],并查看服务端组件的演示[7]来玩转这项新功能。感谢 Sebastian Markbåge、Lauren Tan、Joseph Savona 和 Dan Abramov 在服务端组件上的工作。


References
[1] 零打包体积的 React 服务端组件: reactjs.org/blog/2020/1
[2] RFC: github.com/reactjs/rfcs
[3] Dan 和 Lauren 的演讲: youtube.com/watch?
[4] 很好的解释: news.ycombinator.com/it
[5] 请观看 Dan 和 Lauren 的演讲: reactjs.org/blog/2020/1
[6] RFC: github.com/reactjs/rfcs
[7] 服务端组件的演示: github.com/reactjs/serv

发布于 2020-12-30 10:38

1.server和client组件的相互嵌套问题

2.增加了服务端压力

3.react不再是一个单纯的library

4.concurrent 模式更不稳定

优点是:组件可以共享

发布于 2020-12-23 11:02

聊点别的。

这玩意带Server。

按照我国很多团队前后端分离的形式,许多前端团队的负责人是不喜欢有server的。只需要一个静态服务往上面扔资源就行了。

所以我对这东西在国内流行程度成谨慎态度。


当然,这个方向还是有意义的。如果写过一点react ssr,你就会发现要自己解决一些前端工程问题,需要造一些轮子,搞一些东西完善你的开发、运维啥的。这一切都说明react ssr还是很不完善的。现在官方在这个方向上发力,无疑是往前进了一步。毕竟一个理想的web应用,服务端辅助渲染还是很有帮助的。

发布于 2020-12-26 23:52

在过去十年中,React 及其生态系统经历了不断的发展。每一个版本都带来了新概念、优化乃至范式的转变(虽然更新的比较慢),不断推动着我们对于网页开发可能性的认知边界。

React 服务器组件(RSC)是至今为止继 React Hooks 之后最重要的变革。

RSC 使得 React 应用能够同时利用服务器和客户端渲染的最佳特性,而且全程使用单一语言、单一框架以及统一的 API。

这篇文章会带你了解 React 多年来在渲染技术上的演变历程,帮助你理解为什么 RSC 不仅是必然的,也是构建高性能、低成本、提供卓越用户体验的 React 应用的关键。

编辑于 2024-04-02 23:03