Facebook 的 React Team 考试测验给出了他们的一个探索方案——React Server Component。一种只运行在做事真个 React 组件化能力,我们来一探究竟。
一、怎么开始资源
Introducing Zero-Bundle-Size React Server Components
官方Blog里供应了三个关于 React Server Components 的资源:
1)一个可以运行的 full stack demo 项目。项目可以直接在本地运行,但是须要进行一些数据库配置。如果有docker环境的话,建议在docker里运行,可以避免手动配置环境。
2)RFC。React 团队 一贯采取 RFC 的办法来帮助和辅导 React 的设计。重大变更在经由社区彻底谈论后才会合并到 React 的稳定版本。
3)一个近一个小时的视频。阐明了为什么要开拓 React Server Components 和 对上述 demo项目的演示。有精心翻译的中笔墨幕。
解释
与 React team 的做法一样,这里也推举大家先去看这个视频,先对 React Server Components 有个整体的认识。当然直接看本文也是可以的。
现在 React Server Components 仍处于开拓状态。暂时不适宜深入利用,也不适宜基于这个新特性去开拓升级自己的框架。
二、根本认识组件类型
1)server component。不能包含客户端代码,如利用 DOM api、 useState、useEffect等。
2)client component。现在大家所熟习的普通组件。
3)share component。既能作为 server component, 又能作为 client component,取决于引用该组件的组件。常日是根据 props 直接渲染的组件。
组件命名
1)server component:扩展名 .server.js
2)client component:扩展名 .client.js
3)sharing component:扩展名 .js
这个命名约定不是终极的方案,只是目前快速开拓原型时的大略单纯策略。
组件引用
三种组件之间相互引用只有一个限定:客户端组件不能 import 做事端组件。别的情形下,都可以相互引用。
三、运行机制官方例子
上图展示的是官方供应的demo的侧边栏。包含 Header、SearchInput、NoteList 等组件。个中赤色为做事端组件,绿色为客户端组件。
看到这里,大家可能会困惑,这种交错嵌套的组件是怎么在不同环境下渲染并且拼接到一起的。下面我们通过更大略的例子来阐明。
大略的例子
为了便于阐明,我们来看一个大略的例子。Test 是一个普通的客户端组件,App 是做事端组件,App 组件中利用了 Test 组件。
// Test.client.jsexport default function Test({text}) { return <div className="client">{text}</div>}// App.server.jsimport Test from "./Test.client"export default function App() { return <div className="main"> <Test text={"props to Test.client.js"} /> </div>}
当向做事器要求全体组件时,做事器的相应如下:
M1:{"id":"./src/Test.client.js","chunks":["client7"],"name":""}J0:["$","div",null,{"className":"main","children":["$","@1",null,{"text":"props to Test.client.js"}]}]
比拟做事器的相应以及组件的编写形式,我们可以清楚地看到以下三点:
1)M-id(M1): 表示的是对一个客户端组件的描述。个中 id 为该组件在项目中的路径,可以用来唯一标识这个组件;chunks 是 webpack 打包后的 chunk。
2)J-id(J0): 表示做事端组件渲染后的结果。大家很随意马虎把稳到,这个形式与 React.createElement 返回的结果是高度吻合的。这当然不是巧合,由于这个 JSON 所描述的正是组件 Render 后的结果。
3)在 J0 对应的这段 JSON 中, 有一个标识 @1 。这个标识是对客户端组件 M1 (Text组件)的引用。{"text":"props to Test.client.js"} 是通报给 Text 组件的 props。
因此,我们可以得出一个不那么正式的总结:从根做事端组件开始,只管即便渲染它能渲染的内容,当碰着原生组件 (divs, spans等) 或者客户端组件时,停滞渲染。原生组件在客户端会被直接渲染成 DOM , 而客户端组件在客户端会以大家熟知的办法被解析渲染。至此,一个完全的 React 组件在客户端被完全拼接,从而渲染出一个完全的页面。
客户端组件中 "利用" 做事端组件
上述大略的例子阐明了做事端组件中怎么利用客户端组件。但是通过官方例子的图(赤色为做事端组件,绿色为客户端组件)我们可以看到,是有做事端组件被“包围”在客户端组件里的。
通过前文的组件引用小节,我们知道做事端组件是不能被客户端组件直接 import 利用的,由于这会导致做事端代码泄露以及发往客户真个 js bundle 变大。但是上述例子中却涌现了这种情形,我们不妨从官方 demo 的代码中找一下答案。
代码如下,个中 ClientSidebarNote(绿框部分)由于要包含展开详情的交互,所以是客户端组件。SiderBarNoteHeader(红框部分)只是大略的展示,因此被设计成了做事端组件。
// SiderbarNote.jsimport ClientSidebarNote from './SidebarNote.client';import SiderBarNoteHeader from './SiderbarNoteHeader'export default function SidebarNote({ note }) { const summary = excerpts(marked(note.body), { words: 20 }); return ( <ClientSidebarNote id={note.id} title={note.title} expandedChildren={ <p className="sidebar-note-excerpt">{summary || <i>(No content)</i>}</p> }> <SiderBarNoteHeader note={note} /> </ClientSidebarNote> );}
通过代码我们可以看到,实际上客户端组件并没有直接 import 做事端组件,而是把做事端组件作为客户端组件的 Children。
这里涉及到了一个很关键的点,Lauren Tan 在视频中也着重强调了这一点:在做事端组件中利用 JSX 作为通报给客户端组件的 props 时,这个 JSX会被在做事端渲染,然后再返回给客户端。如 expandedChildren 和 <SiderBarNoteHeader note={note} /> 都会在做事端被渲染。
因此我们才可以看到上图中客户端组件“包含”做事端组件的情形。
四、流协议场景
考虑一个场景,当做事端某个接口被 block 时,我们会面临一个问题:在做事端,部分组件已经渲染完成,而某个组件被 block,这会导致整体被 block 。
如下图展示,在网络良好的情形下,左侧的 noteList 和 右侧的 note 详情会很快展示出来。
但是当网络不好时,右侧获取 note 详情的接口很慢。我们希望全体页面能够按照下图的办法运行,即没准备好的组件轻微再返回,先展示一个骨架屏。
事理和验证
React team 对这个问题供应理解决方案:streaming protocol。我们类比前面做事端组件引用客户端组件的例子。
做事端组件引用客户端组件时,做事端组件创造无法处理客户端组件,于是把客户端组件的处理延迟到客户端实行,并打上tag。客户端渲染好客户端组件后,把渲染后的结果添补到 tag 的位置。做事端组件引用被block的做事端组件时,做事端组件创造暂时无法处理被block的组件,于是暂时放弃被block的组件,并打上tag,把渲染好的结果返回到客户端。在被block的组件准备好后,再次返回数据到客户端,添补到 tag的位置。从而实现渐进式的渲染。下面代码中,合营利用Suspence实现了上述的渐进式渲染。App 是根组件,利用了 Delay 组件。Delay 组件被 block 了5秒。
// Delay.server.jsexport default function Delay() { fetch('http://localhost:4000/sleep/5000'); return <div>I am delay</div>}// App.server.jsimport Delay from "./Delay.server"export default function App() { return <div className="main"> <Suspense fallback={"loading"}> <Delay /> </Suspense> </div>}
如果利用流的办法来读取做事器的相应,我们会得到以下结果,个中 @2 是 J2 的占位符。
======== chunk 1 ==========S1:"react.suspense"J0:["$","div",null,{"className":"main","children":[["$","div",null,{"children":"123"}],["$","$1",null,{"fallback":"loading","children":"@2"}]]}]======== chunk 2 ==========J2:["$","div",null,{"children":["I am delay ",5000]}]
五、动机
原始动机
Dan 在视频中提到:React Server Components 最初是用来办理组件渲染时 client 与 server 须要多次通信的问题的。
这可能会让人遐想到 React SSR 或者 GraphQL,又或者是最初的 JSP/ASP 时期。这个会在后面进行详细的比拟。
设计目标
在明确了把 React 移动到做事端这个方案后,React团队也进一步明确 React Server Components 的设计目标。以下是 RFC 中提到的设计目标:
零打包体积的组件。如果一个组件是做事端组件,那么这个组件将不会涌如今终极发往客户真个 js bundle 中。对后真个完备访问能力。 由于做事端组件只会运行在 server 上,因此可以在做事端组件中调用任何的做事端API,而不用做环境判断。自动代码分割。在做事端组件中引入客户端组件,那么客户端组件会被自动分割成小的chunk。No Client-Server Waterfalls。利用做事端组件在做事端多次获取数据时,都是做事器间的通信(例如:node server 和 java server),内网通信速率 比 client-server 通信速率快很多,因此可以大大提升整体的效率。Avoiding the Abstraction Tax。这个描述可能有些抽象,于是官方给出理解释和例子。像 Angular/vue 这种基于模板的 UI 框架,会利用类似于 AOT 的技能对开拓者写出的组件进行一定程度的优化,但是 React 是利用 JS 来描述组件的,因此很难去优化。利用了做事端组件后,可以在一定程度上去优化 React 中的抽象。例如:不管一个组件被写了多少层Wrapper,终极发往客户真个都是终极的 HTMlElement。// Note.server.jsimport NoteWithMarkdown from "./NoteWithMarkdown.server"function Note({id}) { const note = db.notes.get(id); return <NoteWithMarkdown note={note} />;}// NoteWithMarkdown.server.jsfunction NoteWithMarkdown({note}) { const html = sanitizeHtml(marked(note.text)); return <div ... />;}// client sees:<div> <!-- markdown output here --></div>
不同的寻衅,统一的办理方案。Web开拓领域长期存在一个问题:是利用 “瘦”客户端还是 “胖” 客户端。React 团队认为要同时利用做事真个能力与客户真个能力,同时利用做事端组件和客户端组件许可开拓者用同一种措辞、同一个框架来利用这两种能力。这可能会让人再次想起 SSR,下面一节我们会解释 React Server Components 与现有干系技能的差异和联系。六、与现有技能的关系
SSR
SSR 用于加速首屏的渲染,在要求页面时实行一次,多次要求时,会导致上一次要求渲染的组件和状态全部丢失。暂时不支持数据分批次返回进行渐进式渲染。
React Server Components 可以反复被要求。一次要求就像一次 rerender, 只不过部分事情被分配给 server 做了。乃至有人提出,server components 的 server 不一定是 web server,也可能是 web worker。支持通过 Streaming data 做渐进式渲染。
但是二者并不冲突,我们可以同时利用 SSR 和 React Server Components。
GraphQL
GraphQL 也是 Facebook 的产品,同样是用来一定程度上办理 client-server 多次通信的问题的。目前合营 relay 和 GraphQL 可以做到数据获取代码分散在组件间,终极合并成一个大的 GraphQL Query,通过一次 http 要求获取全部数据,从而达到减少通信次数的目的。但是并不是所有团队都会去利用和接管 GraphQL,因此 React 团队希望利用 React 自己的生态去办理这个问题。对付没有 GraphQL 的环境,React Server Components 是一种替代方案。其余,Facebook在利用 React Server Components 时,会在做事端组件中去调用 GraphQL。
JSP/ASP
React Server Components确实会让人想起来曾经的 JSP/ASP,由于二者都会在做事端进行模板与数据的绑定。但是 React Server Components 实际上是 Partial 页面更新的技能,多次触发路由不会重新渲染全体页面,客户端组件的状态会被保留。
七、未来展望可降级的 react server components。当做事器压力太大时,有办法降级为普通的客户端渲染。目前精良的 SSR 方案都支持通过配置来决定当前要求要不要进行 SSR。React Server Components 适宜的场景还须要最佳实践来确定。可以在一定程度上减少 js bundle size 这个是一定的结果,但是反复要求 Server Components 时多次返回某个相同的 UI 片段的问题的办理方案还须要进一步磋商。八、总结React Server Components 是 React 团队让组件从以客户端为中央到不同的寻衅、统一的办理方案的一次考试测验。React 在做事端可以快速天生静态的内容,在客户端可以构建丰富的、交互式的页面。实际上在这些方面,很多人也在探索,像 Hotwire 的 partail html 技能;微软的 Blazor server 更是早就走向另一个极度:所有状态掩护在做事器上,客户端只是渲染器和事宜触发器。
就像C措辞之父说的一样,“只要设计得当,添加新特性是很自然的事。这种做法是艰巨的,但是仍在取获胜利”。就目前来看,React Server Components 是一个不太被看好的特性,离设计得当还有些间隔,但这种探索还是值得肯定和支持的。
【作者简介】Sprite 82,喜好研究各种措辞对 web 框架的实现,函数式编程的爱好者,最近在编译事理前端中验证学习到的函数式编程知识。