原文作者:魔术师卡颂
12月21日,React
团队公布了一个新的提案 Server Components
。
伴随这个提案同时发出的,还有一个小时的视频讲解、可供运行的Demo、详尽的介绍。
这里有react团队的相关rfc
可见,React
团队很重视这个提案。本文会从如下方面讲解:
Server Components
是什么Server Components
解决了什么问题
ServerComponent是什么
一句话概括:
Server Components
是在服务端运行的 React
组件。
咦?这和 服务端渲染
(SSR)有什么区别?
相比 SSR
将组件在服务端渲染成填充内容的 HTML
字符串,并在客户端 hydrate
后使用。Server Components
更像我们的在客户端写的普通组件一样,只不过他的运行环境是服务端。
我们可以将组件按照功能分为:
- 提供数据的
容器组件
- 渲染数据并提供数据交互的
交互组件
举个例子,Note
组件是 容器组件
,他负责请求并缓存数据。NoteEditor
是渲染 note
数据并执行用户交互的 交互组件
。
function Note(props) {
const [note, setNote] = useState(null);
useEffect(() => {
fetchNote(props.id).then(noteData => {
setNote(noteData);
});
}, [props.id]);
if (note == null) {
return "Loading";
} else {
return <NoteEditor note={note}/>
}
}
如例子所述,我们可以通过在 useEffect
中发起请求并将返回的数据保存在 state
中。
这种「请求-渲染」
模式会遇见被称为 waterfall
的问题:
就像一节一节的瀑布往下流水,NoteEditor
需要等待 Note
请求 note
成功后才能开始渲染。
当 交互组件
依赖的数据源越多,waterfall
问题会更明显。
理论上,如果 React
足够聪明,就能在 服务端
执行 容器组件
的渲染逻辑,在 客户端
执行 交互组件
的渲染逻辑。
按照这样的理念,如下这棵完全在客户端渲染的组件树:
可以拆分为:在 服务端
运行的 容器组件
和在 客户端
运行的 交互组件
。
其中在服务端运行的 容器组件
就是 Server Component
。
ServerComponent的意义
既然 ServerComponent
在 服务端
运行,天然更接近各种 IO
(请求数据库、读取文件、缓存…)。
上面的例子完全可以直接从 数据库
获取 note
数据,同时借助 Suspense
,采用同步的写法。
function Note(props) {
const note = db.notes.get(props.id);
if (note == null) {
return "Loading";
}
return <NoteEditor note={note}/>
}
天然更接近后端
任何其他数据源只需要通过 React
提供的 API
简单封装,使其支持 Suspense
,就能接入 ServerComponent
中。天然更接近后端。
解决waterfall
区别于 SSR
传输的 HTML
字符串。ServerComponent
会将 Note
组件及其从 IO
请求到的数据序列化为类似 JSX
的数据结构,以流的形式传递给前端:
客户端在运行时直接获取到填充了数据的 流
,并借助 Concurrent Mode
执行流式渲染。
0打包体积
假设我们开发一款 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
,当点击其中卡片时,组件对应数据会动态加载。
更好的ahead-of-time (AOT)优化
Vue
作为一门使用 模版语言
的框架,模版语言
的固定写法使其能在编译时针对模版内容作出优化。
由于 JSX
仅仅是 JS
的语法糖,React
很难在编译时做出优化。
ServerComponent
对组件提出了更多限制(不能使用 useState
、useEffect
…)。这些限制从侧面为 AOT
提供更多优化线索。
ServerComponent的使用
下面我们通过改写一个 记事本
组件讲解 ServerComponent
的使用:
// Note.js
import fetchData from './fetchData';
import NoteEditor from './NoteEditor';
function Note(props) {
const {id, isEditing} = props;
const note = fetchData(id);
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
{isEditing
? <NoteEditor note={note} />
: null
}
</div>
);
}
Note
组件的主要功能是根据 props
传入的 id
请求对应的 note
数据。
NoteEditor
用于展示及修改 note
。
其中 fetchData
方法用于获取数据,数据的加载中状态由组件外的 Suspense
完成。
可以看到,交互部分由 NoteEditor
完成,Note
主要功能是获取并传递数据。
接下来我们将 Note
变为 ServerComponent
。
// 注意🙋
// Note.server.js - Server Component
// 注意🙋
import db from 'db.server';
// 注意🙋
import NoteEditor from './NoteEditor.client';
function Note(props) {
const {id, isEditing} = props;
const note = db.posts.get(id);
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
{isEditing
? <NoteEditor note={note} />
: null
}
</div>
);
}
有3点需要注意的改动,我们依次了解下:
1.Note.js
文件名改为 Note.server.js
代表这是 Server Component
。
2.Note.server.js
运行于服务端,我们不需要客户端的 fetchData
方法,可以直接访问数据库,所以这里调用 db.server
提供的方法。
3.NoteEditor
用于展示及修改 note
。这是由客户端用户的交互控制的,所以将文件名改为 NoteEditor.client
代表这是个 Client Component
。
总结
太阳底下没有新鲜事。早期前端交互简单,仅仅作为服务端的 View
层。
随着前端交互变复杂,出现了前端框架主导的客户端渲染(CSR)。
为了解决首屏渲染速度、SEO问题,出现了服务端渲染(SSR),又回到了曾经作为 View
层的起点,只不过控制的粒度更细。
ServerComponent
提案的出现,预示着 React
的长远目标:将对 View
层的控制细化到组件级别。
为什么是「长远目标」
?ServerComponent
落地的大前提是 Concurrent Mode
生产环境稳定,让我们一起期待2021年吧。