揭开React中server-side rending的神秘面纱

1,668 阅读7分钟

原文地址:medium.freecodecamp.org/demystifyin…

image
Server-Side Rendering :SSR 是一种前端框架能够在后端渲染出HTML的能力。那些能够在客户端和服务端完成渲染的应用就叫做universal app

为什么需要SSR?

为了理解为什么需要SSR,这里我们需要了解下web应用在过去十年内的发展史。SSR与SPA(Single Page Application)的兴起紧密相连。与传统的服务端渲染的app相比,SPA在速度和用户体验发面存在很大的优势。 但是使用SPA有个问题,通常情况下用户第一次请求会返回一个空html文件和一堆JS和CSS链接,渲染html之前会先把JS和CSS提前下载下来。 这就意味着首次渲染的时候,用户必须要等更长的时间。同时对于爬虫来说,解析到的页面也是一个空页面。 因此SSR的主要思想就是首次在server端渲染应用,后续可以充分利用SPA的优势,在客户端完成渲染。 SSR + SPA = Universal App 有的文章中也会把Universal App讲成isomorphoic app,实际上这两个是同一个东西。采用SSR的情况下,首次渲染的时候,用户不需要等JS加载完成后在看到渲染完成的页面,而是在请求返回的时候就已经拿到渲染完成的页面了。

对于使用slow 3G的用户来说,使用SSR会大大改善用户体验。用户将会直接看到网页内容而不是等待20s+才能看到网页内容。

image
现在情况下,所有发送到server端的请求都会被直接返回成HTML。这样对于做SEO的部门来说也是十分有利的。 对爬虫来说不会区别对待SPA引用和其他静态站点,同样会为服务端渲染的内容生成索引。 简而言之,使用SSR有两点好处:

  • 首次渲染速度更快
  • 生成的HTML内容可以被索引到。

一步步来理解SSR

下面笔者将通过一个例子,一步步来实现一个完整的SSR案例。首先从React的服务端渲染API开始,后续每一步我们都将加入一些新的内容。 可以follow 这个项目仓库 ,每一步的代码都会有一个tag,读者可以通过git checkout tags/xxx -b xxx的方式获取每一步的代码(xxx为对应的tag名)。

Basic Setup

开始介绍SSR之前,我们需要一个server。这里笔者采用express来渲染React应用。

image

在代码的第10行,我们用express启动了一个静态服务器。同时我们也创建了一个用于处理非静态请求的handle函数。非静态的路由将会返回HTML代码。

在代码的第13~14行,我们用renderToString函数把一开始的JSX代码转换成字符串,这段字符串后续将被插入到HTML模板中。

PS:我们并没有直接启动sever.js ,而是通过index.js来启动server.js。在index.js中,我们用babel插件来抹平client和server端的差异,保证client和server都能够使用es module和jsx。

在SSR中client端的代码也需要从ReactDOM.render的改成ReactDOM.hydrate。这个函数将会接受服务端渲染的react代码并挂载事件处理函数。

image

想看到完整的例子,可以check react-ssr tag为basic的代码。到这儿为止,我们就完成了一个简单的服务端渲染的react app。

React Router

到目前为止,我们的应用实际上啥事也没干。现在我们来往之前的应用加入一些路由。先来看看如何处理服务端部分:

image

现在Layout组件在client端上将会渲染出路由组件。对应的我们需要在server端模拟出client的路由实现。下面我们列出server端代码的修改部分:

image

在服务端的代码中,我们需要把React Application包装在StaticRouter组件中,并提供location参数。 PS:context用于在渲染React DOM的过程中追踪可能的重定向请求:比如client需要根据3XX响应重定向。 完整的案例需要checkout tag为router的代码。

Redux

在项目已经具备路由能力的情况下,下面我们来集成redux。一些场景下,我们需要使用redux来管理client端的状态。但是在服务端渲染的情况,如何根据当前状态来渲染部分DOM是个问题,因此我们有必要在服务端初始化redux。 如果应用在服务端dispatch action的情况下,SSR需要记录下这些操作,并把最终的state和HTML一起返回给client。在client端,会把服务端返回的state设为redux的初始状态。

我们先来看看server端的实现:

image

这段代码看起来实现的十分丑陋,但是我们确实需要把服务端渲染出来的redux状态和HTML代码一起返回给client。 接着来看看client部分的实现:

image

这里我们调用了两次createStore,一次在server端,一次在client端。但是在client端上需要把server端保存下来的状态设为redux的初始状态。

完整的例子可以看当前项目的redux tag。

Fetch Data

最后一步就是加载数据。这是个比较棘手的问题。我们从一个返回JSON数据的接口开始讲起。 在代码仓库中,笔者通过开放API获取了2018第一赛季的Formula的数据。我们希望在Home页面显示所有的Formula数据。

我们可以在所有React app挂载完成、所有元素都已经渲染完毕的的情况下调用API接口来完成需求。如果这样的话,可能会存在一些loading画面,对于用户体验并不友好。

考虑到项目中已经整合了Redux,我们可以通过Redux来保存数据并返回给前端的方式来加载数据。 如何在server端调用API接口、将接口返回数据保存在Redux中并让客户端根据相关数据来渲染HTML呢? 那么需要调用哪些接口呢? 首先我们需要通过一种不同的方式来申明路由。所以我们把路由改成如下所示:

image

同时我们也需要在组件上声明所有的数据:

image

PS: fetchData是一个 Redux thunk action,dispatch fetchData的时候会返回一个promise。 同时在服务端,我们也用了一个react-router中的特殊的函数:matchRoute

image

通过这个方法,当服务端根据当前URL渲染页面的时候会得到需要被mounted的组件。我们会收集所有组件需要的数据,等待所有接口都已经返回数据,并把获取的数据塞到redux中才会继续执行服务渲染。 切换到tag为fetch-data的分支可以看到整个案例。

从这儿开始,我们就会开始从各个维度进行比较,并比较出哪些场景适合使用SSR哪些场景不适合使用SSR。比如说对一个电商app来说,获取所有的产品是重中之重,但是价格以及一些其他的边栏filter相比之下就显得不那么重要。

Helmet

最后我们来看看SEO。当和React打交道的时候,我们经常需要在<head>标签中设置不同的值。比如:titlemeta tagskeywords等等。 记住<head>标签中的内容一般不是React App的一部分。 react-helmet就是为了解决修改<head>标签中的内容而生的,并对SSR提供了良好的支持。

image

你可以在组件树中的任何地方加入head标签内的数据。在client端上,react-helmet提供了一种修改React App以外部分的能力。 我们也在SSR中加入这种能力:

image

现在我们已经显示了一个具备基础功能的React服务端渲染的案例。我们从一个返回HTML内容的express应用开始,慢慢加入了路由、状态管理以及获取数据的能力。最后我们还处理React App之外的部分。完整的代码在master分支可以看到。

Conclusion

正如本文所示,SSR并不适合一件难事,但是SSR也可以做的很复杂。如果一步一步来实现需要会更容易些。那么项目中是否需要加入SSR呢?具体情况具体分析。如果网站访问量很大,则建议做SSR。但是如果你的应用是类似于工具或者dashboard这种应用,则没必要花费较多的精力来实现SSR。