从React SS-AARGH!到React SS-AH!?
在Experts Marketplace,我们一直在思考如何为商家和Shopify专家改善平台,无论是通过引入新功能还是重新思考我们的基础设施。在2021年第二季度,我们做了一个巨大的技术赌注,为整个市场采用React服务器端渲染。该平台已经完全采用React构建,但我们都没有涉足过React服务器端渲染。我们最初的研究发现的信息少得令人吃惊。现有的资源要么是太高层次的,没有可操作性,要么是太具体,没有相关性。
事实是,每个人的设置和进展都是独一无二的。在这篇文章中,我整合了我们的发现和经验,创建了一个React服务器端渲染的蓝图。无论你是从头开始,还是迁移现有的React应用,或者试图采用混合方法,希望你能在走的时候知道你错过了什么,以及要研究更多的东西。如果你想在不同的设置上发挥创意,我希望这篇文章也能给你一些想法。
React SSR和它的好处
在我们深入研究React服务器端渲染(从这里开始称为React SSR)之前,值得简单讨论一下它是什么以及它的好处。React是一个客户端的JavaScript框架,传统上是在浏览器中执行。一个传统的React应用通常涉及到服务器发回一个巨大的JavaScript包和一些轻量级的HTML支架。当浏览器收到完整的JavaScript时,它会执行其中的React代码,将HTML支架转化为可交互的内容。React SSR在这个工作流程中引入了一个小的转折--服务器不是送回一个JavaScript包和HTML支架,而是送回一个JavaScript包和完整的HTML,由服务器渲染React代码的第一遍产生。
在商业利益方面,与传统的客户端渲染的单页应用程序(SPA)相比,React SSR带来了巨大的SEO价值。你的内容更容易被消化,更受搜索引擎的青睐,因为搜索爬虫不需要把稀缺的计算能力用于渲染你的页面。
在工程方面,React SSR应用程序被认为是Isomorphic JavaScript(又称通用JavaScript)。这意味着你在为客户端和服务器端编写JavaScript,在高层次上有助于减少逻辑重复,提高生产力。除此之外,React SSR还为各种高级优化技术打开了大门,以大幅提高网络性能(例如,提供预渲染的静态HTML)。
React SSR蓝图
让我们从基础开始,让我们站在同一起跑线上。一个传统而简单的React应用设置包含两个组件:React客户端代码和服务器。当浏览器向服务器发出HTTP请求时,它会将React代码连同HTML脚手架一起以JavaScript捆绑的方式发送回来。
当浏览器收到这两样东西时,它就会执行React代码,并在脚手架上构建交互式用户界面。浏览器上的React前端可以通过GraphQL与服务器进一步沟通。当用户在应用中导航时,React会处理所有的状态和路由变化,而浏览器不会进一步向服务器发出页面刷新的HTTP请求。如果你有一个更可扩展的设置,你可以让两个不同的服务器分别为HTTP请求和GraphQL请求服务。
一个简单的React应用设置
有了React SSR,由于服务器需要对React进行第一次渲染,并将产生的 "初始状态 "HTML发送回来,我们需要以某种方式为服务器添加执行JavaScript(因此是React)的能力。
如果你的网络服务器是用NodeJS或NextJS编写的,那么你很幸运。这些框架可以执行JavaScript,所以添加在服务器端渲染React的能力是一个相对简单的过程。在这种情况下,你可以简化React服务器的安排,我将在下面解释。
如果你没有使用一个可以原生执行JavaScript的服务器框架,你需要为你的服务器添加一个副手,其工作是专门按需渲染React组件(通常是通过使用 ReactDOMServer).这听起来和浏览器所做的完全一样,但不同的是,这是一个在NodeJS环境中运行的服务器。这个区别是很重要的,我将在后面讨论。
一个简单的React SSR设置
在这个设置中,当服务器收到一个React页面的HTTP请求时,它要求React服务器进行渲染,抓取渲染后的HTML,并将其与React包一起返回给浏览器。从那时起,客户端的React处理进一步的路由和状态变化,很像简单的React应用设置。
如果你正在将一个现有的应用程序迁移到React SSR上,或者你处于其他一些独特的情况下,那么你可能最终会处于一个混合设置。在这种状态下,你的一些路由是由NodeJS在服务器端渲染的,而其他路由是在客户端渲染的。你可能会遇到一些挑战,使应用程序的行为与服务器渲染和客户端渲染相同,但混合设置在其他方面是完全可用的。
有几件事你可能想进一步研究,以使管道工作,下面我将简要地提到它们。
我是否需要改变前端软件包?
事实上,你需要。React,默认情况下,在一个HTML容器中进行渲染,根本不关心容器中的内容。为了确保浏览器的React利用服务器渲染的HTML,而不是盲目地替换它,我们需要告诉React对它进行水化(保留标记并附加事件监听器)。你可以在官方文档中阅读更多关于这一行为的内容。
Web服务器到底是如何与React服务器进行通信的?
一般来说,在这种情况下,网络服务器是作为一个 反向代理:一种代理服务器,代表客户从一个或多个服务器检索资源。假设你使用的是一个著名的服务器框架,有可能会有很多关于设置反向代理的资源/库。
如果我需要从Web服务器向React服务器传递数据怎么办?
这确实是一个非常常见的使用场景。你可能想把一些应用状态传递给React服务器,让它根据这些参数来渲染HTML。这方面的例子包括用户认证状态、要渲染的路由、地域和其他静态应用配置值。
一个简单的方法是通过HTTP头中的自定义字段来实现,但也可以随意发挥创意。如果你使用HTTP头,请记住,不同的服务器框架可能对它有不同的大小限制。确切的大小限制可能不容易找到,但8KB似乎是一个安全的赌注。请不要通过标头附加大型或动态数据--这些应该通过GraphQL完成。
如果React服务器在渲染过程中需要进行Graphql调用怎么办?
如果React服务器像浏览器一样渲染HTML,那么它在这个过程中需要进行GraphQL请求是非常正常的。而且就像浏览器一样,GraphQL请求也会转到你的Web服务器。这是有问题的,因为网络服务器刚刚向React服务器发出了请求,并且还在等待得到渲染后的HTML。这个GraphQL请求导致了服务器之间的循环调用,并产生了一个死锁。
在前面谈到简单的React SSR设置时,我们提到,更多可扩展的服务器架构可能包括不同的服务器来处理HTTP和GraphQL请求。如果你有这样的架构,那么就不用担心,死锁的情况不会发生在你身上。否则,这就是提示你安排一个专门的GraphQL服务器。
假设你现有的网络服务器同时处理HTTP和GraphQL请求,那么做出这种改变并不像你想象的那么难。只需旋转一个你的网络服务器的精确副本,并配置你的云平台的路由规则,将GraphQL请求路由到那里。就可以了。
一个React SSR设置,包括GraphQL服务器
简而言之,你需要一个额外的NodeJS服务器来在服务器端渲染React,一个额外的Web服务器来处理GraphQL请求,以及它们之间的管线。
改变你的开发习惯
建立架构和连接以支持React SSR是一回事,但为React SSR实际构建是另一回事。由于你在一天结束时仍在编写React,在大多数情况下,应用程序应该只是工作。然而,在NodeJS环境中执行的代码与在浏览器环境中执行的代码相比,确实有某些细微差别。以下是其中的一些。
确保渲染输出是确定的,并且不受环境影响
无论一个组件被渲染多少次,或在哪种环境下,如果状态是相同的,输出应该保持不变。非决定性行为的例子是当组件显示生成的随机字符串、时钟或用户代理时。
当这种情况发生时,服务器渲染的输出可能与浏览器拾取的客户端期望的内容不同。这可能会导致内容不匹配的警告。
useEffect 钩子中的任何逻辑都不会被服务器执行
不执行的逻辑是因为服务器只做了一个浅层的渲染周期,不会一直重新渲染标记,直到它稳定下来。因此,在 useEffect钩子中的逻辑在渲染后被触发,不会被执行。这对SSR来说通常不是问题,因为当前端React对应用进行水化时,它会执行useEffect 钩子,并根据需要更新DOM。此外,你可以通过在钩子内运行任何浏览器特定的代码来利用这一行为的优势。
作为一个附带说明。 useLayoutEffect会促使React的服务器端渲染器发出更明显的抗议。请阅读这篇文章,useLayoutEffect和SSR,了解更多信息。
尽可能地消除对浏览器API的依赖性
浏览器API指的是浏览器环境中一些常见的全局对象。一些常见的例子包括:window ,document ,和fetch 。虽然这些API在某些情况下很方便,但它们在NodeJS环境中是不可用的。试图在服务器端渲染包含这些引用的组件时,会出现 "未定义 "错误。
即使是一个看似无辜的引用也会导致整个SSR应用崩溃。比如说。
在上面的代码中,违规的window 引用是在utilities/myFunct1.ts 。它没有被任何人使用,甚至没有被导入,但整个SSR应用程序仍然会崩溃。这是因为myFunct1 在utilities/index.ts 的导出树中(被ssr/app.tsx 引用),这意味着myFunct1 中的所有函数定义和导出语句都被评估,包括window 引用。
作为一个替代方案,当你发现自己想要访问浏览器API时,可以考虑使用React抽象选项。一个常见的例子是用useLocation 钩子代替window.location (如果使用React Router),用useQuery 钩子代替fetch (如果使用 Apollo GraphQL)来调用GraphQL API。或者,你可以将浏览器API引用包装在保证只在浏览器上执行的函数中,比如事件处理程序和useEffect 钩子。
异步导入引用浏览器API的包
根据上面的观点,你可以努力避免或推迟在自己的代码中引用浏览器API,但如果一个导入的包在其全局范围内直接引用它,怎么办?简单地导入该包会使SSR应用程序崩溃,而你无法控制其内部逻辑。
一个解决方案是异步导入违规的包,并且只在客户端使用它。例如,在事件处理程序中导入该包,或者在useEffect 钩子中导入视图逻辑。下面是显示导入的好坏方式的示例代码片断。
避免依赖服务器注入的逻辑
如果你来自一个简单的React设置,你可能会让Web服务器返回更多的东西,而不仅仅是HTML支架,打算在客户端保持React应用程序。一些常见的情况包括,但不限于,注入的HTML元字段,额外的脚本,和序列化的全局JavaScript变量。
由于NodeJS服务器现在生成了标记,如果它不能处理任何常见的情况,就会出现问题。尝试将尽可能多的逻辑带入React的领域,这样NodeJS服务器就可以处理它们。例如,使用React Helmet来管理HTML元字段。
此外,避免将全局JavaScript变量序列化到window 或global 对象中,原因与我们消除对浏览器API的依赖性相同。如果你需要向React应用提供全局变量,那么 useContext钩子是一个很好的工具。
额外的代码质量绊脚石
在读完上面的问题清单后,你可能会有点紧张,说:"天哪,访问浏览器的API可能会破坏整个SSR应用,也可能是完全合法的!"。如何才能在允许合法使用的情况下抓到滥用呢?"有一种工具可以让开发者做到这一点,这就是所谓的测试套件。
不幸的是,当涉及到React SSR时,某些测试不再能提供良好的覆盖。一个令人痛心的例子是单元测试。前端单元测试通常是在模拟浏览器环境的NodeJS环境中运行的,要让它们覆盖实际的NodeJS服务器进行SSR的行为可能是一个令人头痛的问题。另一方面,服务器单元测试只包括请求和响应之间的单一环节。除非你能同时启动Web服务器和NodeJS服务器来运行服务器单元测试(在这一点上,它还是一个单元测试吗?),否则你在覆盖SSR方面是不走运的。
现在可能是你考虑端到端测试的时候了,这是最终的测试程序,因为它涉及到旋转整个应用程序,并运行一个无头浏览器与应用程序的前端进行交互。它涵盖了一个用户会话的整个应用程序的生命周期。在SSR期间的任何错误都会在这里被发现。作为一个例子,Gleb Bahmutov的文章谈到了使用Cypress的SSR测试,这是一个端到端的测试框架。
值得注意的是,端到端测试通常是缓慢和资源密集的。如果你将其用于持续集成,有一个大型的应用程序,并且担心部署速度,可以考虑只覆盖最关键的用户路径。
实现React服务器端渲染不是一件简单的事
根据一些一开始就熟悉服务器端渲染HTML的老派开发者的说法,这个世界已经转了一圈。如果你发现React SSR的无数好处与你的目标一致,那么它就值得追求。我希望这篇文章为你的努力描绘了一个高层次的蓝图,而且你现在知道该往哪个方向进一步发展了。
作为最后的总结,你需要以下的建筑部件和相关的背景。
-
一个执行React的NodeJS服务器
-
一个反向代理,让你的Web服务器与NodeJS服务器对话(除非Web服务器就是NodeJS服务器)。
-
一个单独的GraphQL服务器,为GraphQL请求提供服务
-
连接上述组件的管道
-
对你的开发理念进行调整
-
严谨地使用浏览器的API
-
把一切都放在React中
-
端到端测试作为额外的代码质量绊脚石
他的名字叫Rex Su
有什么是这个人做不到的吗?
他的伙伴是一只叫达西的猫
他是个可爱的人,也是个活跃的人
当他庆祝的时候,他用蛋糕来做
他并不害怕牺牲它来换取肚子的疼痛。
我们一直在寻找人才,我们很想听到你的声音。请访问我们的工程职业页面,了解我们的空缺职位。
在您的收件箱中获得这样的故事!
来自构建和扩展Shopify的团队的故事。这个商务平台为全球超过170万家企业提供支持。
电子邮件地址是的,给我登记
与我们分享您的电子邮件并接收每月的更新。
谢谢你的订阅。
你很快就会开始收到免费的提示和资源。