精读《React Server Component》

截止目前,React Server Component 还在开发与研究中,所以不适合投入生产环境使用。但其概念很是有趣,值得技术人学习。php

目前除了国内各类博客、知乎解读外,最一手的学习资料有下面两处:html

  1. Dan 的 Server Component 介绍视频
  2. Server Component RFC 草案

我会结合这些一手资料,与一些业界大牛的解读,系统的讲清楚 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">

如上图所示,这是先后端正常交互模式,能够看到,RootChild 串行发了两个请求,由于网络耗时与串行都是严重阻塞部分,所以用红线标记。

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 拥有几种让人眼前一亮的特性,都是纯客户端组件所不具有的:

  • 运行在服务端的组件只会返回 DSL 信息,而不包含其余任何依赖,所以 Server Component 的全部依赖 npm 包都不会被打包到客户端。
  • 能够访问服务端任何 API,也就是让组件拥有了 Nodejs 能拥有的能力,你理论上能够在前端组件里干任何服务端才能干的事情。
  • Server Component 与 Client Component 无缝集成,能够经过 Server Component 无缝调用 Client Component。
  • Server Component 会按需返回信息,在当前逻辑下,走不到的分支逻辑的全部引用都不会被客户端引入。好比 Server Component 虽然引用了一个巨大的 npm 包,但某个分支下没有用到这个包提供的函数,那客户端也不会下载这个巨大的 npm 包到本地。
  • 因为返回的不是 HTML,而是一个 DSL,因此服务端组件即使从新拉取,已经产生的 State 也会被维持住。好比说 A 是 ServerComponent,其子元素 B 是 Client Component,此时对 B 组件作了状态修改好比输入一些文字,此时触发 A 从新拉取 DSL 后,B 已经输入的文字还会保留。
  • 能够无缝与 Suspense 结合,并不会由于网络缘由致使连 Suspense 的 loading 都不能及时展现。
  • 共享组件能够同时在服务端与客户端运行

三种组件

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 */);
}

markedsanitize-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 直接在服务器本地发出请求。

运行时 Code Split

咱们都知道 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 查询,耗时几秒的状况下,这样作反而拔苗助长。

减小 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>

虽然在组件层面抽象了 NoteNoteWithMarkdown 两个组件,但因为真正 DOM 内容实体只有一个简单的 div,因此在 Server Component 模式下,返回内容就会简化为这个 div,而无需包含那两个抽象的组件。

限制

Server Component 模式下有三种组件,分别是 Server Component、Client Component、Shared Component,其各自都有一些使用限制,以下:

Server Component

  • ❌ 不能用 useStateuseReducer 等状态存储 API。
  • ❌ 不能用 useEffect 等生命周期 API。
  • ❌ 不能用 window 等仅浏览器支持的 API。
  • ❌ 不能用包含了上面状况的自定义 Hooks。
  • ✅ 可无缝访问服务端数据、API。
  • ✅ 可渲染其余 Server/Client Component

Client Component

  • ❌ 不能引用 Server Component。

    • ✅ 但能够在 Server Component 中出现 Client Component 调用 Server Component 的状况,好比 <ClientTabBar><ServerTabContent /></ClientTabBar>
  • ❌ 不能调用服务端 API 获取数据。
  • ✅ 能够用一切 React 与浏览器完整能力。

Shared Component

  • ❌ 不能用 useStateuseReducer 等状态存储 API。
  • ❌ 不能用 useEffect 等生命周期 API。
  • ❌ 不能用 window 等仅浏览器支持的 API。
  • ❌ 不能用包含了上面状况的自定义 Hooks。
  • ❌ 不能引用 Server Component。
  • ❌ 不能调用服务端 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 VS Server Component

其实在 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">

这样作有两大好处:

  1. 兼顾了 PHP 模式下优点,即先后端代码无缝混合,带来一系列体验和能力加强。
  2. 先后端仍是各自模块化编写,图中红色部分是随前端项目总体打包的,所以开发仍是保留了模块化特色,且在浏览器上还保持了 React 现代框架运行,不管是单页仍是数据驱动等特性都能继续使用。

总结

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 许可证
相关文章
相关标签/搜索