多项目公共代码管理方案:npm包 、git submodule/subtree 、模块联邦、Monorepo

3,545 阅读11分钟

选手介绍

npm 发包

把公共文件放到node_modeules中管理。

把自己项目的封装的公共组件,发一个包到npm服务器,或者自己公司搭建的私服npm服务器,简单快捷。

但是对于某些情况可能不适用,比如在微前端项目中,对于一些公共的配置,如公共ajax配置、css等,发包就显得不是特别合适。

npm 打包大概流程
1. 去npm 官网注册并登录
2. 在本地黑窗口登录刚刚申请的npm账号 `npm login`
3. 在需要打包的文件夹中打开黑窗口,执行 `npm publish`

git subtree / submodule

把公共文件放到一个文件夹下,并用新的git仓库管理。

正常来讲,我们一个项目只用一个git仓库管理。而git subtreegit submodule允许我们在主仓库下面,链接一个新git子仓库,去管理一部分内容。可以想象一下,我们的公共配置、组件库被子仓库管理,同步到其他项目中的时候,只需要让别的项目拉一下代码再打包发布就可以。

git subtreegit submodule的区别:

使用git submodule方式连接主仓库,主仓库只记录连接子仓库的分支已经连接的commit版本,主仓库看不到里边的内容,子仓库完全独立的。类似于我们发了个包到node_modeules中,并且指定了版本,并且有它自己的更新命令。

使用git subtree方式连接主仓库,那么这个子仓库就相当于主仓库中的一个普通的文件夹。主仓库对子仓库有权限控制。

## 添加子项目

命令:
``
git subtree add --prefix=<存放子项目的相对路径> <子项目git地址> <分支> --squash
``

例如:
``
git subtree add --prefix=src/Common https://github.com/M76chao/Common.git master
``

问题:
遇见报错Working tree has modifications.  Cannot add.
先commit当前更改,再重新执行上述命令


遇见报错:SSL certificate problem: unable to get local issuer certificate
管理员命令行,执行`git config --system http.sslverify false`


## 修改子项目代码
在原项目中直接commit就行,无需其他特殊配置,建议子项目代码单独commit,不要跟外围代码一起提交

## 推送子项目代码 push

命令:
```
git subtree push --prefix=<存放子项目的相对路径> <子项目git地址> <分支> --squash
```

例如:
```
git subtree push --prefix=src/Common https://github.com/M76chao/Common.git master
```

## 拉取子项目最新代码 pull
命令:
```
git subtree pull --prefix=<存放子项目的相对路径> <子项目git地址> <分支> --squash
```

例如:
```
git subtree pull --prefix=src/Common https://github.com/M76chao/Common.git master
```

## 其他 git subtree split 当commit记录特别多的时候,使用这个命令,可以提高push 的效率
命令:
```
git subtree split --prefix=<存放子项目的相对路径> --rejoin
```

例如:
```
git subtree split --prefix=src/Common --rejoin
```

查看更多内容,请看这个文章:Git subtree用法与常见问题分析 - 知乎 (zhihu.com)

模块联邦

把公共文件以类似(微前端/CDN)的方式引入到项目中。Webpack 5发布的新内容,允许模块独立开发、独立部署,类似于借鸡生蛋。

比如,原来项目中已经有个组件1,现在你又要写一个新项目,也用到了这个组件1,那么可以直接引用原项目的生产地址中的组件1,本地就不打包这个组件1了。因为本地没有这个组件,所以当远程的这个组件发版了,你们项目会随着改动,不用发版。

十分建议各位看下这个文章: 架构之路-你不知道的git Submodules,monorepo,Module Federation - 掘金 (juejin.cn),并且把他的demo(github)跑一遍,这个demo跑完,我一下子就明白模块联邦是什么意思了。

当然了,vite社区也维护了一个模块联邦的插件:@originjs/vite-plugin-federation

我自己试了一下,发现了一些问题,个人觉得,在开发体验上来说,不如Webpack。

vite问题:

  1. 项目A中,组件1引入了组件2,而我们只把组件1暴露出去。项目2引入了组件1并使用,此时就不会正确解析组件2。
  2. 作为服务组件所在的项目,必须发版之后才能被其他项目所依赖。因为开发环境和生产环境的目录结构不同,其他消费者项目,不容易找到开发环境的组件地址。
  3. 暴露出去的组件,不允许出现textnode,否则会警告,并丢失该文本。如下,“你好,我是”这几个文字就消失了
<h1>你好,我是<span>{{name}}</span></h1>

还有就是,所有模块联邦都有的一个问题:编辑器联想功能缺失。毕竟是以外链的方式引入的,代码没有在本地中,编辑器并不知道里边到底有什么。

另外多说一嘴,前段时间看这个文章: hel-micro 模块联邦新体验 - 掘金 (juejin.cn) ,它和评论区提出的 bit.dev 差不多,大体上都是把组件单独放在一个服务器上,其他项目可以直接引用这些组件。但我个人觉得,这样的做法有些偏激了,我们或许有其它更重要的事情要做,而不是在这个非常窄的领域继续拓宽。

Monorepo

把公共文件放到整体项目之外,以类似相对路径的方式引入到项目中。

Monorepo是一种将多个项目代码存储在一个仓库里的软件开发策略。比如vue3就是基于这种设计开发的。对于公共组件库开发特别友好,因为可以不用发npm包就能用到最新代码,甚至还支持热更新。

image.png

