前后端分离架构概述

前后端分离的核心思想是:前端 HTML 页面,通过 Ajax 调用后端的 RESTful API,并使用 JSON 数据进行交互。

  • 服务器端渲染
  • 客户端渲染                                        // 使用 Node.js 做中间层
  • ( 全站 / 部分 )同构渲染

未分离时代( 各种耦合 )

「 服务器端渲染( fat-server,thin-client )」早在七八年前,架构模式主要使用 MVC,

几乎所有网站都在使用 ASP、JAVA、PHP 做服务器端渲染,

服务器端返回 HTML 之前,在特定的( 模板引擎 )符号里用数据填充,再给客户端,客户端只负责解析 HTML。


以 JAVA 为例,JSP + Servlet 的结构图如下:

即,所有的请求,都被发送给作为控制器的 Servlet,它接受请求,并根据请求信息将它们分发给适当的 JSP 来响应;

同时,Servlet 还根据 JSP 的需求生成 JavaBean 的实例,输出给 JSP 环境,

JSP 可以通过直接调用方法或使用 UseBean 的自定义标签得到 JavaBean 中的数据。

需要说明的是,

View 层,除 JSP 外,还可以使用 Velocity、Freemaker 等模板引擎,从而使开发中人员分工更加明确,提升开发效率。( 现在,可以结合基于 Vue.js 的轻量级应用框架 nuxt.js,来创建服务端渲染 ( SSR ) 应用 )


这个时期的开发方式,主要有两种:

对于这两种开发方式,

  • 方式 1,逐渐不被采用,这是由于前端在开发过程中严重依赖后端,在后端没有完成的情况下,前端根本没法干活
  • 而,方式 2,不得不说,其实很多小型、传统软件公司至今还在使用,其缺点也很明显:
    • 前端无法单独调试,开发效率低
    • 前端不可避免的会遇到后台代码,耦合性强;就算使用了 Freemaker 等模板引擎,不用写 JAVA 代码,也不可避免的要去重新学习给模板引擎的模板语法,无谓的增加了学习成本
    • JSP 本身的一些问题,如:JSP 第一次运行的时候比较慢,因为其中包含了将 JSP 翻译为 Servlet 的步骤;又如,因为同步加载的原因,在 JSP 中有很多内容的情况下,页面响应会很慢

随着网络的发展和客户端及 JS 性能的不断提高,前端不再是简单的 HTML + CSS,它开始承担起更多的功能逻辑和交互。

前端,在这里开始从后端分离出来。


半分离时代的探索

后端返回的内容,除了页面外,还可以是特定格式的数据;

与此同时,Ajax 技术的普及和前端框架( React、Vue 等 )的崛起,加速了进入前后端半分离时代的步伐。


「 客户端渲染 fat-client,thin-server 」前端负责开发页面,通过接口( Ajax )获取数据,采用 DOM 操作对页面进行数据绑定,最终由前端把页面渲染出来。

这也就是 Ajax 与 单页面( SPA )应用结合的方式,其结构图如下:

简单描述,大体就是:

  • 浏览器请求,CDN 返回 HTML 页面;
  • HTML 中的 JS 代码以 Ajax 的方式请求后台的 RESTful 接口;
  • 接口返回 JSON 数据,页面解析 JSON 数据,最终通过 DOM 操作渲染页面。
  • 后端提供的都是以 JSON 为数据格式的 API 接口供 Native 端和 Web 端使用。

所以,Web 工作流程可以这样理解:

  • 打开 Web,加载基本资源,如 CSS、JS 等
  • 发起一个 Ajax 请求到服务器端请求数据,同时展示 loading
  • 得到 JSON 格式的数据后,在根据逻辑选择模板渲染出 DOM 字符串
  • 将 DOM 字符串插入页面中,Web View 渲染出 DOM 结构

这些步骤,都是在用户所使用的设备中逐步执行的,也就是说用户的设备性能与 APP 的运行速度密切相关,

换句话来说就是,如果用户所使用的设备很低端,那么 APP 打开页面的速度会很慢。


