关于前端:精读React-Server-Component

8次阅读

共计 8621 个字符,预计需要花费 22 分钟才能阅读完成。

截止目前,React Server Component 还在开发与钻研中,因而不适宜投入生产环境应用。但其概念十分乏味,值得技术人学习。

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

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

我会联合这些一手材料,与一些业界大牛的解读,零碎的讲清楚 React Server Component 的概念,以及我对它的一些了解。

首先咱们来看,为什么须要提出 Server Component 这个概念:

Server Component 概念的提出,是为了解决 “ 用户体验、可维护性、性能 ” 这个不可能三角,所谓不可能三角就是,最多同时满足两条,而无奈三条都同时满足。

简略解释一下,用户体验体现在页面更快的响应、可维护性体现在代码应该高内聚低耦合、性能体现在申请速度。

  • 保障 用户体验、可维护性 ,用一个申请拉取全副数据,所有组件一次性渲染。但当模块一直增多,无用模块信息不敢随便删除,申请会越来越大,越来越冗余,导致瓶颈卡在取数这块,也就是 性能不好
  • 保障 用户体验、性能 ,思考并行取数,之后流程不变,那么当前业务逻辑新增或缩小一个模块,咱们就要同时批改并行取数公共逻辑与对应业务模块, 可维护性不好
  • 保障 可维护性、性能 ,能够每个模块独立取数,但在父级渲染完才渲染子元素的状况下,父子取数就变成了串行,页面加载被阻塞, 用户体验不好

一言蔽之,在前后端解耦的模式下,惟一连贯的桥梁就是取数申请。要把用户体验做好,取数就要提前并行发动,而前端模块是独立保护的,所以在前端做取数聚合这件事,必然会毁坏前端可维护性,而这并行这件事放在后端的话,会因为后端不能解析前端模块,导致给出的聚合信息滞后,长此以往变得冗余。

要解决这个问题,就必须加深前端与后端的分割,所以像 GraphQL 这种前后端约定计划是可行的,但因为其部署老本高,收益又仅在前端,所以难以在后端推广。

Server Component 是另一种计划,通过启动一个 Node 服务辅助前端,但做的不是 API 对接,而是运行前端同构 js 代码,间接解析前端渲染模块,从中主动提取申请并在 Node 端间接与服务器通信,因为服务端间通信老本极低、前端代码又不须要做调整,申请数据也是动静按需聚合的,因而同时解决了 “ 用户体验、可维护性、性能 ” 这三个问题。

其外围改良点如下图所示:

<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 许可证)

正文完
 0