2020 年底,React 公布了一个全新的特性:Server Components,当时它还处于调研和试验阶段,并没有正式发布,随着 React 18.0 版本的正式发布,Server Component 的脚步声也越来越近了,不出意外的话,应该会在今年的某个 React 18 的 minor 版本中正式发布。
Server Components 听起来好像并不那么激动人心,React 18 所发布的各种特性也似乎平平无奇,自从 Hooks 面世已经三年多过去了,React 似乎停滞了前进的脚步,只是在现有的基础上做些小修小补?
No。
Concurrent rendering(React 18 新带来的特性)是一种本质上的改变,它本身不像 Hooks 那样对开发体验有着近乎翻天覆地的变革,但是这种底层渲染能力/机制的调整,会带来非常非常多的可能性,例如:
Suspense、OffScreen、Server Components
这三种特性,目前都没有生产可用,但是等到未来他们正式发布并渐渐被大面积使用时,每一项特性都会带来非常显著的开发体验的提升。而如果让我从这些未来会出现的新特性中选一个最期待的,那毫不疑问会是 Server Component。
所以,Server Components 到底是什么?他会像当年的 Hooks 一样对整个 React 生态带来巨大的影响么?在我们回答这些问题之前,很有必要先解释一下 Server Components 是什么,又解决了什么问题。
注:下文中的很多内容受 Dan 和 Lauren 的这份演讲视频[1]所启发,如果你想更深入的了解即将到来的 React Server Component,那么非常推荐这段视频 事实上,这篇文章并不是一份对 Server Components 的用法教学,也不会涵盖 Server Components 的每一处细节(甚至为了方便表述会有意地略过一些细节),因此,在读下文之前,最好是对 Server Components 已经有所了解
“前后端分离”是当下主流的 web 研发模式,后端存储数据,并把对数据的操作(增删改查)封装成接口,通过后端服务提供给前端,前端应用发送请求(例如 http 请求或者 rpc 请求)去调用后端提供的接口,从而获取到数据或者是对数据进行修改。
这可能是十几年以来非常普遍的研发模式了,也因此,我们被区分成前端开发和后端开发,各自负责着“楚河汉界”的一侧。我们在各自那一侧都做了非常多的优化、创新、突破,在后端,我们有容器化、微服务、SSR,在前端,我们有 code spliting、前端路由、React Hooks。
但是对于 API 层,我们似乎这么多年以来都未曾有过关注,即便是有,也仅仅是停留于 API 传输性能(例如 grpc)、API 的存在形式(例如 Restful 和 GraphQL)、API 的工程化管理(例如 Postman)。
并非是想说 API 一个邪恶而糟糕的设计,但是自从 Restful 的概念被提出以来,已经 22 年过去了,我们是不是应该在现在重新思考一下:
以网络请求作为前后端的分界是最优解吗?
如果没有 API,我们该如何架构和开发 Web 应用?
让我们再回到刚刚的那张图,考虑一下 API 在带来职责分工明晰之外,同时也带来了哪些问题。
就像 Remix[2] 首页上所展示的,基于 API 和嵌套路由的前端站点,在请求时会出现瀑布流的现象:
数据的之间可能是有前后的依赖关系,抑或是和组件强耦合在一起,需要等待组件的 bundle 加载完成之后才能发出请求,这些都导致了请求瀑布流现象的出现。
后端希望实现小而美的接口,每个接口有独立的职责,例如:
getUser 获取用户信息
getSongs?page=12 获取歌曲列表
getNotifactions 获取通知列表
getFavoirateSongs 获取收藏的歌曲
getNewSongs 获取新发布的歌曲
getRecommendSong 获取今日推荐的歌曲及对应的文案
getSearchBarHotKeywords 获取热门的搜索词
getAdBanner 获取广告 banner 内容
getRecentSongs 获取最近听歌记录
getRecommendedPlayList 获取推荐的歌单列表
……(实在太多了)
每一个接口,单独拿出来看都是合理的,但是放在一起,就会发现用户每次打开这样一个音乐 web app,都要发送至少十几个接口,对于一些稍微复杂一点的网页,首次加载就需要请求几十个接口也丝毫不奇怪。
每一个接口的请求,都会带来网络开销,甚至在有些环境下会有最大并发请求数量的限制(例如在支付宝客户端那的 rpc 请求),或许网络层的 automatic batching 可以解决这个问题,但是遗憾的是,在目前的技术体系内,这个问题并不好解决(这里没有写不能解决,是因为的确有一些可行的方案,例如 BFF、依赖网关来做接口聚合,但它们都引入的新的问题)。
包体积已经是“现代”前端开发领域饱受诟病的一点了,动辄几百 k 的 js 文件,似乎已经背离了浏览器是用来“浏览”网页的初衷了。并不是说我们都要做一个浏览器原教旨主义者,但是如果网页能够在不损失用户体验和开发体验的前提下,恢复到非常轻量和快速的状态,难道不是一件好事么?
在我个人看来,这是大型项目或需要长期维护的应用中最令人头疼的问题了。
假设我们现在有一个非常巨大的应用,需要有十几位开发者共同编写和维护,那如何分工?答案必然是先做模块化,我们把整个应用拆分成几个彼此尽量独立的模块,再由每个人或每几个人负责其中的一个模块。模块化带来的好处是边界清晰(看到一个需求就能判断出来涉及到哪个或哪些模块做哪些改动)、职责明确(每个人都有自己确定的职责)、减少沟通成本(由于模块内部的逻辑是封闭的,不需要外部感知,所以可以降低沟通成本)。
对于前两点,目前的前后端分离架构都还是及格的,但对于第三点,我觉得基于网络请求接口的协作模式,在很多情况下并没有有效地做到逻辑内部封闭、减少需要前后端之间来回沟通的信息量。
举个例子,对于这样的一个页面:
看起来非常简单,一些信息的展示,加上一个充值按钮,这就是我最开始所设想的。
然而,随着这个项目不断的推进,我发现,原本以为是纯静态的标题文案,实际上是需要后端控制的,根据当前用户的所属人群来动态判断文案内容;我发现,由于前端金额计算的可靠性问题,折扣和实际支付相关的内容都是需要在后端预处理之后展示在前端的;我发现,倒计时的参考时间是需要依靠后端返回的;我发现,按钮的文案、点击行为,是需要后端控制的,特别是按钮的点击行为,最终方案是后端返回一个枚举,前端根据这个值来 switch case 一下走不同的逻辑(例如下单、引导先进行注册和绑卡)……
为了阅读体验,我只是列举了其中随手想到的一小部分,如果总结一下,那就是,后端和前端并没有因为“前后端分离”而做到解藕,反倒是藕断丝连,剪不断理还乱。后端感知了过多的前端视图层逻辑,就像是发明了一套 DSL(Domain Specific Language),而前端则是要写一个针对这套 DSL 的解析器和渲染器。
回到我们刚刚提到的,模块化带来的好处。模块化能够降低沟通成本,有一个不可忽略前提,就是架构的合理性。模块化并非是降低沟通成本的本质原因,也并非所有的模块化实践都能带来沟通成本的降低。当前后端分离的实践成为一个僵硬的、死板的“规范”,那它还能真正起到多少降低沟通成本的作用?一个大大的问号。
再次申明一下,下文是假设读者朋友已经对 Server Components[3] 有所了解
基于网络请求的 API 模型,有一个大大的前提假设,就是前端应用和后端应用是两个独立的应用,但是为什么一定要是这样?
或许我们可以让后端应用直接渲染 HTML,用户操作时,重新渲染一遍页面?这其实就是在 Restful 时代之前的架构,有很多弊端,特别是可交互性差,不然也就不会出现后来 Restful 的盛行了。
那再或许,我们可以让前端的 React 组件,运行在后端?
这就是 React Server Components。
一图胜千言,在现在的前后端分离模式下,后端提供接口,前端的 React 组件调用接口。
而如果后端可以运行 React 组件,直接渲染 React 节点树到前端,就不需要所谓的 API 的概念了。
后端运行 React 组件并不是什么新鲜事,我们在 SSR(Server Side Rending)早就习以为常了,但是需要特别注明的一点是,在 SSR 中,后端是运行了 React 组件,生成了一份初始状态的 html,但这份 html 是没有可交互性的,它只是为了让用户能尽早看到页面而做的一种改良式的、修修补补一样的优化。
而 Server Components 所带来的,是我们可以把同一个项目中,一部分的组件作为 Server Components,另一部分组件,作为 Client Components,因此我们可以既享受到后端内部调用带来的便捷、可维护性,又能保证页面的可交互性几乎没有任何妥协。
如果你用过 PHP 或 Django,那你肯定非常熟悉这种模式:后端直接渲染 html 内容,浏览器只负责显示,用户点击按钮,那就重新请求、重新渲染页面,如果页面上需要一些复杂的动态交互,比如让用户可以把一个列表展开/收起,或者是点击某个按钮之后展示一个模态框,那可以借助于 jQuery 来实现。
PHP + bootstrap + jQuery,现在,Server Components 就像是这套范式的升级版,可以被称为一种全新的“全栈”开发模式。
因为是在后端环境下,这些 Server Components 可以使用全部的后端能力,不管是中间件,还是其他后端微服务的调用,甚至是 db 的访问(当然可以直接跑 SQL,但是更好的实践是通过一个数据中间层),都可以实现。这样一来,我们就可以直接把数据从源头获取,放到 React 组件的上下文中,那自然就不需要传统意义上的 API 了。
更准确的说,API 并未消失,我们其实也不会和 API 就此说再见,而是让它换了一种形式。 有模块化的地方,就会有 API,Restful 的 http 网络请求固然是 API,但中间件暴露出来的方法,浏览器提供的 Date 对象,node 提供的文件读取函数,db 提供的 SQL,这些全都是 API。在这种新架构下,API 变成了后端里业务应用和上游服务之间的调用,变成了 Server Components 和 Client Components 之间的 props 传递,前者让 API 变得更加干净、更符合单一职责的原则,而后者让 API 变得自然到你几乎感知不到。
所以:
Server Components 允许我们不再按照 前端 - 后端 进行模块的拆分,而是依照 业务应用 - 底层服务 来进行更合理的模块拆分。从而可以理论上降低模块之间的沟通成本(因为目前还没有办法实践证明)。
由于 Server Components 是在后端运行组件,直接通过网络传输给前端进行渲染,因此很多大体积的包(例如 markdown 渲染、html sanitize)都不需要在前端下载和运行,从而很大程度上降低包体积。
由于底层 db 或上游服务的调用都是发生在后端内部的,因此即便出现并发请求,所带来开销也远远小于前端并发调用后端的 Restful API。
同理,请求瀑布流的问题也会因为调用开销降低而消失或减轻。
如果大胆想象一下的话,未来的研发模式可能这样的:
开发者将不会再区分前端和后端,而是区分为业务应用开发和上游服务开发。现在的后端开发将(真正地)不再需要关注视图逻辑,只聚焦于底层业务逻辑,为前端提供清晰好用、原子化的服务/接口;而现在的前端开发将会拓展到横跨前端和后端(代码运行环境上),负责的是在后端封装好的一个个原子化的底层能力上,构建视图层,而我们也需要一套全新的框架和基础设施,来适配 Server Components。
目前,Server Components 还没有正式发布,而即便正式发布之后,也还有长长的工程化落地的路要走,Server Components 增加了很多额外的限制,server、client、shared 的区分也可能会带来一些理解成本。缓存、性能、server 重新渲染时的增量更新策略、发布时的可灰度性和可回滚性、业务中边界情况的处理,还有很多的问题需要去解决,还有很多的未知尚未被验证。
[1]
演讲视频: https://www.youtube.com/watch?v=TQQPAU21ZUw
[2]Remix: https://remix.run
[3]Server Components: https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html