告别是为了彼此变得更好---SPA 前后端彻底分离的探索之路

5,085 阅读10分钟

前后端分离,目前对于一个前端开发者来说,或者将其放在软件开发领域的历史背景下,都是再平常不过的一件事情了。17 年时进行了一个前后端彻底分离方案的实践,每次想起来这个方案时,总觉得它不够完美。现在将其写下来和大家交流,期望可以促进方案的后续完善。

邂逅

自从接触前端开发,一共接触到了三种前端项目发布的方式:

  1. 通过 FileZilla 直接将前端资源文件上传到业务服务器
  2. SSH 到业务服务器,通过 Git 更新前端项目代码,然后进行新版本资源的构建
  3. 将构建好的前端资源文件推送到 cdn

刚加入丁香园前端团队时,前后端分离的 SPA 项目的实现方式是:前端和服务端项目是两个代码仓库,HTML 模板由服务端语言(JSP/PHP)输出,HTML 引用的前端资源是通过前端资源发布系统发布到 cdn 上。涉及到 cdn,就会面临 cdn 缓存的问题。解决 cdn 缓存问题的方案是传统的时间戳机制。具体操作流程是服务端提供一个更新时间戳的接口,当前端每次发布新版本后,去调用更新时间戳的接口。

基于时间戳机制的前端项目发布流程大致为:

  1. 在发布系统进行项目发布
  2. 通过相关接口更新时间戳

看上去这个流程很简单高效,对不对?让我们来根据实际情况来细化一下这个流程:

  1. 登录发布系统
  2. 在发布系统点击对应项目的发布按钮
  3. 等待前端资源构建并发布成功
  4. 资源发布成功后,调用更新时间戳的接口
  5. 发布完成

流程细化之后,我们可以把流程分为5步,平均算下来每次前端同学进行一次项目发布,大概需要3分钟。在等待发布系统构建资源的时候,前端同学可以并行的去做一些放松的事情,比如:去餐吧喝杯咖啡,上个厕所,随手修个 bug。放松之后,更新一下时间戳,新版本发布便大功告成。接下来便可以进入愉悦的新需求开发之旅了,一切都显得如此惬意。

去年的我,也是这样认为的。因为当时在负责的新版调查问卷(SPA)和 Insight(可以看做 n 个 SPA)等项目,大概平均每天发布一次,所以用来发布的时间成本还是能接受的。

16年年底时,加入了大众医学部,开始和伙伴们一起负责来问丁香医生等项目。项目依旧是前后端分离的 SPA,构建好的前端资源依旧是发布到 cdn,解决 cdn 缓存问题同样采用的是时间戳机制。一切都是熟悉的配方,熟悉的味道。当参与了一段时间的需求迭代后,我发现事情似乎没有想象的那么简单。

让我有这种感受的原因,主要有两点:

  1. 更新时间戳的接口即使在内网,也是需要鉴权的。并不像调查问卷等项目,直接在浏览器访问一下更新时间戳的接口就可以了。鉴权的方式是需要微信扫码。
  2. 需求迭代速度谦虚一点说,很快。仅生产环境,几乎每天都要发布至少一次。

此时细化的一次发布流程变为:

  1. 登录发布系统
  2. 在发布系统点击对应项目的发布按钮
  3. 等待前端资源构建并发布成功
  4. 资源发布成功后,调用更新时间戳的接口(需要鉴权)
  5. 访问更新时间戳的页面,页面弹出鉴权二维码
  6. 发布者拿起手机
  7. 打开微信,点击扫一扫,扫描二维码
  8. 微信接收到同意授权的模板消息
  9. 点击模板消息进行授权,授权成功后进入更新时间戳页面
  10. 点击更新时间戳按钮
  11. 等待生效(服务端有缓存机制)
  12. 发布完成

一次前端发布的时间,平均在7分钟左右。在实际工作中,需要在测试环境、(预发环境)、生产环境进行发布。从团队的角度来看,管理后台等项目也在采用这种发布方式。毛估一下,每周团队在项目发布上就需要花费 2 ~ 3 个小时((7min * 2 * 2 * 5)/ 60)。