为什么说是半分离呢?这是因为,不是所有的应用都是单页面应用,

在多页应用的情况下,前端因为没有掌握 Controller 层,需要跟后端讨论这个页面是同步输出呢,还是异步 JSON 渲染呢?

而且,即使在这个时期,可能也是一个工程师搞定前后端所有工作。因此,这一阶段,只能算是前后端的半分离时代。


对于这种方式,优点是很明显的:

  • 天生的关注分离设计,前端不会嵌入任何后台代码,专注于 HTML、CSS、JS 的开发,不依赖于后端,还能够自己模拟 JSON 数据来渲染页面;遇到 Bug,也能够迅速定位是谁的问题。
  • 局部刷新,无需每次都进行完整页面请求,用户体验好
  • 支持懒加载,比如,在页面初始时只加载可视区域内的数据,滚动后再加载其它数据
  • 富交互,使用 JS 实现各种酷炫效果;
  • 节约服务器成本,省电省钱,JS 支持 CDN 部署,且部署极其简单,只需要服务器支持静态文件即可
  • JS 一次学习,到处使用,可以用来开发 Web、Serve、Mobile、Desktop 多端应用

然而,这种架构也存在着明显的弊端,主要有以下几个方面:

  • JS 存在大量冗余,在业务复杂的情况下,页面渲染部分的代码非常复杂
  • 在 JSON 返回的数据量比较大的情况下,渲染十分的缓慢,尤其是首屏渲染,需要等 JS 运行才能显示数据
  • 不利于 SEO,由于搜索引擎的爬虫无法爬取 JS 异步渲染( Ajax )的数据
  • 资源消耗严重,手机端耗电也在无形中增加。业务复杂的情况下,一个页面可能需要多次 HTTP 请求才能渲染完毕。对于 PC 端,多次 HTTP 请求可能也没啥,但是,移动端建立一次 HTTP 请求需要消耗多少资源呢?

客户端渲染,主要是 SEO 和首屏渲染的问题,正是因为这些缺点,所以,才亟需真正的前后端分离架构。


分离时代的选择

前后端分离,已成为互联网项目开发的业界标准使用方式。一般使用 nginx + tomcat 的方式( 中间也可以加一个 Node.js )来进行解耦,这为以后的大型分布式架构、弹性计算架构、微服务架构和多端化服务打下了坚实的基础。


「 SPA 单页面应用( 半分离 )」目前,大家普遍认同的前后端分离的 Demo 就是 SPA,

其所用到的展现数据都是后端通过异步接口( Ajax/JSONP )的方式提供的,前台只管展现。

从某种意义上来说,SPA 做到了前后端分离,但这种方式存在两个问题:

  • Web 服务中,SPA 占比很少,很多场景还是使用同步 / 同步 + 异步混合的模式,SPA 不能作为一种通用的解决方案
  • 现阶段的 SPA 开发模式,接口通常是按照展现逻辑来提供的,而为了提高效率,前端可能也需要后端帮忙处理一些展现逻辑,这就意味着:后端涉及了 View 层的工作,不是真正意义上的前后端分离

SPA 的前后端分离,是从物理层进行区分( 认为只是客户端的就是前端,服务端的是后端 ),这种方式已经无法完全满足前后端分离的需求,而从职责上划分,可能更符合目前的使用场景:

  • 前端:负责 View 和 Controller 层
  • 后端:只负责 Model 层、业务处理和数据持久化等

目前,JAVA 更适合做 Model 层、持久层的业务,问题是,前端不懂后台代码,Controller 层如何实现呢?


「 使用 Node.js 做中间层 」

Node.js 适合运用在高并发、I/O 密集和少量业务逻辑的场景;

最重要的是,前端不用再学一门其他的语言了( 掌握了 JavaScript 就好 ),上手度大大提高。



So,使用 Node.js 作为中间层,有什么使用场景呢?

  • 可以把 Node.js 当作与前端交互的 API

