社区流行的前端库陆续开始使用monorepo
的仓库管理模式。这并不是一个新概念,monorepo
(单体代码仓库),仓库将纳管多个项目代码。与之相对multirepo(多代码库),则意味着
不同项目分别使用不同仓库管理。
何时使用monorepo?
关于这个问题,应该说monorepo与multirepo有什么优缺点,两种模式并无绝对优劣, 针对场景选择才是王道!切忌盲从大流!
在微服务的大环境下,每一个项目使用单独仓库管理,项目公共依赖则抽离为npm包进行复用, multirepo看起来合情合理。但随着业务发展,仓库数量变得越来越多, 每一个仓库都需要单独维护,部署,成本越来越高。使用npm复用代码的方式变得不够灵活, 如果npm包含的是基础服务,几乎不用经常维护迭代,改动代码的成本不算高。 但如果要复用的代码涉及到业务内容,而业务是复杂多变的,我们需要经常维护npm模块, 每一次修改都需要经过本地调试的link和复杂的发布流程。
大多时候,只是修复一行bug,却需要本地link,然后先发布npm包,再发布依赖方业务组件,这些对开发者来说都是额外负担。
再者,随着仓库增加,相互依赖关系变得难以捉摸,对于一个npm包来说,使用方有哪些, 完全隐藏在各个仓库中,改动影响不好分析。
这种场景下,monorepo的优势可以体现出来,我们将具有相互依赖关系的项目仓库, 使用monorepo的方式进行统一管理,项目间的依赖清晰可见。项目位于同一仓库下, 依赖的复用也变得简单,monorepo构建工具可以自动完成项目间的依赖link以及依赖提升, 无需发布npm就可以完成代码的复用。本地调试和发布的痛苦得以解决。
这么看来monorepo确实解决了部分问题,随业务的增长,monorepo也存在变成巨石项目的可能, 并不是说所有的项目都得放在一个仓库中进行管理。
我的理解是,multirepo和monorepo是需要共存的。仓库拆分的粒度不应该太小。 从业务功能的角度看,同一业务功能下的项目是可能产生依赖关系的, 因此使用monorepo形式进行管理,相互的依赖关系简单清晰,代码复用方便。
而不同业务功能的项目则划分到不同的仓库分开管理。对于团队的基建类项目,也可以认为位于同一业务功能下, 使用monorepo的方式进行管理。
monorepo搭建选型
网上有很多关于搭建的教程,本文不讲具体搭建过程,只谈选型。
monorepo仅是一种仓库管理思想,monorepo的搭建还需借助工具,
目前市面上工具种类很多,例如:lerna
,yarn worksapce
,pnpm worksapce
,nx
,rushjs
。
monorepo最核心的需要是,完成依赖的提升和项目间的link,达到依赖复用及项目间引用的目的。
其余的各项附加功能都算是锦上添花的存在,可以按需选择。
关于依赖提升,由于node的模块寻找机制是从项目本身的node_modules出发,逐级向上查找。 当repo中的所有项目依赖被提升到根目录时,项目A中即便不在自身packake.json声明依赖, 也可以成功引用到其他项目中声明过的依赖,这就是幽灵依赖。
看过很多文章,对幽灵依赖带来的问题,大多持有贬低的态度。其实不然,幽灵依赖使得项目不必再声明完整的依赖, 为monorepo中的项目提供了非常方便的依赖复用功能,因此大可不必对其敬而远之, 我们可以视需求选择是否要声明完整的package.json。
借助工具同样可以避免幽灵依赖的问题,例如使用pnpm去构建monorepo。
此外工具间还能相互组合,例如使用yarn workspace搭建monorepo, 使用lerna管理项目包的版本及发布,总之,并没有一个固定的套路。
相信在明白了monorepo的所需的核心功能之后,你就能根据自己的需求完成选型。
monorepo仓库目录划分
建议使用以下结构来管理monorepo仓库:
- packages 子项目位置
- configs/平铺在根目录 公用配置(tsconfig,打包配置,部署配置,eslint配置等)
- docs 文档网站(按需添加)
monorepo构建
构建可以说时monorepo相当重要的一环,由于项目间存在依赖关系,因此项目在CI的过程中,需要注意构建的顺序, 项目间依赖简单的时候,你可以手动编写脚本指定项目构建的先后顺序,确保发布无误。当项目数量增多依赖复杂时, 最好借助工具分析依赖并进行自动化的构建,这可以节省很大一部分精力。
例如lerna官方文档的例子:分析依赖并按正确顺序构建。
总结
monorepo在解决代码复用问题的同时,也将引入一系列新的问题(纳管项目过多带来的性能压力,本地开发,构建测试发布,版本管理等等)。 任何工具都不是万能的,合理取舍。