React Server Component 第一篇:React Server Component 为何要出现,解决什么问题?

在开始介绍 React Server Component 之前,我想先介绍一下这些年来前端开发模式的一个变更

part 1 刀耕火种 模板+XMLHttpRequest+jQuery

一开始的网站,实际上就是客户端访问一个 url ,服务端将这个页面的信息全部吐回来,为了加快这个过程的开发效率,服务端有一种叫做模板的手段来帮忙实现这个过程,比如使用 express 的时候可以使用 ejs 模板,使用 php 的 thinkphp 框架的 thinlTemplate 等,这里用 express 来举一个例子。

javascript 复制代码
const express = require('express');
const app = express();

// 设置视图引擎为 ejs
app.set('view engine', 'ejs');

// 定义路由处理函数
app.get('/', (req, res) => {
  // 传递变量给模板
  const message = "Hello, World!";

  // 渲染模板并返回结果
  res.render('index', {message});
});

// 启动服务器
app.listen(3000, () => {
  console.log("Server is running on port 3000");
});
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>EJS Example</title>
</head>
<body>
    <h1><%= message %></h1>
</body>
</html>

后面出现了 XMLHttpRequest,人们发现服务端可以不用一开始就吐出所有内容,而是在客户端使用 XMLHttpRequest去动态的拿到数据然后渲染在页面上。 举个例子,现在网页有一个查看评论的功能,第一页只显示了 10 条评论,然后客户在点击查看更多的时候,使用 XMLHttpRequest 获取到第二页的数据,在将其展示到屏幕上。 随着网页所具有的功能越来越多,网页中对于 javascript 的使用也越来越多,为了方便开发人员实现诸如 dom 操作、发送请求等操作,类似 jQuery 这种上层工具应运而生。 此时的开发模式我猜大概是,专职开发网页的程序员于服务端开发人员约定好页面模板中相关的字段,然后开发好页面逻辑后,将其交给服务端开发人员,服务端开发人员把页面仍到模板文件中去,然后再渲染模板文件的时候注入相应的字段。 这个阶段的重点就是在服务端就能获取到数据,然后根据模板渲染生成 html 文档响应给客户端

part2 三大框架崛起 前后端分离 SPA 模式兴起

这时候又有人就说了,既然前端能够通过 XMLHttpRequest 动态获取到数据,那为啥不数据全部都由前端自己去获取然后再展示在页面上?

这个犀利的想法掀起了一波前端开发模式更新的浪潮。 在我大三、大四这个阶段(2017,2018) 年,三大框架的浪潮也开始席卷国内。state = ui 的开发模式征服了绝大部分开发者。 前后端分离的概念也被提出来了,这时候,后端只负责出 api,页面被打包成了静态资源直接丢到服务器上,页面路由、数据等全都由前端页面自己去管理。 加载页面html文件、加载页面js、渲染出页面UI、组件生命周期触发、加载云端api、更新state刷新页面 这种模式叫做 spa 模式,也就是单页web应用。一开始 html 上是空白内容,依赖 js 能力去呈现页面内容。前端通过 url hash 的特性或者history api 去实现页面路由。为了避免出现 js 文件太大的情况,还将不同页面打包为不同的 chunk,在"加载"页面的时候在动态的加载页面的 js 文件。

part3 SPA 的缺陷,引入 SSR

SPA 虽然很火,也彻底划分了前端开发人员和服务端发开人员的界限。但是 SPA 也有缺陷。 比如,一是 SPA 的页面在目前是无法 SEO 的,因为 index.html 页面是空的里面只有一个 root 节点,其他的节点都是 js 文件加载之后绘制上去的,搜索引擎根本无法爬取到页面的内容;二是 SPA 不利于首页白屏时间的优化,如下图所示:

在加载到关键 js(比如是页面的 chunk) 之后 js 才向屏幕上绘制 dom 节点,而再此之前,页面都是白屏的。

此时 SSR 应运而生,它的页面加载的流程跟上图其实区别不大。其核心思想就是使用 nodejs 起一个服务。以 React 为例子:页面 html 请求到服务器之后,在服务端就将 React 组件渲染为 html字符串,这样客户端在获得服务端的响应的时候 html 就已经准备就绪了。在配合合理加载 js 文件的手段,这样在 js 文件加载之前页面就已经绘制在屏幕上了。待到 js 文件加载完成之后,在进行 hydration(通常称为水合)将点击事件等加到 dom 节点上去。

part4 React Server Component

这里我从一个真实的业务场景去切入,来引出为什么 React Server Component 值得被应用。 前段时间在尝试优化我们业务的首页加载性能 页面的首页加载性能,一开始是这样的:

