2.1 HTML
Web网站从诞生到发展至今的二十几年时间里,前端资源的分配比例依次经历了少量HTML+少量CSS、大量HTML+大量CSS+少量JavaScript、大量HTML+大量CSS+大量JavaScript、少量HTML+大量CSS+大量JavaScript,并且目前All-in-JS(即HTML和CSS由JavaScript编写)的趋势越来越明显。在React、Vue、Angular等开发框架的支持下,HTML和CSS可以由JavaScript编写,经编译后产生CSS代码和用于在浏览器环境下渲染HTML的JavaScript代码。
Web网站在从静态的“WebPage”进化到动态的“WebApp”的过程中,客户端渲染(Client Side Render,本书后续将其简称为CSR)是非常重要的一环。服务端渲染(Server Side Render,本书后续将其简称为SSR)到客户端渲染转变的背后是技术的推动。浏览器的高性能和功能丰富性是CSR可行的必要环境基础,JavaScript、HTML的不断进化是支撑CSR从某些方面能够超越SSR的技术条件。虽然在当前的技术环境下,CSR完全有能力在某些场景下取代SSR,但是Web整体的产业生态并没有与前端技术保持同步高速发展,其中最典型的就是SEO。
JavaScript起步晚于HTML和CSS[2],最初的Web网页仅仅是静态的HTML页面,后来陆续加入了CSS和JavaScript。时至今日,绝大部分浏览器仍然保留着“禁用JavaScript”的功能开关。SEO爬虫软件通过分析网站的HTML文档抓取信息,从其诞生之初便没有将JavaScript脚本考虑在内。即便是在依赖CSR的SPA模式大行其道的今天,也仅有Google爬虫初步支持了SPA并且需要在编写时进行特殊处理。[3]所以,前端技术单方面的可行性并非是决定采用SSR还是CSR的唯一因素,还需要结合Web整体产业生态,综合考虑业务产品的使用场景、用户类型甚至市场推广策略等多方面因素。
2.1.1 SSR
SSR的大致工作流程如图2-1所示。
1. 浏览器向网站发起请求。
2. 服务器接收到请求后首先查询数据库中的动态数据(如用户数据),然后将数据通过模板引擎编译为HTML字符串返回给浏览器。
3. 浏览器接收到HTML文档之后将其渲染为可视化的UI。
图2-1 SSR的大致工作流程
SSR相对于CSR最主要的优势在于其支持SEO且首屏时间短,前者从Web整体产业生态出发,后者从用户体验出发。SSR适用于交互简单、以静态内容为主并且有SEO需求的业务产品。CSR与SSR在首屏时间上的差距正在随着用户设备硬件性能、网络技术的提升以及前端技术的发展逐渐缩小,但是SEO仍然是短时间内CSR无法逾越的一道鸿沟,也是目前市场上SSR仍然占据主流的主要原因。目前来看,前端技术单方面的发展无法令CSR完全取代SSR,但在一定程度上影响了SSR的具体实现模式。
传统模板引擎
传统SSR的实现模式是借助与服务端编程语言或框架搭配的HTML模板引擎将数据编译为HTML文档,比如适用于Java的FreeMarker、适用于PHP的Smarty以及适用于Node.js的Pug等。模板引擎可以被理解为一个功能强大的字符串处理工厂,其最大的优势是快速和缓存。
React/Vue SSR
React并不是一个旨在取代jQuery的“工具库”,也不仅仅是一个好用的框架,而是前端领域内的一项革命性技术和理念。Virtual DOM令前端“脱离”了对浏览器环境的强依赖,为在Node.js环境下运行React提供了可行性。使用React、Vue承担SSR的主要优势在于同构编程,同一组件可以同时适用于客户端和服务端,这不仅节约了一定的开发成本,还能够在一定程度上加强前后端视图逻辑的统一性。然而相对于传统模板引擎,React/Vue SSR仍然有一些难以避免的问题。
首先,React/Vue SSR必须依赖Node.js环境,技术栈的迁移对于历史悠久的团队和项目来说是一项成本非常高的工作。其次,React、Vue最主要的阵地仍然是客户端,其核心思想以及实现方式均以面向浏览器环境为主,截至目前,两者均尚未推出专门针对SSR的版本。所以,将React、Vue应用于SSR不能发挥框架的全部能力。事实上,SSR仅仅使用React、Vue框架将数据编译为字符串的几个API,其他只适用于浏览器环境的API(如React的componentDidMount、Vue的mouted等)便成了冗余,而这些恰恰是框架最核心的功能。任何冗余的功能和代码都是有代价的,这些代价引发了React、Vue相对于传统模板引擎最大的劣势之一:性能。分别使用Pug引擎和Vue SSR渲染一个长度为100的列表[4],测试两者的并发处理个数和单次渲染速度,结果如图2-2和图2-3所示。从对比数据来看,Vue SSR相对于Pug模板引擎仍然存在很大的差距。
图2-2 Pug模板引擎与Vue SSR并发能力对比
图2-3 Pug模板引擎与Vue SSR渲染速度对比
综上所述,传统模板引擎适用于非Node.js技术栈团队以及对渲染速度要求较高的业务产品,React/Vue SSR适用于Node.js技术栈团队以及对渲染速度要求略低的业务产品。
2.1.2 CSR
CSR的大致工作流程如图2-4所示。
1. 浏览器向网站发起请求。
2. 服务器接收到请求之后立即返回静态的HTML部分,这部分内容通常是与用户无关的静态数据。
3. 浏览器解析HTML文档,待JavaScript脚本加载完成之后发起异步请求,获取动态数据。
4. 服务器接收到异步请求之后查询数据库并将动态数据返回给浏览器。
5. 浏览器接收到动态数据后使用JavaScript将数据编译为HTML字符串并渲染为可视化的UI。
图2-4 CSR的大致工作流程
相对于SSR,CSR的劣势在于首屏渲染速度慢和弱SEO。服务器的硬件配置通常优于个人终端设备,同时搭配模板引擎的缓存功能,SSR在速度上的优势要远远大于CSR。然而随着个人终端设备硬件性能的不断提升,CSR与SSR在速度上的差距逐渐减小,网络传输速度和浏览器性能的提升也进一步缩短了CSR被一直诟病的首屏等待时间。CSR的优势在于能够更好地支持离线场景和前后端分离[5]方案的实施。此外,随着React、Vue、Angular等框架的普及,对于存在大量动态渲染场景的产品来说,CSR的优势更加明显。
如果抛开弱SEO的问题,CSR在大多数场景下可以取代甚至优于SSR,尤其是在移动为先、SPA模式大行其道的市场背景下。Virtual DOM可以保障动态渲染良好的性能;基于组件的前端路由管理不论是从速度还是灵活性上都优于依赖服务器路由驱动的MVC模式。Google近期推出的PWA[6]概念更是需要大量使用CSR。总体来说,CSR能够令Web网站更接近于WebApp而非WebPage。
Virtual DOM
HTML是结构化的文本文档,每一个元素(element)对应HTML中的一段结构化文本,DOM(Document Object Model,文档对象模型)可以理解为这段结构化文本的抽象。DOM储存于内存中,提供了操作和修改HTML元素的一系列API。DOM操作是前端开发的核心,最早流行的一些前端框架、工具库大都以优秀的DOM操作闻名,比如prototype.js和jQuery.js。与JavaScript逻辑相比,DOM操作的性能消耗非常高,在以静态内容为主的WebPage时代,少量DOM操作的性能损耗基本可以忽略不计。然而对于存在丰富动态内容的WebApp而言,大量、频繁的DOM操作逐渐成为性能瓶颈。Virtual DOM技术的核心是创建DOM对象的一个“轻量级克隆对象”,即虚拟DOM,所有的虚拟DOM组成一个与HTML元素一一对应的虚拟DOM树。虚拟DOM拥有原始DOM除了操作HTML元素的API以外的所有属性。对于JavaScript而言,虚拟DOM仅仅是一个拥有丰富属性的对象,所有针对DOM的操作被映射为对JavaScript对象的修改,性能上自然大幅优于直接对DOM的操作。在接收到动态数据或者用户操作指令后,React、Vue会在内存中将虚拟DOM树“全量更新”,对比检测出受影响的对象,随后对这些对象所对应的真实DOM进行相应修改。之所以采用全量更新策略并且能够保障对比性能,得益于React、Vue高效的diff算法。
React、Vue采用的diff算法非常复杂,本书的意图并非是详解Virtual DOM及其diff算法细节,所以在此不会展开介绍,感兴趣的读者请自行查阅相关资料。
预渲染
用户从输入网站地址按下回车键到能够看到浏览器中有内容输出,这段时间被称为首页的白屏时间。除去DNS查找、TCP握手等开发者无法干预的浏览器前期工作以外,白屏时间的计时起点为浏览器接收到第一个HTTP响应字节,计时终点为HTML文档开始解析。之所以SSR的白屏时间相对较短,是因为HTML文档的HTTP请求响应内容有真实的内容,渲染即可见。而由于浏览器解析〈script〉标签的策略,CSR的白屏时间还需要计入JavaScript脚本的加载、解析时间以及异步HTTP请求和响应的时间,在这些逻辑完成之前,用户看到的是一个空白的加载页面。在较差的网络环境下,白屏时间对用户耐心的消耗会被转变为对用户留存度的挑战,进而间接影响产品的市场竞争力,这是CSR除了弱SEO以外被诟病的另一个核心问题。
从这个角度出发,业内普遍的一种解决方案是在真实数据被渲染之前,预先渲染出如图2-5所示的页面“骨架”来取代空白的加载页面,让用户能够优先得到视觉上的反馈,从而在一定程度上减少对用户耐心的消耗。
图2-5 预渲染页面“骨架”
在开发阶段按照真实页面的布局在index.html中预先写入静态内容,即为“骨架”页面。如图2-5所示,左侧是“骨架”页面,右侧是真实内容,两者从整体布局上保持一致。用户首先看到的是静态的“骨架”页面,在JavaScript异步请求处理完成之后,再以真实的DOM替换“骨架”内容,预渲染整体流程如图2-6所示。
图2-6 预渲染整体流程
预渲染与其说是技术上的革新,不如说是交互设计上的优化。从技术角度来看,预渲染并没有任何难度,但是从工程角度来看其却是一项非常消耗人力且短时间内没有合适工具可承担的工作。“骨架”页面的整体布局必须与真实页面保持一致,这意味着:
● 每个布局不同的页面均需要搭配一个单独的“骨架”页面。
● 一旦页面布局被调整,“骨架”页面的布局也需要被同步调整。
无论是技术、工程还是设计,核心的出发点始终是用户,很多时候,可以说大部分时间都需要在三者之间进行权衡。所以,即便预渲染“骨架”页面的方案与工程思维略有偏离,但只要能够提升用户体验从而转化为产品收益,这便是优秀的方案。
SEO
SPA如何支持SEO是前端领域近几年比较热门的一个话题,然而至今仍未有完美的解决方案产生。目前普遍的弥补方案大致分为两类:
● 构建阶段预渲染SPA的静态内容至index.html。
● 服务器判断请求来源,将爬虫请求重定向到预渲染服务器。预渲染服务器通过仿真浏览器环境解析[7]index.html,并将解析后的HTML文档返回至爬虫请求源,如图2-7所示。
第一种方案提到的静态内容预渲染与前面的“骨架”预渲染并不相同,“骨架”仅仅是没有任何实体内容的占位UI,是一种从设计角度提升用户体验的优化方法;而预渲染静态内容指的是在构建阶段将SPA中与用户无关的内容提前解析为HTML字符串并添加至index.html,这部分内容通常称为静态内容。目前市面上主流的构建工具都提供预渲染功能,比如Webpack[8]。这种方案的优点是实施成本较低,不涉及服务端开发。但其本质上与“首页SSR,动态内容AJAX”的模式大同小异,所以应用面非常窄。对于存在大量路由和动态数据的SPA项目而言,其对SEO的提升微乎其微。
图2-7 重定向预渲染服务流程
第二种方案对于真实用户而言是CSR,对于爬虫程序而言是SSR,只不过是由仿真浏览器环境替代了模板引擎。这种方案的优点是能够将完整的HTML文档提供给爬虫程序,实现可以媲美SSR的SEO支持。但是其部署成本非常昂贵,不仅需要额外的开发工作,还需要硬件设备支持。当然也可以使用第三方提供的云服务,比如prerender.io[9]。更好的做法是在构建阶段将SPA各页面提前解析并存放于预渲染服务器中,在接收到用户请求之后即可立即返回数据。此外,若要完全发挥此方案的优势,SPA的路由管理必须使用HTML5 History模式而非Hash模式,并且需要服务器支持。所以对于一些需要兼容低版本浏览器的SPA项目来说,这种方案的性价比并不高。
前端路由History模式使用的是URL的真实路径(path),Hash模式使用的是URL的片段标识符(fragment identifier),该标识符也被称为hash参数。片段标识符本身的语义是主文档的一个片段标记,并非路由标记。其之所以目前被前端路由广泛使用,一方面是因为HTML5 history API兼容性不理想;另一方面是因为浏览器在URL hash改变后不会向服务器发起请求,无须服务端支持,实施成本低。而正是由于这种机制,第二种方案并不会接收到SPA首页以外的页面请求,从而无法给爬虫程序提供完整的子页面数据。本书将在第5章详细讲解前端路由两种模式的区别和实施方案。
总体而言,SSR相对于CSR有更深的技术沉淀和相对完善的产业生态;CSR随着前端技术的横向延伸有逐步取代SSR的趋势。对于特定的产品类型(如混合应用、小程序等),CSR是相对较优的解决方案,能够更好地支持前后端开发的解耦和分离部署,降低人力和时间成本。而对于依赖SEO的产品而言,如果使用CSR同时兼顾SEO则需要大量额外的工作和成本。基于两者各自的现状和优缺点,重新思考HTML渲染方案的选型,其实并不是非此即彼、非黑即白,现实工作中往往要根据业务特征混合使用两者。比如,资讯类网站的首页、内容详情等需要SEO的页面使用SSR;而用户中心、管理后台等无须SEO的页面则可以使用CSR。