前端巨型项⽬拆分与整合原则⽅案
前端项⽬组织原则
微服务下的前端项⽬
随着微服务与容器化技术的兴起,web项⽬变得不再像原来的单体应⽤项⽬那样庞⼤,通常以单⼀服务功能的实现为原则,服务端应⽤被拆分成了⼀个个的互不依赖的⼩型项⽬。被拆分为独⽴服务的这些⼩型项⽬可以被独⽴的开发、测试、维护,部署,和版本迭代,不⾄于像原单体项⽬⼀样,因为任意模块的微⼩的变更⽽触发所有模块的重新上线流程。微服务时代的服务端应⽤不再以业务模块(条线)进⾏项⽬组织,⽽是被拆分为更细⼒度的微服务项⽬。基于容器化平台部署体系(paas),使得这些微服务项⽬组成的分布式服务体系(saas)体现出了巨⼤的灵活性和可伸缩性。
那么在单体应⽤时代⼀直作为服务端项⽬⼀部分的前端代码,在微服务时代应该如何组织呢?是否也可以像微服务那样,进⾏对应微服务维度的拆分?答案是否定的。因为服务虽然可以拆分,但业务流程却不可能被拆散,服务必须在更⾼的层⾯被编排和整合,才能满⾜实际的业务需求,仅有提供单⼀基础服务的微服务项⽬集⽆法组成完整的业务应⽤。在具体实践中,服务的整合和编排通常可以在两个层⾯中进⾏:前端模型层或者BFF层(Backend for Frontends,为前端⽽存在的后端),这两个层⾯根据业务逻辑对服务进⾏串联和组织,使他们成为有机的,不可分离的应⽤整体。
前后端分离之后的前端项⽬,承担着具象化⽤户与业务流程交互的责任,因此⽆法像微服务后端项⽬⼀样进⾏拆分。正因如此,当下的前端开发框架和项⽬结构,呈现出了微服务技术出现之前的web项⽬风貌,即以业务流程为逻辑和代码分块原则,进⾏项⽬内部的模块化分隔,但仍然进⾏集中开发和构建的单体应⽤项⽬形态。
前端项⽬的拆分与整合
随着项⽬中业务模块的积累,前端项⽬也必然会变得臃肿(如依赖冗余),造成开发和上线过程的恶化。因此单⼀前端项⽬不能⽆限膨胀,必须使⽤某种机制,采⽤⼯程化的⼿段对项⽬进⾏合理的拆分,使其保持敏捷开发的实践要求;同时⼜必须保持⽤户与业务交互层⾯的⼀致性,不能因项⽬拆分⽽造成⽤户体验的割裂。要保证敏捷,就必须是⼀个个独⽴且完整的项⽬,使其开发和部署过程都不受其他项⽬影响。要保证⼀致性,则必须使项⽬存在整合能⼒,使其可以在需要的时候表现为⼀个整体。
业务模块是前端项⽬可以逻辑拆分的最⼩单位,⼀般情况下,业务流程内部的页⾯与页⾯之间存在着紧密的动态交互,⽐如相互之间的跳转,数据交流与共享;⽽业务流程间则不存在交互,彼此在页⾯和数据层⾯都相互隔绝,因此我们可以从是否存在交互为原则,将前端项⽬拆分为彼此独⽴的业务模块,这些拆分后的模块在代码组织上可以作为独⽴的前端项⽬进⾏开发和部署。当然,独⽴的模块间也可以合并为更⼤的项⽬进⾏部署。
因为拆分之后是⼀个个的独⽴且完整的前端项⽬,拥有各⾃独⽴的访问地址(域),独⽴路由和数据状态控制机制,这为项⽬间的整合带来了很⼤的挑战,⽐如需要解决⽤户登陆状态共享等问题。幸运的是,前端项⽬拆分的特点(进⾏业务维度拆分)与微服务化之前的web项⽬的整合特点⾮常相似,以上这些问题在SOA之前的web项⽬整合过程中已经遇到过,这为SPA时代的前端项⽬拆分提供了有益的参考和借鉴。
前端项⽬的构成
我们将前端项⽬整体分为业务流程型项⽬和⾮业务流程型项⽬。业务流程型项⽬具有可以被路由的页⾯和与业务耦合的前端数据模型,可以单独部署为前端服务也可以与其他业务模块共同部署;⾮业务型项⽬则⼀般指的是⼯具型或者组件型项⽬,⽤于提供公共性功能,被业务型项⽬引⼊和依赖,实现业务型项⽬某⼀部分通⽤性需求。
进⼊单页⾯应⽤(SPA)+ 前端 MVC 时代的前端⼯程,每⼀个业务型项⽬通常由以下元素组成:
route-controller:路由控制器。实现页⾯切换逻辑,控制页⾯跳转。
page-component:页⾯组件(视图)。可以路由进⼊的页⾯,是UI效果图的实现,⽤于展⽰数据渲染效果,不包含页⾯交互逻辑,⼀般也不包含页⾯数据模型。
page-controller:页⾯控制器。持有页⾯的数据模型,管理页⾯数据状态,实现页⾯中应该包含的交互逻辑。页⾯控制器只负责本页⾯数据状态的管理,⽽对于多个页⾯共享的数据,则由专门的model-store负责。
model-store:跨页⾯数据状态管理。维护公⽤数据的状态变化逻辑,保证数据⼀致性。
api:服务端接⼝调⽤逻辑。
ui-components:ui组件集。构成静态页⾯的可复⽤ui组件。
utils:⼯具库。可抽离的公共逻辑。
单页⾯应⽤的路由控制
任何web项⽬通常都拥有不⽌⼀个交互页⾯。从浏览器看,每⼀个页⾯都对应了⼀个路由地址(uri),当我们在浏览器的地址栏输⼊相应的路由地址,服务端就会返回该地址对应的html⽂档以供浏览器渲染。
在典型的服务端mvc中,服务端的控制器在接收到浏览器的路由请求后,将根据计算结果选择返回具体的jsp⽂件,这个过程被称为路由跳转。我们可以认为服务端存储了多个待跳转的页⾯,且路由跳转
逻辑都由服务端控制,⽐如说服务端如果判断⽤户未登陆即访问的特定的页⾯,则直接跳转到登陆页⾯(也就是返回登陆页⾯对应的jsp或者html⽂档)。
前后端项⽬分离后,服务端只负责提供ajax服务,不再⽣成和返回html⽂档,当然也不会负责控制路由的跳转,这些⼯作必须由前端项⽬⾃⼰实现。
建⽴在虚拟DOM技术之上的前端开发框架,赋予了前端强⼤的页⾯元素替换能⼒,于是我们将页⾯上的UI元素设计为可替换的ui组件,通过组件的替换和重新渲染来实现页⾯的更新。既然页⾯中的元素可以成为组件,那么⼀个完整的页⾯同样可以称之为⼀个组件,我们称之为page组件(page-component),或者可路由组件。因此,当下的前端页⾯不再需要各⾃独⽴的html或者jsp⽂档承载,他们被虚拟化为了⼀个个page组件,体现在代码中则是⼀个个的javascript对象。这样⼀来,所有的页⾯都只需要⼀个html⽂档的body标签作为dom的挂载点,我们建⽴特定路由地址与page组件的对应关系,通过感知浏览器地址栏的变化,根据对应关系替换(挂载)指定的page组件,即实现了路由的跳转,这便是单页⾯应⽤的路由控制实现原理。
前端MVC体系
实现视图与数据模型的解耦⼀直是web开发不变的追求,特别是在⽤户体验极致化和前端设备多样化今天,分离视图渲染逻辑和数据处理逻辑有着更为⼴泛的意义。⽐如js-native技术允许我们使⽤js语⾔
开发移动端app应⽤,这使得⼤前端跨平台⼯程开发成为现实,在这种情况下,视图的渲染载体不再是单⼀的PC端浏览器,它还可能是移动端浏览器、原⽣app应⽤,甚⾄⼩程序等;⽽且,同⼀种渲染载体在相同的业务需求下也可能有不同的渲染要求,这要求我们必须认真考虑视图的可替换性和数据处理逻辑的通⽤性。
page-component 应该尽可能的剥离出与业务相关的数据状态控制逻辑,以保证视图的可替换性。这不是说视图中不能存在数据和数据处理逻辑,⽽是说视图中的数据处理应该是业务⽆关的,只⽤于⽀持视图本⾝渲染需要,⽐如⽤于适配渲染环境。视图组件都是由更⼩的ui组件组合⽽成,任意⼀个前端ui组件,⽆论是⼩到⼀个按钮,还是⼤到⼀个page组件,都应该遵循这样的原则。以该原则实现的ui组件,也被称作受控组件,即组件本⾝是⽆状态的,且不能改变外部注⼊数据的状态,但根据注⼊的数据状态必然能渲染出确定的结果,它是函数式编程在视图渲染中的体现。
与业务相关的数据模型和处理逻辑则由专门的page-controller(也成为控制组件)负责,也就是page组件的数据注⼊⽅。页⾯控制器响应page组件的数据变更请求(通常由⽤户操作触发),通过⾃⾝逻辑处理或与服务端进⾏数据交互以完成数据状态的变更,从⽽引发页⾯的重新渲染。理想状况下,页⾯控制器作为页⾯的数据模型,其代码不受page组件替换的影响。页⾯控制器持有本页⾯所需的数据模型,除此之外还存在⼀些跨页⾯共享的数据以及全局性数据,这些数据则使⽤专门的model-store进⾏管理,以保证数据状态的⼀致性。
⾮业务型项⽬
在业务型项⽬中,除了构成交互主体的页⾯组件(page-component)和其对应的数据模型外,还包含了很多与业务⽆关的元素,它们⼀般可以分为以下⼏种类型:
⼯具库:与业务解耦的通⽤⼯具模块,如报表导出,打印等;
UI组件库:经过封装的可复⽤ui组件,如表格,表单,导航等;
通⽤系统级功能模块,如鉴权,⽤户登陆状态控制。
这些元素是所有项⽬中不可或缺的组成部分,且具有跨项⽬的可复⽤性,在被拆分的项⽬中更应该保持⼀致,所以应该把他们创建为独⽴的项⽬,进⾏独⽴的开发迭代,并纳⼊前端依赖体系,由业务型项⽬以依赖的⽅式引⼊。
前端项⽬的整合
前端项⽬整合的⽬的是为了保持⽤户与业务交互层⾯的⼀致性,也就是说虽然前端项⽬按照业务模块拆分成了独⽴部署的多个服务,但在⽤户操作层⾯上却可以不受项⽬拆分的影响,仍然可以看成单⼀的服务。这要求我们所有的前端项⽬在部署为服务后具备:
统⼀的页⾯⼊⼝
单点登陆
⼀致的页⾯风格
统⼀的菜单和页⾯跳转控制
前端门户项⽬
要做到以上功能,我们⾸先需要⼀个前端门户项⽬,以提供项⽬间整合的平台。前端门户项⽬的职责在于提供统⼀的登陆⼊⼝,实现⽤户状态的管理,并提供对其他项⽬的路由分发。根据应⽤场景不同,可以分为两种类型:
门户项⽬只负责⽤户登陆,然后提供其他项⽬的跳转⼊⼝,通过⼊⼝转⼊具体的项⽬中,也就是说路由的分发粒度为项⽬级。该类型的门户实际上主要提供了单点登陆能⼒,适合独⽴性⽐较强的前端项⽬间的整合。
门户项⽬提供统⼀的页⾯框架和各个项⽬的菜单⼊⼝,⽽业务项⽬的页⾯则直接嵌⼊到门户项⽬之中,路由的分发粒度为菜单级。该类型的门户要求其他项⽬必须适配门户的菜单控制,⽽⾃⾝将失去独⽴提供服务能⼒,适合⼤型项⽬拆分后的⼀致性整合,我们以下内容全部就该类型展开讨论。
⼀定意义上,门户项⽬承担了前端版“⽹关”的⼯作,门户项⽬和业务项⽬独⽴部署,但其他服务将访问地址“注册”到门户,也就是说除了门户服务,其他项⽬提供的服务对于⽤户都是不可见的,只有注册到门户的服务(页⾯)才能被⽤户访问。
前端服务的注册
在这⾥,我们将业务项⽬中可以被路由的页⾯称为⼀个前端服务,前端服务页⾯不能独⽴渲染,必须嵌⼊到门户中。我们需要通过特定机制将已部署的前端服务注册到门户,使得门户可以通过菜单路由到指定的服务页⾯。⼀个简单的实现⽅案如下:
1. 发布菜单信息到门户,⼀个业务项⽬中可能发布多个菜单。
2. 在门户配置业务项⽬的访问地址。
以上两个步骤的实现可以有3种⽅式:
由服务端菜单管理系统配合存储业务项⽬发布的菜单和对应的uri,门户加载即可。
由门户系统⼿⼯配置业务项⽬发布的菜单。
门户提供注册服务(需要门户以nodejs等状态部署),业务项⽬部署后执⾏注册服务请求。
业务项⽬的⼀致性实现
为了实现与门户的整合,业务项⽬必须符合⼀些规范性要求:
业务项⽬的页⾯元素在视觉和交互表现上应该⼀致,也就是他们依赖相同的样式和ui组件实现。
业务项⽬必须提供符合门户要求的菜单结构,并具备相同的权限控制功能。
业务项⽬必须具备与门户的通讯能⼒,并符合与门户的信息交流规范。
如果有必要,业务项⽬需要具备服务注册能⼒。
可以看到,以上都是与业务⽆关的技术性要求,因此可以通过公共的⽅式实现,实现⽅式可以有两种:
发布公共的业务项⽬模板,由项⽬模板以源码的⽅式实现⼀致性规范;
发布规范的实现到前端私服,由业务项⽬⾃⾏以依赖⽅式引⼊。
⽽且我们还可以将以上两种⽅式结合,发布bus-cli⼯具项⽬,使⽤⼯具项⽬完成业务项⽬的搭建⼯作。
具体开发计划
要点
vue,element-ui、axios 等公共依赖不允许打包,以dll形式从index.html引⼊
公共组件如Table,Excel导出⼯具等, 以项⽬形式上传npm私服
所有项⽬依赖版本统⼀
⾃定义vue-cli模板
依赖最⼩化
门户项⽬
要求:1、vue,element-ui、axios 等公共依赖不允许打包,以dll形式从index.html引⼊;2,依赖最⼩化,即package.json中只保留项⽬中需求的依赖。
功能:登陆,主页,Layout
公共组件项⽬
jsp用什么前端框架
创建公共依赖项⽬,包括公共组件,公共样式,Excel导出⼯具等,要求上传npm私服
vue-cli 模板
⽬的:使⽤vue-cli创建业务项⽬,项⽬中预置公共组件项⽬依赖
静态资源nginx服务
⽤于提供所有项⽬的index.html需要引⼊的dll内容。