现代前端工程化——探秘 Monorepo 仓库管理方式

本文探讨了前端项目管理中的Monorepo和Multirepo策略,重点介绍了Monorepo的优点,如简化组织、减少重复依赖、跨项目开发和方便的仓库管理。通过Lerna工具的实践,展示了如何利用Lerna进行多包管理,优化依赖并提升开发效率。同时,文章也提及了Monorepo的潜在缺点和适用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

monorepo 和 multirepo

一般大到分项目,小到写代码,都遵循着拆分的原则,尽量不在一个项目、方法里糅合太多的东西。对于这种管理仓库的风格,称之为 multirepo。这样做的好处是,按不同项目可以分为多个仓库,仓库之间保证独立,独立开发独立部署,保证了每个项目之间不受其他项目影响。

另一种风格就是 monorepo了,也就是把多个项目放到一个仓库中,虽然把所有东西放到一起听起来很糟糕,但不仅仅是这样。对于 monorepo 而言,你只是把多个项目按你所设的某种场景把它组织到了一起,它的粒度还是保持原有的划分,对于某个团队的某个开发人员而言,他的关注点还是在其中一个项目。可能听上去这种方式相对于 multirepo 而言有些多此一举,我已经把仓库拆分了,为何还要再组织回去呢?对于一些前端项目而言,它的确有组织回去的好处,接下来就具体分析一下。

monorepo 并不是一种工具,它只是对于项目的一种管理手段,一种思维方式。

monorepo 的优点

简化组织

很大一部分情况下,我们在一开始开发项目的时候并不能保证项目的规模只有这么大,随着不断的迭代业务会越来越多,逻辑越来越复杂,于是就要对项目进行拆分。又或者是几个不同的但都处于某一块业务的项目。用这时候使用 monorepo 的方式来管理这些项目,能够简化项目结构组织。用 Dan Luu(参考文档三)的观点来说,很多拆分都不是正确的拆分。可能只是因为代码量多,或其他非必要原因对仓库进行了拆分。那就需要再重新将他们组织回来。

减少重复依赖项

对于前端项目,npm installnpm run dev 这些步骤已经刻在我们的基因里了。但是 install 安装了庞大的 node_modules 后,其实大部分项目所安装的库都有重复。那我们应该把这些依赖项提取出来,而都引用了这些依赖的子项目只需要通过软链接这样的方式引入依赖项即可。

跨项目开发

如果我们同时在给几个 npm 模块进行功能迭代开发,在多个仓库下调试起来可能不是那么方便,还需要手动维护 npm link,通过 monorepo 可以直接在本地进行夸项目间的联调,提高开发的效率。

更方便的仓库管理

对于一些大型、开源的项目来说,多个仓库意味着你要在多个地方进行 issue 之类的处理,你会更倾向于统一这些类似的 issue,管理很多 PR 和 git 钩子等等。

实践

Lerna

前面有讲过,既然 monorepo 不是工具,那我们就需要一个工具来实现它,在参考了一些文章后,我决定使用 Lerna(参考文档二)。

A tool for managing JavaScript projects with multiple packages.

这是 Lerna 官网对于它作用的描述,用来管理多个 JS 项目。不仅可以用 Lerna 来实现对多个项目仓库的管理,也可以用来管理多个 npm 包的维护发布。vue-cli(参考文档五),Babel(参考文档六)等知名项目也使用了 Lerna,大佬们都用了,那我们跟着梭哈就完事儿了。

然后我们只要通过命令 npm install --global lerna 将 Lerna 安装到全局。然后就可以开始了。

通过命令 lerna init 来初始化 Lerna 的目录,初始化完可以看到目录特别简单:

- packages
- package.json
- lerna.json

我们只要把项目放到 packages 文件夹下面即可。

在 lerna.json 中就是对 Lerna 的一些配置了,package.json 是用来管理主仓库的依赖之类的。

lerna.json

{
  "npmClient": "npm",
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0"
}

npmClient 指定了安装使用的方式,可以用 npm 也可以用 yarn。packages 里面定义了子项目的路径,可以根据你想要的逻辑自由定义路径。

那么接下来就要实现一下刚才提到的关于依赖项的优化,Lerna 提供了一个命令:lerna bootstrap 来安装所有 package 的依赖项。运行这个命令相当于给每个项目都运行了 npm install。可我们想要的是把所有 package 的共同依赖提取到一个公共的地方,所以 Lerna 也提供了参数 --hoist 来提取共同的依赖项到主项目的 node_modules 中。

运行命令:lerna bootstrap --hoist

这时候主项目中会有一个 node_modules 里面放了一些公共的依赖,而用到了该依赖的项目,会在 node_modules/.bin 这个目录下生成一些符号链接来连接到公共的 node_modules(软链接)。这样就无需修改每一个项目的 package.json 了。

说到这里,有一个问题。如果项目中依赖的包版本不同怎么办?比如 A 项目使用了 Vue3,B 还在使用 Vue2.6。

Lerna 对于版本问题的解决方式是在公共的 node_modules 中安装版本最高的,再在低版本项目的node_modules 中独立为期安装依赖,并且在命令行中抛出警告。比如我在两个子项目中一个安装了 Vue2.6,另一个安装了 Vue3.0。

同时这样我们也可以清楚的发现不同项目中依赖的包版本有差异的问题,有时候我们最先做的 A 项目安装某一个包时,安装了 2.6.1 版本,之后在 B 项目中安装该依赖时已经有了 2.8.1 版本。这两个版本或许可以直接升级,并不会造成影响。但是手动去维护不同项目的依赖版本效率又特别低,通过 Lerna 可以很清楚的发现这样的版本差异问题。

