前端巨型项目拆分与整合原则方案

Posted arsiya_jerry

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端巨型项目拆分与整合原则方案相关的知识,希望对你有一定的参考价值。

前端项目组织原则

微服务下的前端项目

​ 随着微服务与容器化技术的兴起,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

公共组件项目

​ 创建公共依赖项目,包括公共组件,公共样式,Excel导出工具等,要求上传npm私服

vue-cli 模板

​ 目的:使用vue-cli创建业务项目,项目中预置公共组件项目依赖

静态资源nginx服务

​ 用于提供所有项目的index.html需要引入的dll内容。

微服务服务拆分原则 与 RestTemplate远程调用

(目录)


服务拆分和远程调用


服务拆分原则

这里总结了微服务拆分时的几个原则:


服务拆分示例

服务拆分注意事项:

cloud-demo:父工程,管理依赖

要求:


实现远程调用案例

在order-service服务中,有一个根据id查询订单的接口:

微服务项目下,打开 idea 中的 Service,可以很方便的启动。

根据id查询订单,返回值是Order对象,如图:

user 为 null


在user-service中有一个根据id查询用户的接口:

查询的结果如图:


1.需求:


大概的步骤是这样的:


2.注册RestTemplate

Spring 提供了一个 RestTemplate 工具,只需要把它创建出来即可。(即注入 Bean)

package cn.itcast.order;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication 

    public static void main(String[] args) 
        SpringApplication.run(OrderApplication.class, args);
    

    @Bean
    public RestTemplate restTemplate() 
        return new RestTemplate();
    


3.实现远程调用

发送请求,自动序列化为 Java 对象。

@Service
public class OrderService 

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) 
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.远程查询 user
        String url = "http://localhost:8081/user/"+order.getUserId();
        // 使用 restTemplate 发送 http 请求
        User user = restTemplate.getForObject(url,User.class);
        //3. 封装 user 信息
        order.setUser(user);
        // 4.返回
        return order;
    


提供者与消费者

在服务调用关系中,会有两个不同的角色:

但是,服务提供者与服务消费者的角色并不是绝对的,而是相对于业务而言。

如果服务A调用了服务B,而服务B又调用了服务C,服务B的角色是什么?

因此,服务B既可以是服务提供者,也可以是服务消费者。


以上是关于前端巨型项目拆分与整合原则方案的主要内容,如果未能解决你的问题,请参考以下文章

带你快速了解微前端的拆分和集成

微服务架构的理解

论微服务拆分

微前端面试题

微服务的服务拆分

当 Vite 遇上微前端