前端开发由于单页应用的兴起,前端是越来越工程化,代码量越来越大。不可避免的需要将工程解构,然后分派给各个团队单独开发。
我们希望能类似后端微服务的效果,独立开发,独立部署,而对用户而言是无感知的。
First Split
于是我们基于页面按照团队拆分,将不同的业务页面按业务分成不同的业务线,然后独立开发,独立部署。本质上不同团队已经在开发不同网站了,不过通过一层代理层将各个站点连接起来,让终端用户认为他们仍在使用同一种应用。
此拆分可以将业务部分拆分较细粒度,允许团队独立开发,最后仅仅需要测试人员根据业务要求验证各个业务集成时候是否符合需求即可。
对于大部分开发场景而言,此方案就足够了。不过既然不同团队开发的站点其实是同一个应用,页面上可能有很多共同元素(比如常见的DashBoard应用常常有用户Profile元素,以及切换业务用的Menu)。业务切换回导致页面的Reload,共有元素必定会导致浏览器重新执行,而且除此之外,单页应用框架现在都比较重,首屏加载中框架的启动会占据大部分时间,从而每次业务切换Reload总会导致一部分JS引擎执行时间花费在这些公共部分的重新渲染上,造成用户体验上感觉加载性能慢。
Next Split
虽然上诉方案带来了非常大的开发便利性,但是导致了的用户体验的下降。
我们希望能保持上诉方案中,既能保持各个团队独立开发,独立部署的好处,又能不牺牲页面切换中,由单页应用拆分后带来的应用框架重复价值的性能损失。
所以我们在前诉方案的基础上尝试了我们称为Dynamic Route Loading的方案尝试,确实提升了用户体验的上升,但也带来了很大的缺陷,可供大家作为思路参考。
本思路的核心为:当前的单页应用均具有路由功能,并且均能够提供Lazy Load Route的特性。如果我们能够通过某种手段欺骗单页应用的框架,让他能够拿到拆分过的Lazy Load Route文件,进而去加载对应页面,那么我们就可以做到无需应用框架重启,便可以加载不同业务。
那么我们可以通过Nginx的反向代理功能,让应用能够顺利拿到对应路由的代码,此方案的关键在于如何确保共有代码绝对一致,从而保证框架能够一直认为其为一个完整的应用。
自然可以得出,我们必须保证开发中加入一些限制条件,才能保证本方案能够被顺利执行。
- 每个工程必须拥有所有业务的全量路由表:因为既然在浏览器上需要保证为一个单页应用的形式,那么应用必须知道所有应用的路由,才能正确地加载应用。
- 公同依赖(包含各个业务开发中没有抽离到初始化代码中,但是都会使用的依赖,比如lodash)需要一致:应用需要能被正确执行,那么就必须保证页面切换中,页面中所有部分的代码都是基于同一套基础依赖的,不然就无法保证应用能够被正确执行,比如A,B站需要互相无刷新页面切换,A站使用React开发,B站使用Angular开发,两天路由加载的逻辑根本的不同,你无法使用React的路由去加载Angular的路由代码,反之亦然。当然在实践执行中,你即便使用的同一套框架,很多细节的代码也必须保持一致才能保持整个大应用能准确无误的被执行。
凡事必有代价,这个方案在我们实践中也带来了不少问题。
你别看上面两项限制看起来简单,但是因为我们的基本思路是基于某个现有的前端框架,然后将不同站点通过Nginx等代理软件,伪装成同一个单页应用的一部分来提示用户体验。不同业务必须要保证其均能够运行在同一套代码上下文下,这本身就是本末倒置,之前方案一的出发点就是为了解耦应用,让业务开发者不需要了解其他团队怎么开发,只需要保证符合业务需求即可。但是此方案导致:
- 开发者必须使用同样的开发框架,本身就和微服务的开发框架选项无关性背道而驰。
- 因为应用运行在同一套框架执行上下文上,而开发者团队又是在独立开发和部署的开发模式下。必然导致线上线下环境的割裂,很多联调的问题只会在上线部署配置完成后,一些执行上下文的细微区别的影响才有可能会被暴露出来,轻则业务切换B后应用行为和单独访问B的时候不一致,重则加载B站会导致应用崩溃。
这个方案下,由于环境的割裂,业务开发者又不可能去了解整个应用的开发进展,必然导致很多错误会被推出到上线,而排错又需要了解整个运行原理和所有团队的开发进展,导致排错困难。错误又难以在上线前排查出来。
这个方案而言,我们又进一步做了一些优化方案,其中比较好的方案是将公共依赖尽可能的抽出成为CDN,并且我们保证CDN上的版本总是最新的版本,这样很多依赖版本导致的问题可以结合一些开发部署策略尽可能避免,这样基本能够被应用起来,但不得不说,离真正的前端微服务而言,还有很长的路。
Other Solution Idea
还有其他的一些解决思路,可以参考下这篇文章Building Microfrontends,其中最有希望的是Web Component方案,不过这个方案基于浏览器标准的实现,而且每个组件部分其实完整的应用,可能会引入一定的代码冗余,但是是能满足我们在独立部署下不刷新切换页面愿景的,而且解耦的粒度更下,是页面上的组件级别,是相对有潜力的一个方案。