原文地址:https://medium.freecodecamp.o...
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+才能看到网页内容。
现在情况下,所有发送到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应用。
在代码的第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代码并挂载事件处理函数。
想看到完整的例子,可以check react-ssr tag为basic的代码。到这儿为止,我们就完成了一个简单的服务端渲染的react app。
React Router
到目前为止,我们的应用实际上啥事也没干。现在我们来往之前的应用加入一些路由。先来看看如何处理服务端部分:
现在Layout
组件在client端上将会渲染出路由组件。对应的我们需要在server端模拟出client的路由实现。下面我们列出server
端代码的修改部分:
在服务端的代码中,我们需要把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端的实现:
这段代码看起来实现的十分丑陋,但是我们确实需要把服务端渲染出来的redux状态和HTML代码一起返回给client。
接着来看看client部分的实现:
这里我们调用了两次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呢?
那么需要调用哪些接口呢?
首先我们需要通过一种不同的方式来申明路由。所以我们把路由改成如下所示:
同时我们也需要在组件上声明所有的数据:
PS: fetchData
是一个 Redux thunk action
,dispatch fetchData的时候会返回一个promise
。
同时在服务端,我们也用了一个react-router
中的特殊的函数:matchRoute
:
通过这个方法,当服务端根据当前URL渲染页面的时候会得到需要被mounted
的组件。我们会收集所有组件需要的数据,等待所有接口都已经返回数据,并把获取的数据塞到redux中才会继续执行服务渲染。
切换到tag为fetch-data
的分支可以看到整个案例。
从这儿开始,我们就会开始从各个维度进行比较,并比较出哪些场景适合使用SSR哪些场景不适合使用SSR。比如说对一个电商app来说,获取所有的产品是重中之重,但是价格以及一些其他的边栏filter相比之下就显得不那么重要。
Helmet
最后我们来看看SEO。当和React打交道的时候,我们经常需要在<head>
标签中设置不同的值。比如:title
、meta tags
、keywords
等等。
记住<head>
标签中的内容一般不是React App
的一部分。
react-helmet就是为了解决修改<head>
标签中的内容而生的,并对SSR提供了良好的支持。
你可以在组件树中的任何地方加入head标签内的数据。在client端上,react-helmet提供了一种修改React App
以外部分的能力。
我们也在SSR中加入这种能力:
现在我们已经显示了一个具备基础功能的React服务端渲染的案例。我们从一个返回HTML内容的express应用开始,慢慢加入了路由、状态管理以及获取数据的能力。最后我们还处理React App
之外的部分。完整的代码在master分支可以看到。
Conclusion
正如本文所示,SSR并不适合一件难事,但是SSR也可以做的很复杂。如果一步一步来实现需要会更容易些。那么项目中是否需要加入SSR呢?具体情况具体分析。如果网站访问量很大,则建议做SSR。但是如果你的应用是类似于工具或者dashboard这种应用,则没必要花费较多的精力来实现SSR。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。