具体操作可以看这两个文章:

monorepo是一种非常好的公共代码管理方式,但我们平时开发项目时,用不到它最好的地方,毕竟我们的公共代码很少会经常改动。并且它把公共代码和业务代码完全分开,这样在类似于 Jenkins 的自动发版工具中,就会很麻烦。并且也不利于项目组成员之间传播:成员需要先clone基座项目和公共项目,然后才能在对应位置clone业务项目。

所以个人建议还是老老实实的用monorepo开发npm吧,它不适合管理公司项目。

具体场景分析

场景一

公司新开一个项目,我们都是把之前的项目复制出来一边当骨架,以至于里边的大部分配置都是相似的。或者我们工作了好多年,终于,我们封装了一些自己的库,很多项目都引用了这个库。

然后突然有一天,我们发现了这个库修复了一些bug,或者某些配置发现有些问题,有更好的方案。那此时,这么多项目怎么办?

  1. 把新代码一个个的复制到所有项目中,然后一个个的打包发布
  2. 把这些公共文件放到一个文件夹下边,使用git subtrees管理,别的项目更新代码后,打包发布
  3. 把这个库发布成一个npm包,其他项目都执行更新命令,然后打包发布
  4. 这个库放在项目的外层,项目相对路径引用这个包,不用更新代码,直接打包发布(Monorepo方案)
  5. 先把其中一个改好并发布上线,然后其他项目引用这个项目的公共模块,无需再次打包发布。(模块联邦方案)

首先,我们排除第一种方案,太原始,太费劲了。既然都写这个文章了,再用这样的方案,那我岂不是白写了。

使用npm方案,因为公共代码很可能涉及项目配置以及css等内容,发npm包不合适。但也不失为一种非常简单、有效的办法。并且npm官方也是支持这么做的。

使用git subtrees方案,完全可以解决当前的问题,对于项目改动最小,对组内其他人的影响也最小。

使用Monorepo,首先把公共代码当作基座的一部分提交到git仓库,同时新建apps文件夹,并被gitignore,里边放所有项目。然后组内的其他人,也跟着这么做一次。

在这样的场景下,git subtreesMonorepo的方式差不多,无非是公共组件放在项目内/外的问题。Monorepo稍微复杂一点,如果来一个新人,他需要先下载外边的Monorepo基座,然后才能下载业务项目。

模块联邦方案看起来是最好的,只需要公共库改了,别的项目不用任何改动就能获取最新的更改。但就是因为是无感更新,所以改动的时候才要更加慎重,公共依赖一定要明确好哪几个项目用到了,这次更改会对其他项目有什么影响。其次,因为这个技术是基于webpack5,对于一些比较老的项目,可能需要比较多的改动才能迁移。

场景二

我们现在要开发一个特别特别大的项目,所以想到以微前端的方式开发,一个基座项目,然后再独立开发一个个子项目,运行时以iframe/qiankun/micro-app等方式内嵌子项目。

开发时,我们需要先启动基座项目,跳转到对应的模块后,我们再启动对应模块的项目,然后在子项目上完成开发。此时不一定支持热更新,我们可能需要复制基座项目的token/cookie到子项目,然后单独开发子项目。

这样就有了两个问题:

  • 如何管理这么多应用的公共模块以及配置?
  • 有没有更优雅的启动方式?

第一个问题,我们可以参考场景1的解决方案。

如果使用git subtrees管理项目的话,因为项目还处于开发过程中,那么公共文件的更改次数可能还是比较频繁的,虽然使用git subtrees的方式更新项目比较无感,但项目多了,难免还是难受。

那么此时Monorepo的管理方式就凸显了,它不光不需要进入的一个个项目执行更新命令,甚至配合lerna/rush/TurboRepo后,还可以一键启动多个项目,一键完成所有项目打包。

既然是新项目了,那么公共模块完全可以用模块联邦进行管理,发布到单独服务也好,挂载在基座项目也好。以后改动时,就不用再对子项目一个个打包发版了。

可以看到,其实模块联邦和Monorepo并不是互斥关系,甚至还有些互补的意思。Monorepo偏向开发时,模块联邦偏向运行时。

ps: 对于这个场景,我觉得还有更简单的方案:一个项目打包成多页面形式。以后有时间了,写一个文章专门研究一下。

场景三

公司特别大,人员特别多,以至于很多人彼此之间交流不通畅,甚至都不认识,技术选型五花八门。此时,如何统一管理项目呢?

相信我,如果真的有这么一个人想把事情做统一,那他肯定不会看我的这篇文章。他们最重要的任务,不是统一管理项目,而是如何把现有的公共组件以及规范怎么告诉每一个人。

结语

其实,在一个公司中,用的几乎都是同一个技术栈,甚至框架版本、ui库、依赖插件都一致。模块联邦的适用场景比想象中的要多的多。

Monorepo如果不是开发微前端,也没必要非得上。毕竟他最大的好处是,多项目的统一管理。对于普通项目,相同点不会有特别多,更不会频繁的更改公共模块。

对于小公司,如果没有特别多的私有化配置的话,建议老老实实的上npm包,npm上的包太多了,只要把名字起的怪一点,几十年没人动你的包。或者说,好不容易写个牛逼的组件,还不赶紧发出去显摆显摆。

当然了,如果真觉得npm不安全,git subtree 管理还是非常简单的。

最后,照例吐槽一下前端发展真是太快了,没几年的东西,都一点点被淘汰了,哎。