2020年12月21日,圣诞节前夕,React团队发了关于React Server Components的博客和RFC。一方面是公布他们关于React的进展,另一方面是希望吸取业界的反馈。
本文解答:什么是React Server Components?有什么亮点?
Dan在视频开头,提到了Project Management中的概念:一个项目中,Good、Cheap、Fast三种属性很难同时具备,通常只能追求其二的极致。这同样适用于React开发:好的用户体验、低成本的代码维护、快的性能。
举个例子:一个典型的展示组件的例子。
给一个作曲家id,渲染他的详情页,里面可以看到他的TopTracks,Discography。当然,TopTracks和Discography不一定非要在详情页可以看到(其他地方也可以引用,希望它是公共的,不是详情页私有的组件)
如果能够在父组件统一获取数据,再传递数据给子组件,那么性能是快的,避免了多个API请求。用户体验也好。但是维护成本又高,每个数据需要一层一层传递下去,如果API做了改动,每个子组件都要改;此外要复用TopTracks和Discography的话,就需要在其它地方重新写获取数据的逻辑。
如果我们只允许用户在详情页能看到TopTracks和Discography,这样这俩组件不必是公用组件,我们可以以低成本、快性能的渲染他们。但是用户必须进入详情页才能看到这些组件,用户体验不好。
我们把每个数据获取逻辑放在各个组件内,维护起来很方便,用户体验也不错。但是性能差,发送了3个请求,而且这些请求是「瀑布流」:
先渲染ArtistDetails,等它拿到数据后,才会继续渲染children,children才会开始获取数据。
这只是前端项目中一个例子,但其实很常见。(只是他们产生的原因可能不同,很多项目中产生的原因是后者请求的API依赖于前者的结果。上面例子只是单纯因为渲染的顺序导致了瀑布流)
已有的问题,主要是Client和Server之间来回的数据获取逻辑,一来一回,再来再回,形成了瀑布流,损耗了很多时间。
GraphQL通过一次性请求数据,拿到了所有,解决了这个问题。
GraphQL + Relay (React中用于GraphQL获取数据的库)
这在Facebook内部带来了很多益处,但是难以推广普及:
Client发送请求,Server直接渲染组件,并在Server本地获取数据,不管瀑布有多长,都可以很快的拿到所有数据,然后在Server渲染出组件,一次性返回给Client。
在Server端运行的React Component就是React Server Component。
在过去,前端都是Client Component:
现在,引入Server Component后,组件树可能会是这样:
参考demo:https://github.com/reactjs/server-components-demo
NoteList.server.js
import {
fetch} from 'react-fetch';
export default function NoteList({
searchText}) {
const notes = fetch('http://localhost:4000/notes').json();
return notes.length > 0 ? (
<ul className="notes-list">
{
notes.map((note) => (
<li key={
note.id}>
<SidebarNote note={
note} />
</li>
))}
</ul>
) : (
<div className="notes-empty">
{
searchText
? `Couldn't find any notes titled "${
searchText}".`
: 'No notes created yet!'}{
' '}
</div>
);
}
Server Component里可以直接获取数据(不需要像Client Component一样,要在useEffect中执行)。而且它还可以操作本地文件、甚至可以直接执行数据库查询语句。Server Component在Server执行,后端能做的,它基本都可以做到。
Server Component里可以引用Client Component。以指令的形式返回给Client。
Server Component会将组件及其从IO请求到的数据序列化为特定的数据结构(称之为指令),以流的形式传递给前端:
客户端在运行时直接获取到填充了数据的流,并借助Concurrent Mode执行流式渲染。
指令含义:
Note.server.js中,引用了date-fns
,但是在浏览器Sources中看不到这个资源。而且Server Component全都不在里面。
Amazing!
Server Component不能有状态、事件监听器。如果需要交互,只能使用Client Component。(见SidebarNote.js,它把“数据”(其实是JSX)传递给了Client Component,由Client Component控制不同状态的显示内容)
Server Component不能传递函数参数给Client Component,因为函数无法被序列化。而JSX可序列化,所以也能传递JSX。
如果传递的JSX也是Server Component,那么它会在Server先渲染完毕,再发送给Client。(这意味着这部分Server Component也不会被Client下载)
左侧列表是个Server Component,引用的Client Component保存了展开/收起的状态,如果你新增、删除一个项目,他们展开/收起的状态会被保持!
Amazing!
这在CSR时代,是需要花一定成本才能实现的功能,在Server Component中,re-render直接可以保持Client Component的状态,非常优秀!
有的组件以.js结尾,而非.server.js或.client.js,则它既可以在Server渲染,又可以在Client渲染。
如果Server Component里用到了Shared Component或Server Component,那么将会在Server渲染后,以指令的形式返回给Client,他们不会被下载到Client。
如果Client Component里用到了Shared Component或Client Component,那么会在浏览器渲染,他们会被下载到Client。
如果Client Component里用到了Server Component,会发送请求。
Amazing!
很多系统是区分多角色的,不同角色应该看到不同的内容。这样组件级别的按需加载,正是我们想要的!
这在当前,是非常常见的React代码模式。但是把它切换为Server Component后,无编辑权限的人,不会下载EditToolbar组件。(CSR模式只能做到下载但不显示)
CSR时代,我们通常先调用API去修改数据,才重新get新的数据。但是使用Server Component后,我们只需要1次请求,就可以完成这件事!
Amazing!
React鼓励社区去开发维护这些IO库,可用于Server Components。但这不意味着重新造轮子,他们只是针对已有的node库做了一层很薄的封装,例如react-fs只封装了fs,react-fetch只封装了fetch。都不到一百行。封装成React IO库只是增加了缓存层。
结合Suspense,以流的形式渲染,开发成本低,效果好!
不会,他们可以协作。
假设我们开发一款MD编辑器。服务端传递给前端MD格式的字符串。
我们需要在前端引入将MD解析为HTML字符串的库。这个库就有206k。
import marked from ‘marked’; // 35.9K (11.2K gzipped)
import sanitizeHtml from ‘sanitize-html’; // 206K (63.3K gzipped)
function NoteWithMarkdown({text}) {
const html = sanitizeHtml(marked(text));
return (/* render */);
}
通过ServerComponent我们怎么解决这个问题呢?
只需要简单将NoteWithMarkdown标记为ServerComponent,将引入并解析MD这部分逻辑放在服务端执行。
ServerComponent并不会增加前端项目打包体积。这个例子中,一次性为我们减少了前端206K (63.3K gzipped)的打包体积以及解析MD的时间。
通过使用React.lazy可以实现组件的动态import。之前,这需要我们在切换组件/路由时手动执行。在ServerComponent中,都是自动完成的。
在上图中,左侧列表是ServerComponent,当点击其中卡片时,组件对应数据会动态加载。
Server Components 跟过去的 SSR 相比,你在拉取后不会丢失客户端的状态;
SSR 首次访问时返回渲染完的页面,Server Components 输出的是一系列指令。
Server Components可以通过分块加载、减少打包体积等方式,进一步提升加载速度。
注:二者是不同的技术方案,解决的问题不同,但可以混用。SSR主要解决的问题是SEO和首屏渲染速度。
在 PHP/ASP 时代,页面都是由服务器来渲染。服务器接到请求后,查询数据库然后把数据“塞”到页面里面,最后把生成好的 html 发送给客户端。当用户点击链接后,继续重复上面的步骤。这样用户体验不是很好,每个操作几乎都要刷新页面,服务器处理完之后再返回新的页面。
而 Angular/Vue/React 这种单页应用(SPA)则主要是客户端渲染。服务器接到请求后,把 index.html 以及 js/css/img 等发送给浏览器,浏览器负责渲染整个页面。后续用户操作和前面的 php/jquery 一样,通过 ajax 和后端交互。但是和 php 相比,第一次访问时只返回了什么内容都没有的 idnex.html 空页面,没法做 SEO。另一点就是页面需要等到 js/css 和接口都返回之后才能显示出来,首次访问会有白屏。
https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html
https://github.com/reactjs/rfcs/pull/188