如果需要开发多个 npm 包也能够使用 Lerna,就像 vue-cli 那样。简单举个例子,我们先创建两个项目:

lerna create module-a
lerna create module-b

根据命令行提示配置好一些 npm 的基本信息后,就能在目录下看到项目了:

- packages
  - module-a
    - __tests__ 
    - lib
      - module-a.js
    - package.json
    - README.md
  - module-b
    - __tests__ 
    - lib
      - module-b.js
    - package.json
    - README.md

在 module-a.js 中引入模块 b,并把模块 b 添加到模块 a 的依赖项中:

module-a.js

const moduleB = request('module-b');

package.json

"dependencies": {
  "module-b": "0.0.1"
},

然后运行命令 lerna link 后就能看到模块a中的 node_modules 引入了模块b的软链接。

开发完成后,就要把 npm 包发布到对应的仓库了,可以通过命令 lerna publish 发布。

到这里维护各个 npm 模块的流程就结束了,但是按照 multirepo 的话,我们可能要做几件事:

  1. 修改 module-b 的版本号

  2. 因为 module-a 依赖了 module-b,先修改 module-a 的依赖,再修改版本号

  3. 逐个发布包

这次只是个例子,如果实际开发中有几十个 npm 互相依赖就会造成不小的困扰了。那么对于其他的命令和详细的用法可以参考 Lerna 的文档(参考文档七)。

submodule

还有一种思路,在 Lerna 项目中,我们可以通过 git submodule 来引入不同的子项目仓库(Lerna 项目本身也是一个仓库,称之为主仓库)。

可以在 Lerna 项目根目录运行命令 git submodule add git@github.com:CUndH/mono-subA.git packages/ ,把一个 git 仓库放到 packages 下面。添加后,就可以在根目录下看到一个名为 .gitmodules 的文件,大致如下:

[submodule "packages/mono-subB"]
	path = packages/mono-subB
	url = git@github.com:CUndH/mono-subB.git
[submodule "packages/mono-subA"]
	path = packages/mono-subA
	url = git@github.com:CUndH/mono-subA.git

很清晰,不用多解释,就是一些子仓库的配置信息。

这时候,如果我是负责 mono-subA 项目的开发人员,我不需要去了解主仓库相关的信息。我的关注点还是在 mono-subA 项目上,而主项目不参与具体业务开发,只用于管理项目的版本、发布,便于导航、查阅代码提交日志等,可以交由负责人或者 CI 去进行一些拉取合并代码、发布生产环境等操作。

可以通过 git diff 命令清楚的看到不同子仓库的修改记录:

-dirty 后缀代表这一次修改还没被 commit。

在主项目中添加子项目的时候,git submodule 所添加的子仓库会对应到具体的某一次 commit 上,具体对应的 commit 是当前主项目中对于子项目最近的一次修改。所以在仓库里会这样显示:

所以如果子仓库开发完毕提交了代码后,主仓库所指向的子仓库还是前一次 commit 的仓库,我查了一些文档说可以通过 git submodule update --remote 来获取子仓库的最新代码,这里有个小问题。如果直接运行该命令可能会报错,因为 git 会默认拉取子仓库的 origin/master 分支代码,而现在在 github 创建新仓库默认只有 main 分支了。如果你想要指定子仓库的拉取分支,你可以在 .gitmodules 文件中配置 branch:

[submodule "packages/mono-subA"]
	path = packages/mono-subA
	url = git@github.com:CUndH/mono-subA.git
	branch = main  // 加上这行

对于子仓库的修改,当你 commit 和 push 之后,在主仓库也会有一个待提交的 commit,需要再在主仓库中再提交一遍更新才行,这里有点多此一举的味道,但是我认为这可以更好的控制多个子项目的版本的一致性。假设我们每周会更新一次主仓库来进行发布生产环境这样的操作,那么这一步就能确保主仓库可以准确的控制某一个子仓库的代码更新,而不会因为不可控引起一颗老鼠屎坏一锅粥的情形。

monorepo 的缺点

无论是对于代码层面的设计,亦或是仓库管理层面的设计思想,都像踩跷跷板一样,没有最好,只有最适合。所以 monorepo 也有一些不足的地方:

  • 无法管理某个、某些项目对于指定人员的权限

  • 不同分支下的版本控制会显得较为混乱

  • 对于发布构建的挑战,难度会比单个项目构建要大

  • 不适用于业务相对零散、项目之间关系不大的场景

结尾

monorepo 和 multirepo 并没有哪个更胜一筹,只是不同的管理方式而已,本文也并不是说 monorepo 更好,我查阅的一些文章中也看到了不少反对意见,只要能从中学习到自己本不了解的东西就好,况且在现实中我们对于类似的技术的学习和使用本就不应该做选择题。

参考

  • 文档一:Git 命令 - submodule:https://git-scm.com/docs/git-submodule

  • 文档二:Lerna 官网:https://lerna.js.org/

  • 文档三:dan luu 的博客:https://danluu.com/monorepo/

  • 文档四:Monorepo 可能没你想象的那么香:https://baijiahao.baidu.com/s?id=1663844045880515134&wfr=spider&for=pc

  • 文档五:vue-cli:https://github.com/vuejs/vue-cli

  • 文档六:Babel:https://github.com/babel/babel

  • 文档七:lerna 文档:https://github.com/lerna/lerna

全文完


以下文章您可能也会感兴趣:

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值