总体来说,Node.js 的作用在 MVC 中相当于 C( 控制器 )。

Node.js 路由的实现逻辑是:把前端静态页面代码当成字符串发送给客户端( 如,浏览器 ),

所以,可以简单理解为:路由,是提供给客户端的一组 API 接口,只不过返回的数据是页面代码的字符串而已。

这样,浏览器就不需要直接请求 JSP 的 API 接口,而是:

浏览器请求服务器端的 Node  —  Node 再发起 HTTP 去请求 JSP  —  JSP 依然原样 API 输出 JSON 给 Node  —  Node 收到 JSON 后再渲染出 HTML 页面  —  Node 直接将 HTML 页面渲染到浏览器

这样,浏览器得到的就是普通的 HTML 页面,而不用再发 Ajax 去请求服务器了。

  • 可以把 Node.js 作为桥梁架接服务器端 API 输出的 JSON

后端出于性能或其他原因,提供的接口所返回的数据格式也许不太适合前端直接使用,前端所需的排序功能、筛选功能及视图层的页面展现,可能都需要对接口所提供的数据进行二次处理,这些处理虽然可以放在前端来进行,但数据量一大可能就会浪费浏览器性能,因此,增加 Node 作为中间层便是一种良好的解决方案。


使用 Node 做中间层,又有什么好处呢?

  • 适配性提升:在实际开发中,通常会给 PC、移动及 APP 端各自研发一套前端,但其实对于这三端来说,大部分端业务逻辑是一样的,唯一的区别就是交互展现逻辑不同。如果 Controller 在后端手中,后端为了这些不同端页面展现逻辑,自己维护这些 Controller,模板无法重用。如果增加了 Node 层,此时架构图如下:

在这种结构下,每种前端的界面展示逻辑由 Node.js 层自己维护,如果产品经理中途想要改动界面什么的,可以由前端自己专职维护,后端无需操心。前后端各司其职,后端专注业务逻辑的开发,而前端专注于产品效果的开发。

  • 响应速度提升;有时会遇到后端返回给前端的数据太简单了,前端需要对这些数据进行逻辑运算,在数据量比较小的时候,对其做运算分组等操作,并无影响,但是当数据量大的时候,会有明显的卡顿效果。这时,node中间层其实可以将很多这样的代码放入 node 层处理,也可以替后端分担一些简单的逻辑,又可以用模板引擎自己掌握前台的输出,这样做灵活度、响应度都大大提升。

例如,即使做了页面静态化之后,前端依然还是有不少需要实时从后端获取的信息,这些信息都在不同的业务系统中,所以需要前端发送 5、6 个异步请求,有了 Node 之后,前端可以在 Node 中去代理这 5 个异步请求,还能很容易的做 bigpipe,这块的优化能让整个渲染效率提升很多。在 PC 上可能觉得发 5、6 个异步请求也没什么,但是在无线端,在客户手机上建立一个 HTTP 请求开销很大,有了这个优化,性能一下提升好几倍。

  • 性能得到提升;大家应该都知道单一职责原则。从该角度来看,我们,请求一个页面,可能要响应很多个后端接口,请求变多了,自然速度就变慢了,这种现象在 mobile 端更加严重。采用node作为中间层,将页面所需要的多个后端数据,直接在内网阶段就拼装好,再统一返回给前端,会得到更好的性能。
  • 异步与模板统一;淘宝首页就是被几十个 HTML 片段(每个片段一个文件)拼装成,之前 PHP 同步 include 这几十个片段,一定是串行的,Node 可以异步,读文件可以并行,一旦这些片段中也包含业务逻辑,异步的优势就很明显了,真正做到哪个文件先渲染完就先输出显示。前端机的文件系统越复杂,页面的组成片段越多,这种异步的提速效果就越明显,前后端模板统一在无线领域很有用,PC 页面和 WIFI 场景下的页面适合前端渲染(后端数据 Ajax 到前端),2G、3G 弱网络环境适合后端渲染(数据随页面吐给前端),所以同样的模板,在不同的条件下走不同的渲染渠道,模板只需一次开发。

