背景

公司有一个历史悠久、体积庞大的老项目,除了传统老旧项目存在的特点,该项目强依赖后端模板,且每一个页面都对应一个模板,启动依托于docker,随着项目不断更新迭代,项目结构越来越复杂,维护成本越来越高,进而引发一系列的问题…

巨石应用存在的问题

  • 项目体积过大,clone时间长

  • 结构混乱,高度耦合,难以维护

  • 需要解决构建时间长的问题,需要维护复杂的webpack配置

  • 多人开发同一个项目,发布部署容易混乱

  • 技术栈迭代成本高昂

如何拆分?

既然是巨石应用,很容易想到的改造方案就是拆分成多个子应用,分而治之,通常来说有以下几种拆分方案:

  • 按技术拆分

  • 按照业务或者页面拆分

  • 按照权限拆分

  • 按照变更的频率拆分
    我们的项目不存在权限限制以及技术栈不统一的问题,显然这两种拆分方式都不合适,在我们的业务场景中,一个项目是由多个页面组成的,每个页面之前的状态相互独立,显而易见的直接按照业务拆分即可,这里推荐madge工具,它可根据你指定的入口自动为你找到入口的依赖,并将其从主项目中剥离出来,拆分完成之后,又该采用哪种架构方式呢?

传统解决方案

muti repo

也就是直接将其拆分成多个仓库,简单粗暴但存在不少问题:

管理、调试困难

多个 git 仓库管理起来天然是麻烦的。对于功能类似的模块,如果拆成了多个仓库,无论对于多人协作还是独立开发,都需要打开多个仓库。
虽然 vscode 通过 Workspaces 解决多仓库管理的问题,但在多人协作的场景下,无法保证每个人的环境配置一致。
对于共用的包通过 Npm 安装,如果不能接受调试编译后的代码,或每次 npm link 一下,就没有办法调试依赖的子包。

分支管理混乱

假如一个仓库提供给 A、B 两个项目用,而 B 项目优先开发了功能 b,无法与 A 项目兼容,此时就要在这个仓库开一个 feature/b 的分支支持这个功能,并且在未来合并到主干同步到项目 A。
一旦需要开分支的组件变多了,且之间出来依赖关联,分支管理复杂度就会呈指数上升。

依赖关系复杂

独立仓库间组件版本号的维护需要手动操作,因为源代码不在一起,所以没有办法整体分析依赖,自动化管理版本号的依赖。三方依赖版本可能不一致:一个独立的包拥有一套独立的开发环境,难以保证子模块的版本和主项目完全一直,就存在运行结果不一致的风险。

占用总空间大

正常情况下,一个公司的业务项目只有一个主干,多 git repo 的方式浪费了大量存储空间重复安装比如 React 等大型模块,时间久了可能会占用几十 GB 的额外空间,对于没有外接硬盘的同学来说,定期清理不用的项目下 node_modules 也是一件麻烦事。

不利于团队协作

一个大项目可能会用到数百个二方包,不同二方包的维护频率不同,权限不同,仓库位置也不同,主仓库对它们的依赖方式也不同。一旦其中一个包进行了非正常改动,就会影响到整个项目,而我们精力有限,只盯着主仓库,往往会栽在不起眼的二方包发布上。

代码无法复用

之前我们说过我们的项目强依赖于后端,如果直接了当的拆分成多个项目的话,后端代码也会存在于多个仓库,但是由于整个的改造我们仅仅只想针对于前端,需要对后端同学是无感知的,显然这种方式无法做到

现代化解决方案

monorepo

是管理项目代码的一个方式,指在一个项目仓库 (repo) 中管理多个模块/包 (package),不同于常见的每个模块建一个 repo。

优势:

  • 多个项目一套配置但是可以分批构建分开发布

  • 开发阶段不用来回切换项目

  • 可以共用代码 同时不用发布即可本地调试公共包

  • 简化的依赖管理,无需为依赖项指定版本号,因为所有项目只有一个通用版本号。

  • 依赖可复用

劣势:

  • 仓库体积较大

  • 提交记录容易混乱

  • 因为是一个仓库,不好对子项目做单独的权限处理

  • 不适合做自动化发布 多个项目一般都是独立的发布不好统一

  • 需要自己管理依赖关系

  • 提交历史容易混乱,需要自己管理

适用场景:

适合内聚性比较强的工具类、框架类工程,例如react相关、 Angular相关,一般发布会有一系列的依赖同步更新;同时这种工程发展是可控的,不会随意膨胀,所以库的体积也是可控的。

不适用场景:

  • 体积不可控的项目

  • 需要做权限管理的项目

  • 不适用子项目技术栈差异大的场景

回到我们的业务场景,由于各个子应用之间的构建存在差异没必要共用一套构建配置,且存在项目体积不可控、目前只支持全量发布等因素,因此未采用改方案

git submodules + muti repo

什么是git submodules?

允许您将 Git 存储库保留为另一个 Git 存储库的子目录,这使您可以将另一个存储库克隆到您的项目中,并将您的提交分开,子仓库包含了一整个完整的git仓库,甚至包含了.git目录。
我司采用的也是该方案,在我们的场景中可以将巨石项目中的各个页面拆分成一个独立的项目,然后将所有页面都依赖的底层服务以及公共组件、公共方法等抽离出来作为子仓库,修改子仓库的时候无需切换项目只需cd ${子仓库目录}即可。

