为什么会产生这种技术,要解决的问题是什么呢?
每种技术背后的原理又是什么呢?
在了解这些概念之前,我们要先了解一个熟知的概念,那就是SPA(SinglePageApplication),没错,就是大家熟知的单页应用,其实CSR、SSR、Prerender都是基于SPA,关于SPA的概念我就不多阐述了。
即,渲染过程全部交给浏览器进行处理,服务器不参与任何渲染。
打包下来页面是这个样子:
我们来使用create-react-app来建立一个web工程,并在Chrome里使用slow3G网络下做个实验:
即,打包的时候就预先渲染页面,所以在请求到index.html就已经是渲染过的内容
流程:浏览器-->服务器-->index.html(预渲染的内容)-->Render-->bundle.js+images-->Render
我们将刚刚的工程加入prerender-spa-plugin这个插件,再次运行看看结果
这次打包下来的主页html是这个样子的:
流程:浏览器-->服务器-->服务器执行渲染-->index.html(实时渲染的内容))-->Render-->bundle.js+images-->Render
可见SSR在服务端多做了一些实时渲染的操作,那么我们这次运行下来回事什么结果呢?
P.S其实另一方面的原因是,CSR对SEO太不友好了,搜索引擎抓取不到关键信息,只能抓取一个毫无元素的白屏页面,会导致搜索引擎搜索不到你的页面信息进行推荐,SSR和Prerender都能很好的解决这个问题。(吐槽一下:Google已经实现了抓取基于SPA的CSR)
在做出选择之前,我们必须要充分的了解两者的差异。
说了那么多利弊,那么,预渲染是怎么做到生成页面的呢?
做过爬虫的同学肯定知道headless的概念
HeadlessChrome在Chrome59中发布,用于在headless环境中运行Chrome浏览器,也就是在非Chrome环境中运行Chrome。它将Chromium和Blink渲染引擎提供的所有现代Web平台功能引入命令行。它有什么用处呢?headless浏览器是自动测试和服务器环境的绝佳工具,您不需要可见的UIshell。例如,针对真实的网页进行测试,创建网页的PDF,或者只是检查浏览器如何呈现URL。
Prerender就是利用Chrome官方出品的Puppeteer工具,对页面进行爬取。它提供了一系列的API,可以在无UI的情况下调用Chrome的功能,适用于爬虫、自动化处理等各种场景。它很强大,所以很简单就能将运行时的HTML打包到文件中。
原理是在Webpack构建阶段的最后,在本地启动一个Puppeteer的服务,访问配置了预渲染的路由,然后将Puppeteer中渲染的页面输出到HTML文件中,并建立路由对应的目录。
下面是流程图
目前来说,主流的框架React、Vue都已经支持SSR,只是配置会繁琐点,有人就会疑惑,框架还要支持SSR?
可事实是,正是因为现代SPA的VirtualDOM的存在,才能使SSR变成现实,但是,SSR这种理念的实现,并非易事。
我们先来看看详细的SSR流程图:
可以看出,SSR和Prerender的最大区别就在于,Prerender是静态的,SSR是动态的,SSR会在服务端实时构建出对应的DOM。
这也是SSR的难点所在:同构(即服务器和浏览器共同构建)。
同构这个概念存在于Vue,React这些新型的前端框架中,同构实际上是客户端渲染和服务器端渲染的一个整合。我们把页面的展示内容和交互写在一起,让代码执行两次。在服务器端执行一次,用于实现服务器端渲染,在客户端再执行一次,用于接管页面交互。
上面我们说过,SSR的工程中,React代码会在客户端和服务器端各执行一次。你可能会想,这没什么问题,都是JavaScript代码,既可以在浏览器上运行,又可以在Node环境下运行。但事实并非如此,如果你的React代码里,存在直接操作DOM的代码,那么就无法实现SSR这种技术了,因为在Node环境下,是没有DOM这个概念存在的,所以这些代码在Node环境下是会报错的。
但是就是由于VirtualDOM技术的存在,让这一切变成了可能,这里不过多介绍VirtualDOM,简单来说,它就是一个普通的JS对象,只不过映射了HTMLDOM的结构,React在做页面操作时,实际上不是直接操作DOM,而是操作VirtualDOM,也就是操作普通的JavaScript对象,这就使得SSR成为了可能。
我们可以直接在代码里判断当前的运行环境,如果是浏览器,就可以直接操作DOM,如果是服务器,就需要使用VirtualDOM生成HTML字符串。
所以React分别为浏览器端和服务器端分别提供了BrowserRouter和StaticRouter两种路由,通过BrowserRouter我们能够匹配到浏览器即将显示的路由组件,对浏览器来说,我们需要把组件转化成DOM,所以需要我们使用ReactDom.render方法来进行DOM的挂载。而StaticRouter能够在服务器端匹配到将要显示的组件,对服务器端来说,我们要把组件转化成字符串,这时我们只需要调用ReactDom提供的renderToString方法,就可以得到App组件对应的HTML字符串。
还没有!
对于一个React应用来说,路由一般是整个程序的执行入口。在SSR中,服务器端的路由和客户端的路由不一样,也就意味着服务器端的入口代码和客户端的入口代码是不同的。而入口则是Webpack进行打包完成的。
针对代码运行环境的不同,要进行有区别的Webpack打包,我们需要在Webpack的配置中加入target:'node',表明是服务器环境进行打包,除此之外,还有各种各样的配置需要解决。
如果要用到redux进行全局状态管理,一定要记得写成这种形式:
constgetStore=req=>{returncreateStore(reducer,defaultState);};exportdefaultgetStore;因为服务器端的Store是所有用户都要用的,但是不能让所有用户共享Store,所以在服务器端渲染中,Store的创建应该像下面这样,返回一个函数,每个用户访问的时候,这个函数重新执行,为每个用户提供一个独立的Store。
由于服务器不存在挂载元素这一生命周期,所以例如React的componentDidMount或者VUE的mounted生命周期都不会执行了,所以在服务端利用接口获取数据的时候,不能写入上述的生命周期中。
当然在真正实现SSR架构的过程中,难点有时不是实现的思路,而是细节的处理。比如说如何针对不同页面设置不同的title和description来提升SEO效果,这时候,我们其实可以用react-helmet这样的工具帮我们达成目标,这个工具对客户端和服务器端渲染的效果都很棒,值得推荐。还有一些诸如工程目录的设计,404,301重定向情况的处理等等,不过这些问题,我们只需要在实践中遇到的时候逐个攻破就可以了。