git Monorepo代码管理模式
Author:zhoulujun Date:
什么是Monorepo
Monorepo(monolithic repository) 是管理项目代码的一个方式,指在一个项目仓库 (repo) 中管理多个模块/包 (package),不同于常见的每个模块建一个 repo。
monorepo 是一种将多个项目代码存储在一个仓库里的软件开发策略("mono [ˈmɑːnoʊ]" 来源于希腊语 μόνος 意味单个的,而 "repo [ˈriːpoʊ] ",显而易见地,是 repository 的缩写)。
无论是世界一流的互联网企业 Google,Facebook,还是社区知名的开源项目如 前端的:Babel、React、Vue,后端的:Laravel、Symfony,系统的:NixOS等 都采用这种模式。
Babel团队给出了他们的看法:https://github.com/babel/babel/blob/master/doc/design/monorepo.md
monorepo 管理代码只要搭建一套脚手架,就能管理(构建、测试、发布)多个 package。
在项目的第一级目录的内容以脚手架为主,主要内容都在 packages 目录中、分多个 package 进行管理。目录结构大致如下:
├── packages | ├── pkg1 | | ├── package.json | ├── pkg2 | | ├── package.json ├── package.json此时会有一个问题,虽然拆分子 npm 包管理项目简单了很多,但是当仓库内容有关联时,调试变得困难。所以
理想的开发环境应该是只关心业务代码,可以直接跨业务复用而不关心复用方式,调试时所有代码都在源码中。
Monorepo 简单的说,是指将公司的所有代码放到一个 Git / Mercurial / Subversion 的代码仓库中。
Monorepos 有时被称为单体代码库(monolithic repositories),但不应该与单体架构(monolithic architecture)相混淆,单体架构是一种用于编写自包含应用程序的软件开发实践。这方面的一个例子就是 Ruby on Rails,它可以处理 Web、API 和后端工作。
单一代码库(monorepos) vs 多代码库(multirepos)
多代码库不是微服务(microservices)的同义词,两者之间并没有耦合关系。事实上,我们稍后将讨论将单一代码库和微服务结合起来的例子。只要仔细设置用于部署的 CI/CD 流水线[2],单一代码库就可以托管任意数量的微服务。
类似管理方案
Monorepo:只有一个仓库,并且把项目拆分多个独立的代码工程进行管理,而代码工程之间可以通过相应的工具简单的进行代码共享。
Single-repo Monolith:同样也只有一个仓库,而它并不会独立的分割每个代码工程,而是让他们成为一体来进行开发管理,模块的拆分取决于代码工程的设计。
Multi-repo:则是通过建立多个仓库,每个仓库包含拆分好的代码工程,而仓库间的调用共享则是通过NPM或者其他代码引用的方式进行。
所有项目在一起?
Monorepo 的核心观点是所有的项目在一个代码仓库中。这并不是说代码没有组织都放在 ./src 文件夹里面。相反,通过使用 Buck / Bazel,monorepo 中的代码都是分割到一个个小的模块中的。比如一个普通的互联网公司,代码仓库的构架可能是这样的:
./frontend/web/vendors ./frontend/web/vendors/vue.js ./frontend/web/ui/registration ./frontend/web/ui/login ./frontend/ios/apps/app1 ./frontend/ios/vendors/yoga ./frontend/ios/features/authentication ./frontend/ios/libraries/network ./frontend/android/apps/app1 ./frontend/android/vendors/sqldelight ./shared/protobuf_schema ./backend/vendors/envoy_proxy ./backend/messaging/gateway ....
这样的分割,每个人大部分时间其实仍然只是工作在少数的几个文件夹以内的,并不会导致一个 IDE 打不开太大的项目之类的事情。但是很多事情就简单了很多。
比如编译。在这样的一个代码仓库中,所有的依赖都已经在仓库里了,用 bazel run 就可以执行任意的一个项目,不需要多余的配置。大大加快了开发的速度。
比如要修改前端和后端的通信协议,只需要一个 commit 就可以把 protobuf 改掉,同时把使用 protobuf 的后端和三个前端的代码一起改了。
monorepo 方案的优势
代码重用将变得非常容易:由于所有的项目代码都集中于一个代码仓库,我们将很容易抽离出各个项目共用的业务组件或工具,并通过 TypeScript,Lerna 或其他工具进行代码内引用;
依赖管理将变得非常简单:同理,由于项目之间的引用路径内化在同一个仓库之中,我们很容易追踪当某个项目的代码修改后,会影响到其他哪些项目。通过使用一些工具,我们将很容易地做到版本依赖管理和版本号自动升级;
统一管理:由于只有一个仓库,所有的配置都可以统一进行管理,而无需为不同项目重复构建环境,包括通用的代码规范检测,相同的测试框架,以及统一的CI/CD构建流程等。
简单依赖:多个代码工程的相同依赖可以提升至根目录进行管理,大大减少重复安装所带来的空间浪费。同时,代码工程之间也可以在保持隔离的同时相互引用,而无需在构建时依次构建相关依赖包并重新发布。
代码重构将变得非常便捷:想想究竟是什么在阻止您进行代码重构,很多时候,原因来自于「不确定性」,您不确定对某个项目的修改是否对于其他项目而言是「致命的」,出于对未知的恐惧,您会倾向于不重构代码,这将导致整个项目代码的腐烂度会以惊人的速度增长。而在 monorepo 策略的指导下,您能够明确知道您的代码的影响范围,并且能够对被影响的项目可以进行统一的测试,这会鼓励您不断优化代码;
它倡导了一种开放,透明,共享的组织文化,这有利于开发者成长,代码质量的提升:在 monorepo 策略下,每个开发者都被鼓励去查看,修改他人的代码(只要有必要),同时,也会激起开发者维护代码,和编写单元测试的责任心(毕竟朋友来访之前,我们从不介意自己的房子究竟有多乱),这将会形成一种良性的技术氛围,从而保障整个组织的代码质量。
monorepo 方案的劣势
项目粒度的权限管理变得非常复杂:无论是 Git 还是其他 VCS 系统,在支持 monorepo 策略中项目粒度的权限管理上都没有令人满意的方案,这意味着 A 部门的 a 项目若是不想被 B 部门的开发者看到就很难了。(好在我们可以将 monorepo 策略实践在「项目级」这个层次上,这才是我们这篇文章的主题,我们后面会再次明确它);
由于单仓的管理模式,使用Monorepo将无法简单的控制各个模块代码的访问限制,任何有权限访问该仓库的人员将有权限访问所有的代码工程,这可能会导致部分安全问题。
新员工的学习成本变高:不同于一个项目一个代码仓库这种模式下,组织新人只要熟悉特定代码仓库下的代码逻辑,在 monorepo 策略下,新人可能不得不花更多精力来理清各个代码仓库之间的相互逻辑,当然这个成本可以通过新人文档的方式来解决,但维护文档的新鲜又需要消耗额外的人力;
对于公司级别的 monorepo 策略而言,需要专门的 VFS 系统,自动重构工具的支持:设想一下 Google 这样的企业是如何将十亿行的代码存储在一个仓库之中的?开发人员每次拉取代码需要等待多久?各个项目代码之间又如何实现权限管理,敏捷发布?任何简单的策略乘以足够的规模量级都会产生一个奇迹(不管是好是坏),对于中小企业而言,如果没有像 Google,Facebook 这样雄厚的人力资源,把所有项目代码放在同一个仓库里这个美好的愿望就只能是个空中楼阁。
性能问题:当仓库的代码规模非常的巨大,达到GB/TB的级别,会增大开发环境的代码下载成本,以及本地硬盘的压力,执行git status也可能需要花费数秒甚至数分钟的时间。并且,当代码工程很多且活跃数量也很多的情况,会加大分支管理策略和各个代码工程版本管理的压力。
Code reviews:通知可能会变得非常嘈杂。例如,GitHub 有有限的通知设置,不适合大量的 pull request 和 code review。
为何用Monorepo
Monolith时期
Monolith这种模式是最早开发人员所使用的仓库架构模式,当时的前端功能还很简单,还没有过多的框架出现,仅仅是HTML,CSS的编写以及加上简单的JS逻辑。在那个时期,项目的前后端还杂糅在一起,类似JSP的开发模式被广泛应用,因此前后端的代码会集中在一个仓库里,最终页面会由后端进行渲染,仓库代码也会整体进行构建和部署。
而往后发展,Web能力开始变得越来越强大,前端能做到的事情逐渐变多,对应的工程也越来越复杂,AJAX的应用催生了前后端分离的概念。顺应趋势,前端开始将整体模块单独抽离形成仓库进行管理,到这里我们开始看到了Multi-repo的影子,但前端的代码依旧会统一在一个仓库里进行开发和管理。
随着前端功能还在不断的增多,更多的依赖被引入到仓库中,使得仓库越来越臃肿,特别在当时NPM还没被应用时,前端往往会去各个开源库官网或GitHub上下载文件并放入自己的项目当中。因此,在Single-repo Monolith架构下,对开发人员代码架构组织能力要求越来越高,同时扩展性和维护性也变得越来越低。
Airbnb 的基础设施工程师延斯·范德海格(Jens Vanderhaeghe)也讲述了微服务和单一代码库是如何帮助他们向全球扩张的[16]。
Airbnb 最初的版本被称为“monorail”,是一个独立的 Ruby on Rails 应用程序。当公司开始指数级增长时,代码库也随之增长。当时,Airbnb 实施了一项名为“民主发布(democratic releases)”的新发布政策,意味着任何开发者都可以在任何时候发布产品。
随着 Airbnb 的扩张,民主程序的限制也受到了考验,合并更改变得越来越困难。Jens 的团队实施了一些缓解措施,比如合并队列和增强监控。这在一段时间内有帮助,但从长远来看还是不够。
Airbnb 的工程师们为维持 monorail 系统进行了英勇的斗争,但最终,经过数周的辩论,他们决定将该应用分割为多个微服务。因此,他们创建了两个单一代码库:一个用于前端,一个用于后端。两者都包含数百个服务、文档、用于部署的 Terraform 和 Kubernetes 资源以及所有维护工具。
Multi-repo时期
Multi-repo的流行很大程度上是为了解决这种模块高度耦合,代码臃肿的情况,开发者们开始更加倾向将整个业务项目进行拆分,独立进行管理。
伴随着当时Web环境质的飞跃,SPA框架的流行,对应Bundle工具的产生,npm的流行以及ES,Commonjs模块化的代码引入方式天然的为分割代码模块提供了良好的时机。这使得更多开发人员将工具代码单独成库并发布成包,并将庞大的业务进行拆分,每个业务模块建立单独的库由各自团队负责开发以及维护,各种包都通过npm来进行共享。
然而随着模块拆分不断的增多,开发者们又发现过多的仓库加大了维护的成本,新的项目环境搭建,和涉及整体业务的重构和依赖同步都将变得繁琐,此时回归单repo的概念又开始兴起。因此,已经被提出很久的Monorepo开始浮出水面,应运而生的工具也开始占据了一席之地。
Monorepo时期
Monorepo的出现开始解决环境及依赖统一的问题,代码之间的共享也不再强依赖于NPM来进行。既保留了Monolith单仓环境维护的便利性,同时满足Multi-repo对于项目解耦的独立开发管理。
而后类似lerna+yarn的包管理方案的出现让Monorepo拥有了较为完整的解决方案,并伴随着新兴的技术Pnpm,Changesets,Turborepo的不断推出,Monorepo的整个管理流程变得越来越完善和简单,也逐渐被很多开发者所采用。
以上是个人对于这三种策略发展趋势的理解,三种管理模式并不是依照时间递进取代的关系,更多的是随着技术发展的趋势和实际项目情况做出更合适的选择。并且随着未来技术的迭代更新,譬如Http import/Module Federation等技术的成熟,也许会再次改变这三者的使用选择。
monorepo 解决方案
Monorepo只是一个概念,它并不代表某项具体的技术,开发人员需要使用相应的技术手段或者工具来达到或者完善它的整个流程,从而达到更好的开发和管理体验。
包管理方案
而实现Monorepo最重要的一个环节就是如何管理包依赖。这里简单的介绍目前两种较成熟的支持Monorepo的包管理工具。
常见的 monorepo 解决方案是 lerna 和 yarn 的 workspaces 特性。用 yarn 处理依赖问题,lerna处理发布问题。
Lerna
Lerna是npm模块的管理工具,为项目提供了集中管理package的目录模式,如统一的 repo 依赖安装、package scripts和发版等特性。
Lerna的工作流可以非常完善,它包含了包管理的流程以及各项参数配置,这里仅使用它的版本管理功能来描述它的一个发布过程:
使用lerna version命令,lerna会动找出上一个版本发布依赖有过变更的 packages,并提示开发者确定发布的版本号。
开发者依照提示确认后,lerna会自动将有更新的对应的package.json中的version字段。
如果配置了--conventional-commit参数,lerna会依据先前规范的 commit message生成当前版本的CHANGELOG。
提交修改,并打上版本的tag,推送到git上。
使用lerna publish命令,依据提示选择要发布的包完成发布。
更加简单的工作流程如下图:
Lerna CLI
Lerna 提供了很多 CLI 命令以满足我们的各种需求,但根据 2/8 法则,您应该首先关注以下这些命令:
lerna bootstrap:等同于 lerna link + yarn install,用于创建符合链接并安装依赖包;
lerna run:会像执行一个 for 循环一样,在所有子项目中执行 npm script 脚本,并且,它会非常智能的识别依赖关系,并从根依赖开始执行命令;
lerna exec:像 lerna run 一样,会按照依赖顺序执行命令,不同的是,它可以执行任何命令,例如 shell 脚本;
lerna publish:发布代码有变动的 package,因此首先您需要在使用 Lerna 前使用 git commit 命令提交代码,好让 Lerna 有一个 baseline;
lerna add:将本地或远程的包作为依赖添加至当前的 monorepo 仓库中,该命令让 Lerna 可以识别并追踪包之间的依赖关系,因此非常重要;
Changesets
Changesets是一个用于Monorepo项目下版本以及Changelog文件管理的工具,它也是Pnpm官方所推荐使用的版本管理工具,它所做的工作相较于Lerna而言更加专一。
Changesets的工作流会将开发者分为两类人
项目的开发者:开发者在Monorepo项目下进行开发,开发完成后,给对应的子项目添加一个changeset文件。
项目的维护者:项目的维护者后面会通过changeset来消耗掉这些文件并自动修改掉对应包的版本以及生成CHANGELOG文件,最后将对应的包发布出去。
更直观的工作流程如下时序图:
npm未来将支持 monorepo 特性
npm@7 或许会带来一流的 monorepo 支持,期待中!
包构建方案
在某些场景下,Monorepo的规模较大,包之间拥有拓扑式的依赖结构,而此时进行项目构建往往会需要依照依赖的链式逐步进行,过程将会耗费大量时间。而为了解决这样的构建痛点,也有相应的技术浮出水面,比如Turborepo。
Turborepo
Turborepo是一个用于JavaScript/TypeScript monorepos的快速构建系统。目的是为了解决大型monorepo项目构建速度缓慢的一大痛点。
turbo的核心是永远不会重新构建已经构建过的内容。
turbo会把每次构建的产物与日志缓存起来,下次构建时只有文件发生变动的部分才会重新构建,没有变动的直接命中缓存并重现日志。turbo拥有更智能的任务调度程序,充分利用空闲CPU,使得整体构建速度更快。另外,turbo还具有远程缓存功能,可以与团队和CI/CD共享构建缓存。
这里也给出一个简单的示例来辅助理解Turborepo在构建中的优势,假设有如下的包依赖结构,其中我们要对app E进行构建,它依赖4个lib包的构建,而lib包之间也有相互的依赖,特别lib B还同时依赖lib A和lib C:
(lib A -> lib B) -> lib D -> app E
(lib C -> lib B)
正常的构建过程将会依照顺序依次执行,而Turborepo会在这个基础上通过它的缓存机制来确定哪些包需要进行构建,同时通过它的任务调度机制来进行并行构建,从而加快整个构建流程。
辅助技术
代码规范工具
Monorepo的多包管理既方便了做项目管理,同时对团队规范的统一有了一定的要求,那么适当的工具便成了必要,这里主要介绍两个非常常用的代码规范校验工具,Eslint和Prettier。
Eslint
Eslint是目前最受欢迎的Javascript代码质量校验工具,它可以通过静态分析代码来发现语言规范问题,多数问题可以被自动修复,Eslint修复程序具有语法意识,因此不用担心修复后而引入错误。同时,它内置自定义解析器,使用者可以编写自己的规则来使得Eslint能够更加适合所开发的项目。
Prettier
Prettier可以理解为一个代码格式化工具,它提供一套完整的代码风格方案,它通过解析代码并使用自己的规则强制重新打印代码,从而使得代码能够保证一致性。使用它可能不会让项目的代码完全符合开发者想要的格式规范,但却在一定程度上是最通用和便捷的团队规范方案。并且,通过plugin的形式prettier可以被集成到Eslint当中,使得两者的结合使用会更加便利。
同时,由于Monorepo的规模性,每次修改代码都进行全量的规范校验带来时间上的消耗,此时可以利用husky和lint-staged两个工具在进行git提交时进行增量代码的校验。
提交规范工具
Monorepo下大概率会有多个团队进行开发,而在git仓库模式下,每次提交的Commit信息会成为重要的功能管理依据,同时在一些场景中,会依据它自动生成CHANGELOG文件,这里也介绍两种常用的工具commitlint和commitizen。
Commitlint
Commitlint是一个提交规范校验工具,它将帮助团队遵守一定的提交信息格式约定,默认采用Convenional提交规范,它也提供一定的配置允许使用者更改校验规则。同时,通过Husky添加相应的git hook,来达到提交自动校验的提示的功能。
Commitizen
Commitizen是一个提交日志工具,辅助开发者使用提交规则,再使用它进行git提交操作时,将自动提示填写Commit Messsage所必须的字段,并获取有关提交信息格式的及时反馈,使用者只需按提示输入相关内容信息即可。
通常,上面两者技术也可以配合进行使用,既提供相应脚手架工具来辅助提交信息填写,同时保证提交时规范的校验。
文档服务工具
vitepress
具体参看:《vitepress搭建markdown文档博客》
参考文章:
https://www.toptal.com/front-end/guide-to-monorepos
Monorepo 是什么,为什么大家都在用? https://zhuanlan.zhihu.com/p/77577415
All in one:项目级 monorepo 策略最佳实践 https://segmentfault.com/a/1190000039157365
Monorepo-多包单仓库的开发模式 https://juejin.cn/post/6844904206076248072
前端工程化之多个项目如何同时高效管理 — monorepo https://qdmana.com/2021/07/20210716095331608p.html
使用mono-repo实现跨项目组件共享 dennisgo.cn/Articles/Engineering/mono-repo.html#使用mono-repo实现跨项目组件共享
MonoRepo最佳实践 https://mrrabbitan.github.io/2021/04/12/MonoRepo/
Monorepo——探秘源码管理新姿势! https://jishuin.proginn.com/p/763bfbd73b74
monorepo--依赖 https://cloud.tencent.com/developer/article/1634495
5 分钟搞懂 Monorepo https://xie.infoq.cn/article/4f870ba6a7c8e0fd825295c92
Vue3.0 中的 monorepo 管理模式__Vue.js 是chttps://www.vue-js.com/topic/5feabb554590fe0031e595a3
转载本站文章《git Monorepo代码管理模式》,
请注明出处:https://www.zhoulujun.cn/html/tools/VCS/git/8783.html