此外,这种前后端分离的方式还有以下几个问题:

  • HTML 模板开发效率较低。项目所使用的 HTML 文件需要前端同学写好后发给后端,后端再进行“套模板”,这种做法本身就有一个流程的复杂度。后端同学如果对 HTML 掌握的不熟练,那么需要前端同学去跟后端同学结对编程,来确保 HTML 的正确,此处有一个沟通成本。项目上线后,如果需要对 HTML 进行改动,需要前端先修改好 HTML,然后把 HTML 发给后端,后端再次进行“套模板”。后端同学修改好 HTML 模板文件后,可能不会因为这一个改动进行发版,需要跟随着后端项目的其他改动代码一起发版,此时对于前端、测试同学有一个等待的成本。
  • cdn 资源有一点点浪费。页面中所有前端资源是使用同一个时间戳,这意味着每次更新时间戳都会更新页面中的全部资源引用地址,从而当用户再次使用应用时,需要重新下载页面引用的所有资源。而前端的某个新版本,可能仅仅是需要更新部分资源文件即可。
  • 加长了应用的响应时间。这一点同样是由上一点描述的时间戳机制导致的,重新下载“新”的资源而不是利用浏览器的缓存,必然会导致用户需要等待更长的时间。

念念不忘,必有回响

为了解决上述问题,我设计了一个的方案:

方案说明:

  • 前端项目生产环境构建时,将文件名中加入 hash 值。
  • 前端资源发布系统在资源构建成功后,将 index.html 同步到业务服务器(本着线上项目不做写操作的原则:会把 HTML 文件放到后端项目和Node项目目录之外的地方)。
  • 在业务服务器上新增一个 Node 服务。该服务的作用之一为:收到浏览器端首页的请求时,将 index.html 返回。
  • 当用户访问应用时,由运维将请求首页 HTML 的请求转给 Node 服务,其余业务接口保持原有方式不变。

方案确定后,在团队小伙伴的配合下,该方案在一个流量较小的项目上线了。

新的火花

上述方案存在一个问题的:在原有的技术体系中,引入了 Node.js。本质上是在稳定的技术体系下,增加了技术复杂度。因此,在不增加技术复杂度的前提下,需要开始探索新的解决方案。在后端同学的配合下,新方案相比于引入 Node.js 方案改动如下:

  • 业务服务器给前端发布系统提供同步 HTML 模板文件的接口,前端发布系统每次成功构建前端资源后,调用该接口将模板文件同步给业务服务器。
  • 业务服务器获取模板文件后,将文件内容存入数据库,持久化存储模板。在服务重启和用户请求时,服务端从数据库中获取模板。
  • 服务端可以将从数据获取的模板放到缓存中,这样可以避免高频的读取数据库操作。对应的业务服务器需要监控数据库中的数据变化情况,以便于及时更新缓存中的模板资源。

方案上线后,前端项目的发布流程变为:

  1. 登录发布系统
  2. 在发布系统点击对应项目的发布按钮
  3. 等待前端资源构建并发布成功

前端项目发布这件事,终于变成了点击一下发布按钮,整件事情就做好了。整个发布流程耗时变为不到一分钟。此外,当前端需要改变 HTML 模板时,也不再需要将文件发给后端同学,苦苦等待后端项目的发版。

该方案上线后,组内同学在周报中提及到的使用新方案后的感受为:

为了方便前端同学获取同步模板的进展,在发布系统中增加了同步过程的提醒:

没有银弹

上述方案相比于 JSP/PHP 提供 HTML 模板存在一个问题,就是在前端在构建 HTML 时,(暂时)不能将应用初始化的数据放入 HTML 中。解决方案是服务端提供一个接口,在应用启动时去调用该接口获取初始化数据。

对于前端加载优化,整体上思路上是需要减少网络请求的,而此方案却在增加网络请求,这意味着页面加载时间会变长。但是综合利弊之后,还是决定采用这个方案。

最终方案上线后,其实心中已经做好了页面平均加载时间会变长的心理准备,但是有些意外的是,几天后去 mta 看数据发现全国范围内平均响应时间缩短了 0.2s 左右。为什么不升反降,目前我还不能得出一个准确的答案。猜测的一个原因是: hash 值的方案避免了用户进行不必要的资源更新。

待完善

上述方案虽然做到了前端项目的一键发布,但是还不够称作为一个完善的解决方案。因为该方案只是解决了 SPA 类型项目的发布问题,对于之前“套模板”重 SEO 的项目而言,并不是很适用。(提到 2018 年的技术浪潮下,如何开发一个重 SEO 的网站这个话题,又可以写一篇文章了,其中的心路历程还是蛮坎坷的)

言归正传,在前端资源发布系统层面,该方案可以考虑去增加文件历史和发布回滚功能,以备不时只需。

这个方案是和业务线的服务端同学配合实现的,从公司层面来看,可以考虑的点是是否可以将这个方案做成一个通用的服务。

在和团队的交流时,相学长提出可以将 HTML 引用的资源抽象成 JSON Tree 进行存储。之前看过一些类似的解决方案,不过目前自己还是更倾向于分开的“更彻底”,这样可以让服务端同学更安心的提供接口。

写在最后

由于水平有限,欢迎大家对此方案提出建议。非常期待。

本文作者:丁香园前端工程师 @志遥