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