这个页面在 server 端渲染的是骨架屏,然后在 client 端页面组件的 useEffect 中并发查询如图所示中的 1,2 请求,待请求 1 响应之后再根据接口响应内容判断是否要展示商品列表组件,如果要展示商品列表组件在在商品列表组件的 useEffect 中查询 3 接口,在根据接口响应的内容渲染出商品列表。

其实这个页面最关键的元素是商品列表(在 Lighthouse 性能分析中,当页面显示出商品列表时,商品列表中的元素也被选取为 LCP 关键元素),但是商品列表却要等到接口 1,2 响应完毕之后才能被渲染,这无形之中就拉长了 LCP 的时间。 如果将 1,2 两条接口放到 next.js 的服务端去请求会不会快一点呢?答案是肯定的。next.js 服务和云端的服务都部署在我们公司的集群之内,举个例子,从北京到北京和从杭州到北京,那肯定是本地人更快。

根据统计出来的数据来看,查询 1 接口在 client 端超过 25%的用户花费 200ms,而在 next.js 服务端调用这条接口所需时间仅为 42ms 左右。 于是我将接口1 和 接口2 放到了服务端去请求,客户端不在请求这两条接口,最后的结果是:LCP 的时间得到了优化,但是 FCP 的时间变长了。 为什么 FCP 的时间会变长呢?因为在 server 端渲染 html 字符中加入了接口请求啊,且这个接口请求是会阻塞渲染 html 字符的。 React 不是搞过一个 Suspense 组件么,要是在 server 端也能 Suspense 就好了... 在 server 端接口请求的时候先返回骨架屏等到server 端的接口响应了绘制出了新的 html 字符串在替换页面上的 fallback UI。

React 18 其实支持服务端的 Suspense 了,提供了新的服务端渲染的 api,它将 React Tree 渲染为数据流,在一开始的时候返回 Suspense 组件的 fallback UI,当服务端的数据准备就绪之后,React 将额外的 HTML 发送到同一个流中,它是一个最小的内联 script 标签,已替换 fallback UI。

关于 React 18 Suspense 的详情可以阅读末尾的 New Suspense SSR Architecture in React 18 文章。

讲到了这里,还是没有出现 React Server Component,好像 React 18 的 Suspense SSR 已经满足了我们的需要的了啊?别急,在往下看看。 讲道理,只要升级到 React 18,使用 React 新提供的 api 在利用 Suspense 是能满足需求的。但是如果要按照现有的去实现应该怎么做呢?请看demo: codesandbox.io/p/devbox/ss...

sandcodebox 不太会用,demo 需要自己手动访问 http://localhost:5555. 且预览控制台打印不出 log,需要完整功能的可以下载仓库代码,仓库地址:github.com/Chechengyi/...

在 codesandbox 的预览界面访问 http://localhost:5555。 可以看到先出现 fallback ui ,然后再出现原本的 UI。 但是有个问题,组件在 server 端和 client 端都会执行一次,这里这里幸运的是 server 端和 client 端都有 fetch 这个 api,才导致程序没有报错,能在服务端执行的逻辑可远远不只 fetch,例如还有读取文件内容甚至操作数据库等等,这些操作在客户端是无法进行的。

退一步讲,我们阉割服务端的能力,约定只在 server 端执行 fetch 操作,按照这样的模式开发下去,组件内执行 fetch 的地方都要去区分 server 端和 client 端的逻辑,还得考虑 server 端把数据拼接到 html 里面让 client 端直接读这个数据,这样才能保证 server 端和 client 端的"同构"。类似这样:

太麻烦了,能不能有一些组件我能确保它就只在 server 端渲染呢? 终于讲到主角了。 之所以费那么多话是想说,RSC 它真的是从实际的业务场景的角度出发得出的用以提升应用程序体验的手段,我猜测 React 团队也是这样一步步探索最终确定现在的 SSR 还不够,需要提供一种开发范式让开发人员确定有一部分组件只会在服务端上运行,开发人员可以利用这部分能力去在服务端执行获取页面关键数据的操作,且能够不阻塞页面第一屏的渲染。 前端开发就是一个圈,绕了一圈发现回到了 part1,只不过现在的模板变成了 React。只不过经过了这一圈,前端开发的边界比起以前拓展了太多了。

