React 官方对 Server Comopnent 是这样介绍的: zero-bundle-size React Server Components。
这是一种实验性探索,但相信该探索是个未来 React 发展的方向,与 React Server Component 相关的周边生态正在积极的建设当中。
在 React Server Component (以下称 Server Component) 推出之后,我们可以简单的将 React 组件区分为以下三种:
Server Component | 服务端渲染组件,拥有访问数据库、访问本地文件等能力。无法绑定事件对象,即不拥有交互性。 |
---|---|
Client Component | 客户端渲染组件,拥有交互性。 |
Share Component | 既可以在服务端渲染,又可以在客户端渲染。具体如何渲染取决于谁引入了它。当被服务端组件引入的时候会由服务端渲染当被客户端组件引入的时候会由客户端渲染。 |
React 官方暂定通过「文件名后缀」来区分这三种组件:
Server Component
需要以 .server.js(/ts/jsx/tsx)
为后缀Client Component
需要以 .client.js(/ts/jsx/tsx)
为后缀
Share Component
以 .js(/ts/jsx/tsx)
为后缀
简单来说 Server Component 是在服务端渲染的组件,而 Client Component 是在客户端渲染的组件。
与类似 SSR , React 在服务端将 Server Component 渲染好后传输给客户端,客户端接受到 HTML 和 JS Bundle 后进行组件的事件绑定。不同的是:Server Component 只进行服务端渲染,不会进行浏览器端的 hyration(注水),总的来说页面由 Client Component 和 Server Component 混合渲染。
这种渲染思路有点像 Islands 架构,但又有点不太一样。
如图:橙色为 Server Component, 蓝色为 Client Component 。
浏览器请求到 HTML 后,请求入口文件 - main.js, 里面包含了 React Runtime 与 Client Root,Client Root 执行创建一个 Context,用来保存客户端状态,与此同时,客户端向服务端发出 /react
请求。
// Root.client.jsx 伪代码
function Root() {
const [data, setData] = useState({});
// 向服务端发送请求
const componentResponse = useServerResponse(data);
return (
<DataContext.Provider value={[data, setData]}>
componentResponse.render();
</DataContext.Provider>
);
}
Client Root 代码执行后,浏览器会向服务端发送一个带有 data 数据的请求,服务端接收到请求,则进行服务端渲染。
服务端将从 Server Component Root 开始渲染,一颗混合组件树将在服务端渲染成一个巨大的 VNode。
module.exports = {
tag: 'Server Root',
props: {...},
children: [
{ tag: "Client Component1", props: {...}: children: [] },
{ tag: "Server Component1", props: {...}: children: [
{ tag: "Server Component2", props: {...}: children: [] },
{ tag: "Server Component3", props: {...}: children: [] },
]}
]
}
M1:{"id":"./src/BlogMenu.client.js","chunks":["client0"],"name":"xxx"}
J0:["$","main", null, ["]]
J: 代表 Server Compnent 渲染出的类 react element格式的字符串
import ClientComponent from "./ClientComponent";
const ServerRootComponent = () => {
return <ClientComponent title="xxx" />
};
但需要注意的是:这里传递的数据必须是可序列化的,也就是说你无法通过传递 Function 等数据。
// Client Component
function ClientComponent() {
const sendRequest = (props) => {
const payload = JSON.stringify(props);
fetch(`http://xxxx:8080/react?payload=${payload}`)
}
return (
<button
onclick = {() => sendRequest({ messgae: "something" })}
>
Click me, send some to server
</button>
)
}
// Serve Component
const ServerRootComponent = ({ messgae: "something" }) => {
return <ClientComponent title="xxx" />
};
RSC 推出的背景是 React 官方想要更好的用户体验,更低的维护成本,更高的性能。通常情况下这三者不能同时获得,但 React 团队觉得「小孩子才做选择,我全都要」。
根据官方提出 RFC: React Server Components( https://github.com/josephsavona/rfcs/blob/server-components/text/0188-server-components.md ),可以通过以下几点能够看出 React 团队是如何做到"全都要"的:
通常情况下,我们在前端开发上使用很多依赖包,但实际上这些依赖包的引入会增大代码体积,增加 bundle 加载时间,降低用户首屏加载的体验。
例如在页面上渲染 MarkDown
,我们不得不引入相应的渲染库,以下面的 demo 为例,不知不觉我们引入了 240 kb 的 js 代码,而且往往这种大型第三方类库是没办法进行 tree-shaking。
// NOTE: *before* Server Components
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 */);
}
为了获取数据,前端通常需要请求后端接口,这是因为浏览器是没办法直接访问数据库的。但既然我们都借助服务端的能力了,那我们当然可以直接访问数据库,React 在服务器上将数据渲染进组件。
......
@loadable
。然而无论是使用哪一种,都会有以下两个问题:React Server Component 将所有 Client Component 的导入视为潜在的分割点。也就是说,你只需要正常的按分模块思维去组织你的代码。React 会自动帮你分割
import ClientComponent1 from './ClientComponent1';
function ServerComponent() {
return (
<div>
<ClientComponent1 />
</div>
)
}
React server component通过在服务器上的实时编译和渲染,将抽象层在服务器进行剥离,从而降低了抽象层在客户端运行时所带来的性能开销。
举个例子,如果一个组件为了可配置行,被多个 wrapper 包了很多层。但事实上,这些代码最终只是渲染为一个
<div>
。如果把这个组件改造为 server component 的话,那么我们只需要往客户端返回一个<div>
字符串即可。下面例子,我们通过把这个组件改造为server component,那么,我们大大降低网络传输的资源大小和客户端运行时的性能开销:// Note.server.js
// ...imports...
function Note({id}) {
const note = db.notes.get(id);
return <NoteWithMarkdown note={note} />;
}
// NoteWithMarkdown.server.js
// ...imports...
function NoteWithMarkdown({note}) {
const html = sanitizeHtml(marked(note.text));
return <div ... />;
}
// client sees:
<div>
<!-- markdown output here -->
</div>参考自: https://juejin.cn/post/6918602124804915208#heading-5
我们可以通过在 Server Component ,将 HOC 组件进行渲染,可能渲染到最后只是一个 <div>
我们就无需将 bundle 传输过去,也无需让浏览器消耗性能去渲染。
「默认走 Server Component,若有交互需要则走 Client Component」 通过这种原则,相信在一定程度上能给减轻开发者的心智负担。