优势

  • 代码可复用,调试成本低:拆分出子模块之后,也能做到可以共用一套公共代码而无需安装npm包并且可以直接本地调试

  • 可以直接在拆出来的项目中,运行submodule的代码:因此可以在submodule中放一些公共服务,适用于依赖于docker或者是前后端未完全分离之类的项目,比如在我们的业务中,只需子仓库维护一套后端模板与docker配置,运行子仓库的宿主仓库的时候只需cd ${子仓库目录}运行容器,然后将宿主仓库的端口代理到容器运行的8083端口即可,在发挥了多仓库优势的同时保证了基础服务(后端代码)的唯一性

劣势

  • clone的时候需要额外clone一下子项目,如果子仓库体积过大就会造成clone时间过长

  • 修改子项目的时候需要切换到子项目的仓库

  • 相比npm包的形式,只要修改了submodule中的代码就会牵一发而动全身,存在风险

  • 只是对复用代码提供了解决方案,其他传统多仓库的问题还是存在

webpack5模块联邦(Module Federation)

也是一种提取公共模块的方案, 每个子应用单独构建,主应用在运行时通过容器加载远程模块即子应用的构建。主应用自动使用子应用当前版本的最新资源,一句话概括就是:它能允许一个线上部署的项目在运行时加载其他线上部署项目中的组件。

基本原理

设想一下,在 webpack 中可以将一些后续加载的模块打成 chunk 包,然后在使用到这个模块的时候再懒加载,这种情形一般是在同一个项目中进行。
那么我们是否也可以在 A 项目中把一个组件及其依赖打包成 chunk 包,而在 B 项目中按约定的地址异步 import 刚才那个 A 项目的 chunk 包,并运行使用呢?答案就是 Module Federation。

基本概念

  • 远程组件提供方称为 remote 端,远程组件使用方称为 host 端。

  • 一个项目既可以为 remote 端,也可以为 host 端,可以使用数个不同其他项目的组件,也可以为数个其他项目提供不同的组件。

具体怎么工作?

例子:项目要引入一个工单工作台的页面,如下:

工单工作台作为 remote 端的配置

remote端(工单工作台)

(1) 由于我们两个项目都使用的是 vue3 + vite 方案,所以使用 vite-plugin-federation 插件是我们最佳的选择。首先需要在 host 端和 remote 端安装插件,如下命令:

yarnadd@originjs/vite-plugin-federation-D

(2) 在插件内注册需要提供的组件,并且提供需要共享的三方依赖

(3) 配置完成后打包
在 remote 端打包后会生成对应的入口文件,chunk 文件及依赖包文件

host端(IM 和电话工作台)

(1) 插件配置,设置远程包地址

(2) 引用及注册组件,main.ts 文件如下

这时需要注意,ticket-share 为 host 侧在 vite 插件中注册的包名,Detail 为 remote 端暴露(exposes 字段)声明的组件名。除了在全局注册,还可以在任意需要引用的地方引用。
(3) 组件的使用
在需要的地方引入使用,并根据业务情况传递 props,props 的使用方式与普通组件无异。

此时我们已经完成了全部配置,此后我们只要打包上线运行即可。

线上如何调试

使用Redirector 浏览器扩展:
假设微应用程序的远程入口文件在https://my-federated-app.com/federated/my-micro-app/remoteEntry.js. 然后,如果我想使用本地开发服务器处理此应用程序,我可以将包含该模式的任何请求重定向/federated/my-micro-app/到http://localhost:3000运行开发服务器的位置

优势

  • 更新成本低:引入公共包不需要打包再发布,相比npm包,不需要挨个升级使用该包的项目的依赖版本,他也适用于复杂度高的,业务关联性高,更新频次高的三高页面级组件

  • 技术成本低:只需要修改一下webpack配置即可

  • 支持延迟加载:bundle以仅在必要时加载模块,从而提高 Web 性能。

  • 减少重复性依赖:引入的公共代码可以直接使用宿主环境的依赖

  • 灵活拆分,细粒化程度高:相比git submodules,可以颗粒化到单页应用或者组件。也就是说他也可以代替iframe

劣势

  • 没有样式隔离机制,容易造成主子应用样式污染

  • 本地开发需要开启多个端口的服务,比较麻烦

  • 共享包的维护成本高:需要将组件共享的包都找出来,共享有时是强制的,如果漏了某些包,可能会导致页面报错或崩溃,同一个库,有时需要维护在约定的版本范围内,有时一个包太老或太新也会在加载时报错(不过是否需要共享可供用户选择)

  • 需要合理规划chunk拆分,如果拆分chunk过多会导致请求Chunk数量过多,并发量过大,如果多个模块共用一个remoteEntry又无法对其中包含的模块的版本做单独管理

回到我们的业务场景,由于模块联邦只能复用JS chunk,对于我们依赖的后端服务无法复用,因此无法作为基层解决方案,但是他能解决目前代码复用方式存在的问题:由于我们的各个子项目都依赖了submodule中的公共组件、公共方法,每当修改一次这些公共组件、方法的时候,无论子应用需不需要修改调用方式,都需要在每个应用到这些公共组件、方法 的应用重新编译打包,显然影响到了效率,利用模块联邦的特性,我们可以在保证修改无副作用的前提下,直接上线submodule的内容,对于子应用是无感知的,只是cdn链接指向的Js chunk的内容变了而已

微前端

微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。微前端架构与框架无关,每个微应用都可以独立开发、独立运行、独立部署。

qiankun

乾坤是市面上常见的微前端框架,以此为例说说他的特性:
qiankun采用HTML Entry 方式进行了替代优化。

什么是HTML Entry” />