Server Component 的特点:

  1. Server Component 只在服务器上运行,对于组件所依赖的一些包不会被下载到客户端,有助于减小依赖包的大小。
  2. Server Component 可以做一切在服务端能够做的操作,比如直接操作数据库、文件系统等。
  3. 有了 Server Component 自然也有了 Client Component,它们可以无缝集成,Server Component 可以在服务器上加载数据且通过 props 传递给 Client Component,然后 Client Component 在处理页面交互的部分,如:点击事件等等
  4. Server Component 可以动态选择要呈现的 Client Component,这样客户端可以只下载需要呈现的页面所需最小的代码量
  5. Server Component 可以与 Suspense 相结合,以增量方式将 UI 以数据流方式传输到客户端,开发人员可以精心设计加载状态,避免出现白屏、快速显示重要内容。

第1条:举个例子现在 Server Component 获取到了评论数据,其中有一个字段是评论时间,在 Server Component 里面引入类似moment.js 的时间处理库,Server Component 在服务端运行的时候就直接用 moment.js 处理了时间,客户端获取到的产物中没有 moment.js 代码。

总结-那么 React Server Component 到底解决了什么问题?

这是 React 核心开发成员 Dan 在 React Server Component 的宣讲视频 中提到的点,将软件开发中的不可能三角:Good、Cheap、Fast 引用到 UI 的领域:想要打造良好的用户体验(Good)、且产品的维护成本很低(Cheap)、在性能方面还要快(Fast)。

回顾一下前端开发历经的这些改变

part2,通过开发能够满足良好的用户体验和可维护性,但是性能不好,客户端必须在关键 js 加载完成之后页面才有内容。

part3,通过开发能够满足良好的用户体验和性能,但是维护性不好,需要时刻在代码里关注 server 端和 client 端的区别逻辑。

至于 part1,我都懒得讲,因为 state = ui 的开发模式实在太香了,我才不想回去自己处理那么多的 dom 操作。

我理解,React Server Component 就是在 part3 的基础之上,解决代码维护性的问题,且带来更好的用户体验和性能。其直接在服务端上运行,在获取数据方面相比 client 端有天然的优势;其让开发者能直接在 React 组件中编写服务端操作逻辑,符合现有 React 开发人员的开发习惯,且支持流式渲染不阻塞页面请求不会让客户端出现长时间的白屏情况;其提出概念明确区别了"服务端组件"和"客户端组件",明确职责服务端渲染负责数据获取、客户端组件负责实现用户交互, 开发者不需要再关注一个组件双端渲染的差异。

最后说一遍结论:RSC 是 React 团队提出的一种开发范式,目的是让应用程序拥有好的体验的情况下、代码易于维护且性能还好。React 提供了相关的 api,但是需要开发者将此功能集成到自己的框架当中,以实现组件仅在服务端运行的能力。目前 next.js 的 14 版本已经将这部分功能集成。

强调 2 点:

  1. React SSR 和 React Server Componnet 不同,它们也不存在相互替代的关系,相反,它们结合使用才能使应用程序变的更完美
  2. React 仍然只是纯粹的 UI 库,React Server Componnet 只是 React 团队提出的一种开发范式,以让应用程序获得更好的性能。

今天的内容就到这里了,如果文中有错误的地方欢迎指出。

后续计划:

  • React Server Component 第一篇:React Server Component 为何要出现,解决什么问题?(done)
  • React Server Component 第二篇:尝试实现 React Server Component (努力中)
  • React Server Component 第三篇:了解 next.js 源码,看 next.js 是如何结合 SSR 和 RSC 的(努力中)

参考文章

New Suspense SSR Architecture in React 18

RFC: React Server Components

相关推荐
曈欣29 分钟前
vue 中属性值上变量和字符串怎么拼接
前端·javascript·vue.js
麒麟而非淇淋29 分钟前
Node.js 学习
学习·node.js
QGC二次开发1 小时前
Vue3:v-model实现组件通信
前端·javascript·vue.js·前端框架·vue·html
努力的小雨2 小时前
从设计到代码:探索高效的前端开发工具与实践
前端
小鼠米奇2 小时前
详解Ajax与axios的区别
前端·javascript·ajax
Bunury3 小时前
Vue3新组件transition(动画过渡)
前端·javascript·vue.js
zero.cyx3 小时前
JS函数部分
开发语言·前端·javascript
超级小的大杯柠檬水3 小时前
SpringBoot lombok(注解@Getter @Setter)
java·前端·spring
AvatarGiser3 小时前
《ElementUI/Plus 踩坑》el-table + sortablejs 拖拽顺序错乱(Vue2/3适用)
前端·vue.js·elementui
flytam3 小时前
如何使用 React、TypeScript、TailwindCSS 和 Vite 创建 Chrome 插件
chrome·react.js·typescript