前言
不止一次被问到这个问题,但是感觉每次的回答都不尽完美。
今天想好好聊一聊这个话题。
回顾
在来猿辅导之前,我待的公司里,没有一个是有做过彻底的前后端分离,特别是 C 端项目。
而在这种模式下,前后端基本上都是共用一个项目,架构基本都使用的 MVC 模式,在这里面前端主体是负责 V(View)层,然后也多多少少需要写一点 C(Controller)层。
这种情况下,不可避免的就会有各种各样的问题:
- 前端需要去了解并熟悉必要的后端知识及体系架构。
- 对应的服务端也需要去熟悉必要的前端知识及体系架构。
- 一个工程或者项目里同时有大量的后端代码以及前端代码,它们相互杂糅在一块。
前后端分离
关于前后端分离这个话题,前后端都有很多探索。
前端层面最初主要是在 B 端。B端把入口的 index.html 直接放到 nginx 上,或者放到其他地方,然后通过 nginx 做一层转发。
页面上所有的交互都是通过 API 的方式交互。
这种方式一般是采用的是 hash 路由模式,所有页面通过直接打包到当前代码里来实现。
那 C 端能不能用呢?
当然也行,只是会存在很多的问题:
一是性能问题
上面这种模式下,可以理解为几乎所有的页面都在一个项目里。
这么一来:
页面非常大,导致不管是加载还是执行都非常的慢。 所有请求都需要在页面上处理。 针对第一条,你也可以说使用异步加载来规避页面太大的问题。
我们先看看正常情况下的请求处理是:请求 js => 执行 js
然后,异步加载就变成了:请求 js => 执行 js => 请求 js => 执行 js
这里明显可以看到异步加载多了 请求 js => 执行 js 的步骤,而且是串行的。
针对第二条,这种模式下,所有的请求就只能是在页面处理。
二是能力受限
很多时候 C 端都需要考虑鉴权、用户信息处理等。
但如果是这种模式的,所有的处理都需要有对应的一条请求来做。
比如,frog里要用到 userId,就必须得请求用户信息接口来获取。
如果需要额外的特殊逻辑,比如自动登录之类的功能,基本都需要依赖服务端。
C 端的应用
正是因为上面的问题,所以 C 端非常少的直接用这种方式去做。
大部分是通过拆分的方式来做。
通过生成很多个项目,然后配置不同的路由来做。以此来规避其中最重要的一个问题:性能问题。
毕竟 C 端是直接面向用户的,性能相关问题就是重中之重。
这种方式下,在初期项目规模不大的话,其实蛮好的,业务划分明确,相互之前不影响。但是随着业务逐步发展,项目越来越多,就会碰到各种各样的问题:
- 依赖升级比较麻烦,需要通过升级所有的项目去做到。项目不多还好,一旦很多就是重复的体力劳动了。
- 同一,涉及到某些代码 bug,或者功能迭代,特别是数据结构和接口级别的改动,如果所有地方都要去做一遍,那就太反人类了。
- 最重要的也是最麻烦的是开发模式不统一,每个项目由于历史的原因,可能使用了不同的开发模式,特别是在编译打包、启动、上线上。导致你在写代码的时候你的某个写法可能在当前的系统上是不支持的。意味着你着手开发一个功能前,都需要先去熟悉去了解当前项目的开发模式,这很抓狂。
- 系统太多,不管是对于新人,还是老人来说,要去改某个东西,都可能出现考虑不全的情况。不可避免的会出现遗漏的情况,重则导致线上故障。
整体看,这种模式的后期维护成本会非常的高。
微服务的崛起
为什么我们要讲到微服务呢?
因为服务端的架构是会直接影响到我们前端的架构。
在前后端分离这个大方向上,不仅仅是前端在探索,服务端也在做类似的事情。
在传统的 MVC 这套架构层面,服务端其实也是很不爽的,因为他们也需要去熟悉和了解前端层面的知识。
而服务层面夹杂了太多跟前端项目的东西,就会导致整个项目很大,他们也需要小心翼翼的维护着。对他们来说又何尝不是一种煎熬呢?
所以前面谈到的 B 端的前后端分离架构,一经出现就受到了大家一致的好评,纷纷效仿。
对于他们来说,只要负责开发 API 接口就好,可以把重心只专注于业务和功能层面的开发即可。这是一个双赢的过程。
也正是因为这个方式,服务端逐步演进到现在的微服务架构。
随着微服务的逐步推进,特别是在 C 端,传统的 MVC 模式就变得非常不适合了。我们急切需要另一种方式来适应这套微服务架构。
Node.js 框架的成熟
我们还需要谈到一个点 Node.js 服务端框架的成熟。
Node.js 的出现给整个前端界带来了翻天覆地的变化。
我们以前不敢想,不敢做的,通通都成为过去时。
以前你写服务层面的东西可能需要先学习各种各样的语言,现在你可以直接用 JS 来搞。
最初因为 Node.js 服务异步 I/O 理念,很是火了一把,仿佛间到处都在使用 Node.js 做开发。
但我们现在知道最终都只是一时的热点。
Node.js 真正火起来的是在工具和编译上的应用,而不是服务端。
但是服务层面的框架也经历很多年的更新迭代,特别是 async+await 的支持后,整体服务不管是稳定性、可用性、维护性都得到了大大的增强。
再加上像阿里云,特别像淘宝这样的大公司都在大规模应用,都是给 Node.js 服务打了一剂强心剂。
然后就有了基于 Node.js 服务的 C 端前端分离架构。
C端前端分离架构
市面上多数人喜欢把这套架构叫做 BFF架构,也就是 Backends For Frontends (服务于前端的后端)。
我们通过和最前面的一个架构对比,可以看到最大的差异就在于把 Nginx 那一层换成了 Node.js,然后 Node.js 本身又是可以再继续调用 Micro Server。
对于前端来说,就是多了一层前端自己的服务端。就因为有了这一层,你真的可以为所欲为。
比如说,
- 你页面依赖某个接口配置,你完全可以在服务层面定时去获取,然后传到页面。而不是在页面上,每次去获取。
- frog 要 userId,你只需要从 Cookie 中获取,然后传递到页面就好。
- 完美支持 history 模式。
-
… 有很多优点,同时也要看到它的缺点:
- 多人同时开发。
- 跟 B 端类似,项目逐步积累会变得非常的大。
针对多人同时开发的问题
这个模式下,不论哪种架构都会有同样的问题。
传统的团队一般 gitflow 的方式做开发。
这种开发模式比较适合一个开源的项目开发,或者有一个明确目标的项目开发。
但对于业务团队来说,需求繁乱复杂,排期也不固定,上线也不固定。在多人同时开发的场景下,带来的各种冲突和不一致问题。特别是测试环境的部署。
大部分团队的做法,都是在 develop 分支,或者再单独创建一个新的 test 分支来做测试环境部署。然后其他的 feature 分支通过合并到 develop或 test 分支来部署测试环境。
这种情况下,不可避免的把各种代码都放到了测试部署分支上,继而导致这个测试部署分支异常的混乱,必须隔一段时间就去重置一下。
曾经有一个线上故障,说是改了部分代码测试环境测试什么一切都没有。但上线后没有重新完整走一遍流程,就完事了,等到线上报障后定位了很久才发现,测试环境之所以可以是因为,另外一个人在测试环境做了一个修改导致的。
我们的解决办法就是
从 Master 分支checkout 出去
- Master 是主干分支,是准上线分支。所有其他分支都应该以 Master 为准。
- Online 是上线分支,只接收从 Master 上过来的合并提交。
- 每个成员都有自己的一个独立的分支,以及对应的 Node.js Server,所有跟成员自己的需求都在应该这个分支开发,并部署到对应的 Node.js Server 上供测试。
- Master 也有自己的 Node.js Server,并且是最核心的。用于最后的验证。
- Bugfix 分支是紧急上线分支,做完修改后需要合并到 Master 再上线。
- Project 分支是多人共同开发一个大项目时使用,方便及时同步最新的代码。
针对非常大的问题
影响最大的就是开发环境下的静态资源编译。正常情况下都是启动的时候就加载所有的静态资源,但是随着需求越来越多,加载的东西也越来越多,启动就变得越来越慢。
在没有 vite 的情况下,我们用的各个路由独立去编译。
而有了 vite 后,这个基本就不是问题了。
然后就是上线的静态资源打包编译慢的问题,我们的做法就是资源静态化,这是一个折中的方式,最终我们希望达到的是根据内容的改变去动态打包。
这是一个非常有挑战意义的事情。
一个阵痛
如果底层库有大的变动或者变迁的话,涉及到要改的地方会非常的多。
但这个是任意的一个架构都无法避免的事情,但这个做好了就是一劳永逸的事情。
总结
整体看,这种模式下,它是非常一个持续维护和功能迭代的项目,虽然短期会有阵痛,比如做大的版本调整,组件升级,底层库升级。但长期看是非常值得的。特别是不用关心各个系统之间的差异。
有问题直接修改,而不用考虑其他系统。
最佳实践
从公司针对产研的前端团队来说,这样大型的前后端 Node.js 项目控制在 1 ~ 3 个之间会是一个比较好的做法。
过多的话,一样会产生上面说的各个不统一的问题。
当然,如果还能更大,我想目前你这个组也对应的非常庞大,那将会另外一个话题。
展望
当你团队开始在用 Node.js 做服务端层面开发的时候,你就相当于半只脚踏入了服务端领域。
这个时候,你会发现你可以做的事情真的是太多太多,只要不涉及到业务上的,你完全可以不依赖于服务端,什么东西都可以自己搞。
拿我们小组来说,我们最初做活动页面总是要涉及到跟存储相关的,然后我们就设计了一个通用的存储,后续做很多活动页面根本就不需要服务端的参与。
继而在这个通用的存储之上又开发像基地这样的自动化活动页面搭建能力。
再就是接管了我们最核心的课程详情页的详情部分。
但是,现实的窘迫也是存在的。我可以在非业务逻辑上做任意的东西,但涉及到业务层面,都还是被层层剥离了。
我们做过微信定时更新 access_token,但不久后就挪到了服务端统一处理。
我们做过跟 MQ 的消息监听处理,但也转到了服务端去做。
我们做过主动推送微信消息的能力,但最终也转到了服务端去统一管理,并入他们的统一通知处理系统。
不是我们不能,而是我们差得太多。我们要接入的是一个已经非常成熟的 Java 后台体系,但与之对应的是我们还不完全具备这个能力,我们各种能力本身是缺失的。
这个能力主要是两个:人力、配套设施。核心还是人力。
但我觉得只要有更多的人加入,总是会有突破的那一天。
不积跬步,无以至千里。任重而道远,各位~