截止目前,React Server Component 还在开发与研究中,所以不适合投入生产环境使用。但其概念很是有趣,值得技术人学习。php
目前除了国内各类博客、知乎解读外,最一手的学习资料有下面两处:html
我会结合这些一手资料,与一些业界大牛的解读,系统的讲清楚 React Server Component 的概念,以及我对它的一些理解。前端
首先咱们来看,为何须要提出 Server Component 这个概念:mysql
Server Component 概念的提出,是为了解决 "用户体验、可维护性、性能" 这个不可能三角,所谓不可能三角就是,最多同时知足两条,而没法三条都同时知足。react
简单解释一下,用户体验体如今页面更快的响应、可维护性体如今代码应该高内聚低耦合、性能体如今请求速度。webpack
一言蔽之,在先后端解耦的模式下,惟一链接的桥梁就是取数请求。要把用户体验作好,取数就要提早并行发起,而前端模块是独立维护的,因此在前端作取数聚合这件事,必然会破坏前端可维护性,而这并行这件事放在后端的话,会由于后端不能解析前端模块,致使给出的聚合信息滞后,长此以往变得冗余。git
要解决这个问题,就必须加深前端与后端的联系,因此像 GraphQL 这种先后端约定方案是可行的,但由于其部署成本高,收益又仅在前端,因此难以在后端推广。github
Server Component 是另外一种方案,经过启动一个 Node 服务辅助前端,但作的不是 API 对接,而是运行前端同构 js 代码,直接解析前端渲染模块,从中自动提取请求并在 Node 端直接与服务器通讯,由于服务端间通讯成本极低、前端代码又不须要作调整,请求数据也是动态按需聚合的,所以同时解决了 "用户体验、可维护性、性能" 这三个问题。web
其核心改进点以下图所示:sql
<img width=300 src="https://img.alicdn.com/imgextra/i2/O1CN01NttXOI21kaFJgNDx1_!!6000000007023-2-tps-720-466.png">
如上图所示,这是先后端正常交互模式,能够看到,Root
与 Child
串行发了两个请求,由于网络耗时与串行都是严重阻塞部分,所以用红线标记。
Server Component 能够理解为下图,不只减小了一次网络损耗,请求也变成了并行,请求返回结果也从纯数据变成了一个同时描述 UI DSL 与数据的特殊结构:
<img width=500 src="https://img.alicdn.com/imgextra/i1/O1CN01MDYxZ71K0IkACLmFJ_!!6000000001101-2-tps-1142-468.png">
到此,恭喜你已经理解了 Server Component 核心概念,若是你只想泛泛了解一下,读到这里就能够结束了。若是你还想深刻了解其实现细节,请继续阅读。
归纳的说,Server Component 就是让组件拥有在服务端渲染的能力,从而解决不可能三角问题。也正由于这个特性,使得 Server Component 拥有几种让人眼前一亮的特性,都是纯客户端组件所不具有的:
Server Component 将组件分为三种:Server Component、Client Component、Shared Component,分别以 .server.js
、.client.js
、.js
后缀结尾。
其中 .client.js
与普通组件同样,但 .server.js
与 .js
均可能在服务端运行,其中:
.server.js
必然在服务端执行。.js
在哪执行要看谁调用它,若是是 .server.js
调用则在服务端执行,若是是 .client.js
调用则在客户端执行,所以其本质还要接收服务端组件的约束。下面是 RFC 中展现的 Server Component 例子:
// Note.server.js - Server Component import db from 'db.server'; // (A1) We import from NoteEditor.client.js - a Client Component. import NoteEditor from 'NoteEditor.client'; function Note(props) { const {id, isEditing} = props; // (B) Can directly access server data sources during render, e.g. databases const note = db.posts.get(id); return ( <div> <h1>{note.title}</h1> <section>{note.body}</section> {/* (A2) Dynamically render the editor only if necessary */} {isEditing ? <NoteEditor note={note} /> : null } </div> ); }
能够看到,这就是 Node 与 React 混合语法。服务端组件有着苛刻的限制条件:不能有状态,且 props
必须能被序列化。
很容易理解,由于服务端组件要被传输到客户端,就必须通过序列化、反序列化的过程,JSX 是能够被序列化的,props 也必须遵循这个规则。另外服务端不能帮客户端存储状态,所以服务端组件不能用任何 useState
等状态相关 API。
但这两个问题均可以绕过去,即将状态转化为组件的 props
入参,由 .client.js
存储,见下图:
<img width=250 src="https://img.alicdn.com/imgextra/i4/O1CN01ChPZdO1ky0Nsu2ygV_!!6000000004751-2-tps-514-278.png">
或者利用 Server Component 与 Client Component 无缝集成的能力,将状态与没法序列化的 props
参数都放在 Client Component,由 Server Component 调用。
这句话听起来有点夸张,但其实在 Server Component 限定条件下还真的是。看下面代码:
// NoteWithMarkdown.js 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 */); }
marked
与 sanitize-html
都不会被下载到本地,因此若是只有这一个文件传输,客户端的理论增长体积就是 render
函数序列化后字符串大小,可能不到 1KB。
固然这背后也是限制换来的,首先这个组件没有状态,没法在客户端实时执行,并且在服务端运行也可能消耗额外计算资源,若是某些 npm 包计算复杂度较高的话。
这个好处能够理解为,marked
这个包仅在服务端读取到内存一次,之后只要后客户端想用,只须要在服务端执行 marked
API 并把输出结果返回给客户端,而不须要客户端下载 marked
这个包了。
因为 Server Component 在服务端执行,所以能够执行 Nodejs 的任何代码。
// Note.server.js - Server Component import fs from 'react-fs'; function Note({id}) { const note = JSON.parse(fs.readFile(`${id}.json`)); return <NoteWithMarkdown note={note} />; }
咱们能够把对请求的理解拔高一个层次,即 request
只是客户端发起的一个 Http 请求,其本质是访问一个资源,在服务端就是个 IO 行为。对于 IO,咱们还能够经过 file
文件系统写入删除资源、db
经过 sql 语法直接访问数据库,或者 request
直接在服务器本地发出请求。
咱们都知道 webpack 能够经过静态分析,将没有使用到的 import 移出打包,而 Server Component 能够在运行时动态分析,将当前分支逻辑下没有用到的 import 移出打包:
// PhotoRenderer.js import React from 'react'; // one of these will start loading *once rendered and streamed to the client*: import OldPhotoRenderer from './OldPhotoRenderer.client.js'; import NewPhotoRenderer from './NewPhotoRenderer.client.js'; function Photo(props) { // Switch on feature flags, logged in/out, type of content, etc: if (props.useNewPhotoRenderer) { return <NewPhotoRenderer {...props} />; } else { return <OldPhotoRenderer {...props} />; } }
这是由于 Server Component 构建时会进行预打包,运行时就是一个动态的包分发器,彻底能够经过当前运行状态好比 props.xxx
来区分当前运行到哪些分支逻辑,而没有运行到哪些分支逻辑,而且仅告诉客户端拉取当前运行到的分支逻辑的缺失包。
纯前端模式与之相似的写法是:
const OldPhotoRenderer = React.lazy(() => import('./OldPhotoRenderer.js')); const NewPhotoRenderer = React.lazy(() => import('./NewPhotoRenderer.js'));
只是这种写法不够原生,且实际场景每每只有前端框架把路由自动包一层 Lazy Load,而普通代码里不多出现这种写法。
通常考虑到取数网络消耗,咱们每每会将其处理成异步,而后在数据返回前展现 Loading:
// Note.js function Note(props) { const [note, setNote] = useState(null); useEffect(() => { // NOTE: loads *after* rendering, triggering waterfalls in children fetchNote(props.id).then(noteData => { setNote(noteData); }); }, [props.id]); if (note == null) { return "Loading"; } else { return (/* render note here... */); } }
这是由于单页模式下,咱们能够快速从 CDN 拿到这个 DOM 结构,但若是再等待取数,总体渲染就变慢了。而 Server Component 由于自己就在服务端执行,所以能够将拿 DOM 结构与取数同时进行:
// Note.server.js - Server Component function Note(props) { // NOTE: loads *during* render, w low-latency data access on the server const note = db.notes.get(props.id); if (note == null) { // handle missing note } return (/* render note here... */); }
固然这个前提是网络消耗敏感的状况,若是自己就是一个慢 SQL 查询,耗时几秒的状况下,这样作反而拔苗助长。
看下面的例子:
// 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>
虽然在组件层面抽象了 Note
与 NoteWithMarkdown
两个组件,但因为真正 DOM 内容实体只有一个简单的 div
,因此在 Server Component 模式下,返回内容就会简化为这个 div
,而无需包含那两个抽象的组件。
Server Component 模式下有三种组件,分别是 Server Component、Client Component、Shared Component,其各自都有一些使用限制,以下:
Server Component:
useState
、useReducer
等状态存储 API。useEffect
等生命周期 API。window
等仅浏览器支持的 API。Client Component:
❌ 不能引用 Server Component。
<ClientTabBar><ServerTabContent /></ClientTabBar>
。Shared Component:
useState
、useReducer
等状态存储 API。useEffect
等生命周期 API。window
等仅浏览器支持的 API。其实不难理解,由于 Shared Component 同时在服务器与客户端使用,所以兼具它们的劣势,带来的好处就是更强的复用性。
要快速理解 Server Component,我以为最好也是最快的方式,就是找到其与十年前 PHP + HTML 的区别。看下面代码:
$link = mysqli_connect('localhost', 'root', 'root'); mysql_select_db('test', $link); $result = mysql_query('select * from table'); while($row=mysql_fetch_assoc($result)){ echo "<span>".$row["id"]."</span>"; }
其实 PHP 早就是一套 "Server Component" 方案了,在服务端直接访问 DB、并返回给客户端 DOM 片断。
React Server Component 在折腾了这么久后,能够发现,最大的区别是将返回的 HTML 片断改成了 DSL 结构,这实际上是浏览器端有一个强大的 React 框架在背后撑腰的结果。而这个带来的好处除了可让咱们在服务端能继续写 React 语法,而不用退化到 "PHP 语法" 之外,更重要的是组件状态得以维持。
另外一个重要不一样是,PHP 没法解析如今前端生态下任何 npm 包,因此无从解析模块化的前端代码,因此虽然直觉上感受 PHP 效率与 Server Component 并没有区别,但背后的成本是得写另外一套不依赖任何 npm 包、JSX 的语法来返回 HTML 片断,Server Component 大部分特性都没法享受到,并且代码也没法复用。
因此,本质上仍是 HTML 太简单了,没法适应现在前端的复杂度,而普通后端框架虽而后端能力强大,但在前端能力上还停留在 20 年前(直接返回 DOM),惟有 Node 中间层方案做为桥梁,才能较好的衔接现代后端代码与现代前端代码。
其实在 PHP 时代,先后端均可以作模块化。后端模块化显而易见,由于能够将后端代码模块化的开发,最后打包至服务器运行。前端也能够在服务端模块化开发,只要咱们将先后端代码剥离出来便可,下图青色是后端部分,红色是前端部分:
<img width=400 src="https://img.alicdn.com/imgextra/i3/O1CN01jsKjLq1iWPHi9C4pQ_!!6000000004420-2-tps-894-642.png">
但这有个问题,由于后端服务对浏览器来讲是无状态的,因此后端模块化自己就符合其功能特征,但前端页面显示在用户浏览器,每次都经过路由跳转到新页面,显然不能最大程度发挥客户端持续运行的优点,咱们但愿在保持前端模块化的基础上,在浏览器端有一个持续运行的框架优化用户体验,所以 Server Component 其实作了下面的事情:
<img width=550 src="https://img.alicdn.com/imgextra/i3/O1CN01gzaZNY1lBkGbGJKUy_!!6000000004781-2-tps-1332-760.png">
这样作有两大好处:
Server Component 尚未成熟,但其理念仍是很靠谱的。
想要同时实现 "用户体验、可维护性、性能",重后端,或者重前端的方案都不可行,只有在先后端取得一种平衡才能达到。Server Component 表达了一种职业发展理念,即将来先后端仍是会走向全栈,这种全栈是先后端同时作深,从而让程序开发达到纯前端或纯后端没法达到的高度。
2021 年国内开发环境依然比较落后,所谓全栈,每每指的是 “先后端都懂一点”,各端都作不深,难以孵化出 Server Component 这种概念。固然,这也是咱们继续向世界学习的动力。
也许 PHP 与 Server Component 的区别,就是检验一我的是真全栈仍是伪全栈的试金石,快去问问你的同事吧!
讨论地址是: 精读《React Server Component》· Issue #311 · dt-fe/weekly
若是你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
版权声明:自由转载-非商用-非衍生-保持署名( 创意共享 3.0 许可证)