使用 Node.js 做中间层,前后端职责如何划分呢?


「( 全站 / 部分 )同构渲染( SSR + CSR )」

同构渲染,指的是前后端都是用来了 JavaScript,首次渲染的时候使用 Node.js 加载了 HTML 文件。

同构渲染,既不属于前端渲染,也不属于服务器端渲染,可以看作是 SSR 与 CSR 的结合,它是通过一份代码来实现的

  • 前端页面模板( Vue )
    • Node.js 中间服务( 中间层 — 大部分工作就是渲染页面和调用后台接口 )负责前端模板和后台数据的组合
  • 数据,依然由 JAVA 等服务端语言提供

我们知道,客户端渲染,主要面临的问题有两个:SEO 和首屏性能。

  • SEO 很好理解,由于传统的搜索引擎只会从 HTML 中抓取数据,导致前端渲染的页面无法被抓取;
  • 而,首屏性能 — 前端渲染常使用的 SPA 会把所有 JS 整体打包,无法忽视的问题就是文件太大,导致渲染前等待很长时间,特别是网速差的时候,让用户等待白屏结束并非一个很好的体验。

而,同构的出现,恰恰是为了解决前端渲染遇到的问题才产生的,其优势在于:

  • SEO 问题解决
  • 服务端渲染不用关心浏览器兼容性问题( 随着浏览器发展,这个优点逐渐消失 )
  • 服务端渲染不需要先下载一堆 js 和 css 后才能看到页面( 首屏性能 )
  • 对于电量不给力的手机或平板,减少在客户端的电量消耗很重要

至 2014 年底伴随着 React 的崛起而被认为是前端框架应具备的一大杀器,然而 2018 年后,很多产品逐渐从全站同构的理想化逐渐转到首屏或部分同构。这不得不让我们反思,同构的优点真的都是优点吗?

  • 有助于 SEO

首先,确定应用是否需要做 SEO。如果是一个后台应用,只需要做一些后台内容宣导就可以了;内容型网站,可以考虑专门做一些页面给搜索引擎;其次,时至今日,谷歌已经可以在爬虫中执行 JS 像浏览器一样理解网页内容了 ...

  • 共用前端代码,节省开发时间

同构,其实并没有节省前端的开发量,只是把一部分前端代码拿到服务器端执行,而且为了同构,还要处处兼容 Node 不同的执行环境,有额外成本。

  • 提高首屏性能

由于 SPA 打包生成的 JS 往往都比较大,会导致页面加载后花费很长的时间来解析,也就造成了白屏问题,而服务端渲染可以预先使到数据并渲染成最终 HTML 直接展示,理想情况下能避免白屏问题。但是,在实际的产品中,很多页面可能需要获取十几个接口的数据,单是数据获取的时候都会花费数秒钟,这样全部使用同构反而会变慢。


「 首屏或部分同构 — 优化首屏渲染 」

回到前端渲染遇到首屏渲染的问题,除了同构就没有其他解法了吗?通过以下三步可以有很大改观:

  • 分拆打包

现在流行的路由库如 react-router 对分拆打包都有很好的支持,可以按照页面对包进行分拆,并在页面切换时加上一些 loading 和 transition 效果。

  • 交互优化

首次渲染的问题可以用更好的交互来解决,可以看下 linkedin 的渲染

是不是感觉比较自然,打开渲染并没有白屏,有两段加载动画,第一段像是加载资源,第二段是一个加载占位器,过去我们会用 loading 效果,但过渡性不好。近年流行 Skeleton Screen 效果(骨架屏),其实就是在白屏无法避免的时候,为了解决等待加载过程中白屏或者界面闪烁造成的割裂感带来的解决方案。

  • 部分同构

部分同构可以降低成功同时利用同构的优点,如把核心的部分如菜单通过同构的方式优先渲染出来,现在通常的做法就是使用同构把菜单和页面骨架渲染出来,给用户提示信息,减少无端的等待时间。