个人博客与简书同步:zhuzhaohua.com
概述
在之前的文章《前后端分离详解》中,提到单页面应用(SPA)有其弊端。本文将解释SPA所遇到的问题以及为了解决这些问题,所做的取舍。
SPA的优点
它是前后端分离的最佳实践。
-
关注点分离
单页面应用符合【关注点分离】这一架构基本原则,它只关注表现层,与后台松耦合;
-
用户体验好
画面无需刷新,数据异步获取,页面流畅、网络占用较小;
-
减轻服务器压力
单页面应用采用的是客户端渲染,减轻了后台渲染画面的压力;
-
表现层与后台解耦
这种解耦,是多端化、服务化的前提条件;
SPA的缺点
-
首屏加载缓慢
在第一次访问应用时,会首先将整个应用所需要的框架资源加载进来,这其中包括SPA框架的内容,各种工具库等。在加载完这些资源后,才会开始渲染首屏画面。虽然在后续的使用中有很好的体验,但这第一次的加载确实会有一些卡顿或延迟;
-
SEO不友好
SEO(搜索引擎优化)是指利用搜索引擎规则提高网站曝光率的手段,也就是让别人可以通过搜索引擎搜索到你的网页内容。SPA天生对SEO是免疫的,因为国内SEO爬虫一般是抓取网络内容而无视脚本,而SPA采用hash实现路由,并不会请求后台,即便通过ajax与后台交互,所获得的也只是json,并非网页资源。更可气的是,SPA首屏是通过js异步加载的,爬虫不会像你的浏览器一样在那里转圈圈等着资源加载完。
-
复杂度
如果是传统开发,只要功底深,用记事本都能写出来一张复杂的画面。但单页面应用是高度工程化的,各大框架都有自己的开发工具,比如vue-cli,就是基于vue生态的脚手架。为了实现单页面应用,需要理解前端的组件化,需要学习webpack,需要理解前端router、状态管理、ajax的封装、数据mock等等。而且要考虑更多的因素,比如单页面应用需要自己实现画面的前进后退。
客户端渲染
上文所述的缺点,其实都是一个原因,就是客户端渲染,那么究竟什么是客户端渲染?光靠嘴说,可能没那么直观,接下来以本博客网站为例,实际说明。
本站是使用vuepress搭建(这并不是本文重点,但如果你感兴趣,可以参看我的《本博客搭建教程》)。
博客的主体是一个单页面应用,在开发模式下,我们看看登录首页,究竟发生了什么事情:
localhost:8080是我本地开发环境的地址,那么在这个请求发出后,应该会得到博客的首页。的确,从响应可以看到,是一个html的结构:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title></title>
<link rel="icon" href="/img/logo.jpeg"></head>
<body>
<div id="app"></div>
<script type="text/javascript" src="/assets/js/app.js"></script></body>
</html>
但这个html实在是太单薄了对吗?body里有一个id为app的DIV,然后就是一个app.js的脚本。画面上其他的东西都是怎么来的呢?
玄机就在这个app.js脚本中,画面上的所有的内容都是从这个脚本开始渲染出来的,从网络上来看,localhost之后,就是加载app.js,然后加载各种js,画面就一层一层的被渲染出来,这就是典型的客户端渲染:
浏览器从后台获取基础画面以及脚本,然后通过脚本渲染画面,这个渲染的过程发生在浏览器,所以被称之为客户端渲染,你要叫它浏览器渲染也没毛病。
那么我们再来思考上文提到的SEO问题,爬虫只会抓网络请求,而且没有处理js的能力,所以,这个爬取的过程,得不到任何有价值的信息(因为html里什么都没有),也就做不了SEO,也就没人能搜索到你的网站,除非直接输入网址。
服务端渲染-SSR
为了解决客户端渲染首屏慢、无法SEO这两大问题,前端人真的是绞尽脑汁。历经千辛万苦建设起来的单页面应用的高楼大厦,可不能就这么倒掉啊。
其实要解决这两个问题,非常的简单,就是回到过去,采用服务端渲染。
那么什么是服务端渲染呢?
JSP和Thymeleaf都是服务端渲染。浏览器向服务器发一个请求,服务器通过后台逻辑去组装数据,然后绑定在模板文件中进行画面渲染,将渲染后的页面通过字符流返回给浏览器。说白了就是让服务端返回一张完整的html,而不是给浏览器一个脚本让它自己去渲染。
但是很明显,传统的服务端渲染不是前后端分离的。前后端分离和单页面应用是我们不可撼动的底线,但我们还要服务器渲染,二者鱼和熊掌,怎么可能兼得呢???
预渲染的妥协
有一种比较缓和的办法,也就是本网站使用的vuepress框架所采用的办法,叫做预渲染。
这种方式旨在改善某部分画面的SEO,比如你的首页,或者某些需要被渲染的页面(如公司介绍)。它可以在构建的时候,生成某些画面的简单渲染版本。还是以本站举例,这次我们看看生产构建完的版本是什么样的:
这次请求返回的内容不再是空洞的html里,相对复杂了一些,包含了一些关键字。
我们再看看具体的某一篇文章:
这篇文章,甚至可以看到具体的文本内容。
这就是预渲染所做的事情,它提前把一些页面内容渲染好,并“挂载”在其对应的路由中,当这个路由触发的时候,会优先去读取这个提前渲染了的页面。这个页面的加载速度会很快,而且内容很饱满,是可以SEO的。
(本文旨在阐明概念,技术细节不是本文重点,这里就不详细介绍预渲染的实现方法,如果感兴趣,可以搜一下prerender-spa-plugin。)
预渲染可以解决一些问题,对于比较小的需求,比如个人网站、公司网站、产品介绍之类的还是可以应对的。但它并没有改变客户端渲染的本质,只不过是部分画面的关键内容得到了提前的渲染,整个站点绝大部分内容的渲染依旧是在客户端完成的,而且,最最重要的是数据依旧通过ajax异步获取,这对于大型网站,比如电商,数据交互特别多、页面逻辑比较复杂的场景,页面的一部分样式已经展现了出来,但商品数据迟迟不能刷新、要转很久的圈。这种情况下,用户体验依旧很差。
还需要妥协的彻底一点。
更彻底的妥协
更彻底的妥协,就是回到过去。。。回到那个后台制霸的年代,回到那个服务器渲染的年代。。。但,今时不同往日,即便是服务器渲染,我们也不再会选择jsp或者mvc。我们有node和mvvm,即便是做后台,也要做前端人自己的后台,前端人不会再看后台的脸色了!
这种全新思路的集大成者,是react生态的Next.js 和 vue生态的Nuxt.js。名字很像,因为nuxt对标的就是next。(据说angular原生支持服务端渲染)
这种思路需要NodeServer作服务器,就像正常开发单页面应用一样去编写程序,服务器在启动时,会执行它的构建过程,它需要构建服务端与客户端两套应用,前者用来服务端渲染,后者用来混合静态标记。
这里解释下【静态标记】。也许你会问,既然已经有了服务器渲染的版本,渲染完了就把html返回给浏览器不就行了吗?为什么还要整出来一套客户端的版本?问题在于,我们大费周章的采用服务器渲染的目的就是减少客户端的渲染,优先把有用的内容推给浏览器,让它尽快的展示给用户,那么这个渲染完的html就需要是静态的,它不具备单页面应用那些复杂的动态效果,所以需要一个“客户端激活”的过程,而这个激活的过程,需要把服务端渲染的画面与静态版本进行比对,并且标记需要激活的节点。
另外需要注意的是,由于mvvm将在node服务器中运行,而非浏览器,所以它将不能使用特定的api,比如浏览器的window、document等等,而且生命周期函数也会受到限制。
这种node+mvvm的服务器渲染方式与tomcat+mvc没什么本质区别,只不过技术栈采用的是前端的技术栈。它依旧是前后端分离,因为表现层与业务层依旧解耦,只不过这里的表现层不再使用静态资源,而是动态渲染的。
结语
如果你之前没有接触过服务端渲染,读到这也许会纳闷,搞得这么复杂,费这么大劲,值得吗?其实技术实现上远比上面介绍的还要复杂。但请你看看标题:妥协,这篇文章通篇使用妥协这个字眼,这也许是前端人的无奈吧。
笔者技术粗浅,斗胆认为当前的这套方法论有待商榷,因为它所引入的复杂程度已经超过它所能解决的问题。笔者作为一名搬砖的码农,感觉当前的这套工具使用起来不是那么的得心应手。SPA与SSR都仍在发展的路上,我相信在不远的将来,必定会有一套鱼和熊掌都能兼得的更好的方案,大神们加油!
常见问题
1.我应该用SPA还是SSR?
SSR最主要的使用场景是面向互联网,面向用户,也就是所谓的2C。无论是SEO优化还是提高首屏加载的速度,其实都是为了让它在网络上有更好的表现。但如果你是要做一个企业级的内部管理系统或者一个管理后台,也就是2B,那么SPA就可以了。如何选择,没有一定之规,主要看实际的需求。