monorepo相关概念

引言

代码(code)” 是程序员用开发工具所支持的语言写出来的源文件,用于实现或支持所有依托于计算机的程序及应用,因此,如何管理代码是开发人员在项目进程中非常重要的一环。

而“仓库(repository)”则是存储项目中所有代码文件和更改信息的重要载体和方式,它会帮助开发人员以“版本控制”的形式管理整个项目的生命周期。这里的仓库通常指代Git仓库,当然对其他比如Mercurial/Subversion的代码仓库也同样适用。而如何去设计仓库的代码管理策略将会直接影响项目的开发流程和使用体验。

在如今快节奏的前端开发中,管理多个项目和组件的复杂性成为了开发团队的挑战,Monorepo(单一代码仓库)作为一种新兴的解决方案,正在成为开发团队的新宠。

一些优秀开源项目都是使用Monorepo的方式管理项目的,比如Vue3ViteElementPlus,同时这几个项目都是采用 pnpm 作为包管理工具。

Monorepo简介

一、Monorepo是什么?

        Monorepo 只是一个管理概念,实际上它并不代表某项具体的技术,更不是所谓的框架。开发人员需要根据不同场景、不同的研发习惯,使用相应的技术手段或者工具,来达到或者完善它的整个流程,从而达到更好的开发和管理体验。

  Monorepo 是一种软件开发的策略模式,它代表"单一代码仓库"(Monolithic Repository)。与之相对的是另一种流行的代码管理方式 MultiRepo,即每个项目对应一个单独的仓库来分散管理。

  Monorepo 模式中,所有相关的一些项目和组件都被存储在一个统一的代码仓库中,而不是分散在多个独立的代码仓库中。

        如果多个项目之间没有明确定义的关系,我们不会将其称为单一存储库。

        同样,如果一个存储库包含一个庞大的应用程序,而没有对离散部分进行划分和封装,那么它只是一个大存储库。您可以给它起一个奇特的名称,例如“garganturepo”,但我们很遗憾地说,它不是 monorepo

        简单理解:Monorepo 是指把多个项目放到一个仓库,且这些项目之间具有良好的关系,用统一的本地关联、构建、发布流程,来消费业务域下所有管理的组件模块。

   Monorepo不等于Monolith(巨石)。

        现在主流的 Monorepo 所承担的责任并不只是存储的问题,还可以承担比如依赖管理,增量构建等一系列工程化的功能,已经成为工程化技术中非常有价值的一块领域,所以有时你为了实现某个特殊的功能不得不借助社区的力量,或者站在大佬的肩膀上。

二、Monorepo谁在用?什么场景适合用?

Monorepo 这个词您可能不是首次听说,在当下大型前端项目中基于 Monorepo 的解决方案已经深入人心。

  • 大型互联网公司:Google、Facebook、Uber、MicroSoft、阿里、字节等。

  • 前端常用的开源库:Vue、React、Vite、Babel、Element-plus、Vue-next。

那些场景适用?
  • 代码共享:当多个项目或模块之间需要共享代码、组件或工具库时。
  • 统一版本管理:需要统一管理各个项目的版本依赖,确保一致性。
  • 简化依赖管理:减少依赖安装和版本冲突,提高构建和部署效率。
  • 协作与团队工作:团队成员可以更轻松地共享代码、协作开发和进行代码审查。
  • 简化构建和部署:需要更方便地进行整体构建和部署,尤其对于有相互依赖关系的子项目。
  • 敏捷开发和迭代:需要加快开发和迭代周期,避免在多个代码仓库之间切换和同步。

三、Monorepo为什么会出现?

Monorepo的出现是为了解决传统的分离式代码仓库所面临的一些挑战和痛点,我们先来看看代码管理的发展历程:

1、单体应用架构——Monolithic时期

        项目初期起步阶段,团队规模很小,此时适合「单体式应用」,一个代码仓库承接一个应用,管理成本低,最简力度支撑业务快速落地。

        传统的「单体式应用」程序通常将所有的功能和模块打包在一起,形成一个单一的代码库和部署单元。这种单一的代码库包含了应用程序的所有部分,从前端界面到后端逻辑,甚至包括数据库模式和配置文件等。

├── client/

│   ├── src/

│   │   ├── components/

│   │   ├── pages/

│   │   ├── services/

│   │   ├── utils/

│   │   ├── App.js

│   │   └── index.js

├── server/

│   ├── controllers/

│   ├── models/

│   ├── routes/

│   ├── config/

│   └── app.js

├── tests/

│   ├── client/

│   └── server/

├── package.json

├── README.md

└── ...

存在问题:

  1. 难以实现部分更新和独立扩展的灵活性

  2. 高度耦合,代码臃肿

        前后端分离后,前端的「单体式应用」程序通常将前端所有的功能和模块打包在一起,形成一个单一的代码库和部署单元。

project

├── node_modules/

│   ├── lib@1.0.0

├── src/

│   ├── compA

│   ├── compB

│   └── compC

└── package.json

前端的「单体式应用」目录架构大概长这样。

优点:

  1. 代码管理成本低。

  2. 代码能见度高(无需额外的学习成本)。

  3. 发布简单,链路轻便。

缺点:

  1. 代码量大了后,调试、构建效率显著下降。

  2. 无法跨项目复用。

2、多仓多模块管理:Multirepo时期

        团队规模变大,人员分工明确,单体应用的缺点会愈发突出,此时 「Multirepo」就更适合。模块分工更明确,可拓展性、可复用性更强,调试构建发布能力也有一定提升。        

        多库多模块:将不同的功能模块、组件或服务等分别存放在独立的仓库中,可以单独进行版本控制、构建、部署和发布,使得不同的团队或开发者可以独立地开发、测试和维护各自的模块,更容易实现并行开发和团队协作。

// Repository - module1

├── node_modules/

├── src/

│   ├── components/

│   ├── pages/

│   ├── utils/

│   ├── App.js

│   └── index.js

├── styles/

│   ├── main.css

├── public/

│   ├── index.html

├── package.json

└── ...

// Repository - module2

├── node_modules/

├── src/

│   ├── components/

│   ├── pages/

│   ├── utils/

│   ├── App.js

│   └── index.js

├── styles/

│   ├── main.css

├── public/

│   ├── index.html

├── package.json

└── ...

// Repository - lib

├── node_modules/

├── package.json

├── src

│   ├── ...

├── README.md

// 共享代码

- lib 进行发包,比如包名为 @my-scope/lib

- 进入project1 或 project2 进行npm install

- 在代码中引入

 import {method} from '@my-scope/lib';

存在问题:

  1. 跨仓库开发:多仓维护成本高,

  2. 开发调试:npm包(修改->发布->安装成本高),调试麻烦(npm link),

  3. 版本管理:依赖版本同步升级管理麻烦

  4. 项目基建:脚手架升级,新老项目规范很难保证统一

        前后端分离后,前端的「Multirepo」目录架构大概长这样。

project

├── node_modules/

│   ├── lib@1.0.0

│   ├── lib@2.0.0

│   ├── pkgA

│   ├── pkgB

│   └── ..

├── src/

└── package.json

packageA

├── node_modules/

│   └── lib@1.0.0

├── src/

└── package.json

packageB

├── node_modules/

│   └── lib@2.0.0

├── src/

└── package.json

优点:

  1. 便于代码复用。

  2. 模块组件独立开发调试,业务理解清晰度高。

  3. 人员编排分工更加明确。

  4. 提高研发人员的公共抽取思维能力。

  5. 源代码访问权限设置灵活。

缺点:

  1. 模块划分力度不容易把握。

  2. 共同引用的版本问题,容易导致重复安装相同依赖的多个版本。

  3. 构建配置不复用,不好管理。

  4. 串行构建,修改模块体量大时,发布成本急剧上升。

  5. Code Review、Merge Request 从各自模块仓库执行,比较分散。

3、单仓多模块管理:Monorepo时期

        随着组件/模块越来越多, multirepo 维护成本越来越大,于是我们意识到我们的方案是时候改进了。

        单仓多模块管理可以解决上述问题,代码之间的共享也不再强依赖于NPM来进行,既保留了Single-repo 单仓环境维护的便利性,同时满足Multi-repo 多仓对于项目解耦的独立开发管理。

├── packages/

│   ├── module1/

│   │   ├── src/

│   │   ├── tests/

│   │   ├── package.json

│   │   └── ...

│   ├── module2/

│   │   ├── src/

│   │   ├── tests/

│   │   ├── package.json

│   │   └── ...

│   └── shared/

│       ├── src/

│       ├── tests/

│       ├── package.json

│       └── ...

├── libs/

│   ├── lib1/

│   │   ├── src/

│   │   ├── tests/

│   │   ├── package.json

│   │   └── ...

│   ├── lib2/

│   │   ├── src/

│   │   ├── tests/

│   │   ├── package.json

│   │   └── ...

│   └── ...

├── docs/

├── .gitignore

├── package.json

└── ...

1. 怎样进行项目间代码共享?发包至npm再引用安装吗?

答案:肯定不是的,举个例子:

  1. 假设 lib 的包名为 @my-scope/lib,无需发包至NPM。

  2. 在一级目录的 package.json 添加包名 @my-scope/lib: "workspace:*"。

  3. 在两个 projects 中的代码中引入:

import {method} from '@my-scope/lib';

        文件夹指定为工作空间后,多个项目都可以引入这个文件夹,文件夹里面的内容可以被其使用。

2. 各个项目怎么装包、运行?切换目录挨个装、挨个运行吗?(那岂不太麻烦了)

答案:在根目录下就可以给各个项目安装。

——>pnpm install.

3. 各个项目代码开发完分别提交一次commit,还是一起提交呢?

答案:可以一起提交。

4. lib包还能单独发布npm 包吗?

答案:可以。

5. 编译构建的顺序要我们自己定吗?

答案:不同的Monorepo库不同的处理方式,有些需要自己指定构建顺序,有些会内置判断项目依赖关系,无需自定义构建顺序。

        前后端分离后,前端的「Monorepo」目录架构大概长这样。

project

├── node_modules/

│   ├── lib@2.0.0

│   ├── pkgA

│   ├── pkgB

│   └── ..

├── src/

└── package.json

mono-project

├── node_modules/

│   └── lib@2.0.0

├── packages/

│   ├── packageA

│   │    └── package.json

│   └── packageB

│        └── package.json

└── package.json

优点:

  1. 所有源码在一个仓库内,分支管理与单体应用一样简单。

  2. 公共依赖显示更清晰,更方便统一公共模块版本。

  3. 统一的配置方案,统一的构建策略。

  4. 并行构建,执行效率提升。

  5. 保留 multirepo  的主要优势:

    1. 代码复用;

    2. 模块独立管理;

    3. 分工明确,业务场景独立;

    4. 代码耦合度降低;

    5. 项目引入时,除去非必要组件代码。

  6. CR、MR 由一个仓库发布,阅读和处理十分方便。

缺点:

  1. git 服务根据目录进行访问权限划分,仓库内全部代码开发给所有开发成员(这种非特殊限制场景不用考虑)。

  2. 当代码规模大到一定程度时,git 的操作速度达到瓶颈,影响 git 操作体验(中小型规模不用考虑,而且就算是 def 平台可并行量也为 500)。

4、优缺点对比梳理

        总体来说,当业务发展到一定规模时,monorepo 的升级相比 multirepo 来说,是利远大于弊的

四、在MonorepoPolyrepo(multirepo也是一个意思)之间进行分析

        为了便于讨论,我们假设Monorepo 的对立面是Polyrepo(multirepoPolyrepo是当前开发应用程序的标准方式:每个团队、应用程序或项目都有一个存储库。通常每个存储库都有一个构建工件和简单的构建管道。

        该行业转向 Polyrepo 的做事方式有一个重要原因:团队自治。团队希望自己决定使用哪些库、何时部署应用程序或库以及谁可以贡献或使用他们的代码。

        这些都是好事,那么团队为什么要采取不同的做法呢?因为这种自主权是由隔离提供的,而隔离会损害协作。更具体地说,这些是 Polyrepo 环境的常见缺点:

  1. 繁琐的代码共享 :要跨存储库共享代码,您可能会为共享代码创建一个存储库。现在,您必须设置工具和 CI 环境,将提交者添加到存储库,并设置包发布,以便其他存储库可以依赖它。我们不要开始跨存储库协调第三方库的不兼容版本......

  2. 大量代码重复 :没有人愿意经历设置共享存储库的麻烦,因此团队只需在每个存储库中编写自己的公共服务和组件的实现即可。这不仅浪费了前期时间,而且随着组件和服务的变化,还增加了维护、安全和质量控制的负担。

  3. 对共享库和消费者进行成本高昂的跨存储库更改 :考虑共享库中的严重错误或重大更改:开发人员需要设置其环境,以跨具有断开的修订历史记录的多个存储库应用更改。更不用说版本控制和发布包的协调工作了。

  4. 工具不一致 :每个项目都使用自己的一组命令来运行测试、构建、服务、linting、部署等。不一致会产生记住在项目之间使用哪些命令的心理负担。

单一存储库如何帮助解决所有这些问题呢?
  1. 共享代码和可见性:使用现有的 CI 设置,如果所有使用者都在同一个存储库中,则无需发布版本化的包。

  2. 跨项目的原子性的更改 :每次提交时一切都会协同工作。当您在同一次提交中修复所有内容时,不存在重大更改之类的事情。

  3. 单一版本的依赖项 :无需担心由于项目依赖于第三方库的冲突版本而导致的不兼容性。

  4. 开发人员的移动性 :以一致的方式构建和测试使用不同工具和技术编写的应用程序。开发人员可以自信地为其他团队的应用程序做出贡献,并验证他们的更改是否安全。

五、Monorepo功能

Monorepo 有很多优势,但要发挥作用,需要拥有合适的框架,帮助我们保持快速、易于理解和管理。

  使用 Monorepo 策略后,收益最大的两点是:    
  1. 避免重复安装包,因此减少了磁盘空间的占用,并降低了构建时间;

  2. 内部代码可以彼此相互引用。

Monorepo策略 期望框架能提供以下的功能:

        功能详细介绍可参考https://monorepo.tools/#workspace-analysis

        但是:目前前端领域的 Monorepo 生态的显著特点是只有库,而没有大一统的框架或者完整的构建系统来支持。

   现有Monorepo库

  1. Bazel(谷歌)

  2. Gradle Build Tool(Gradle, Inc)

  3. Lage(微软)

  4. Lerna

  5. moon

  6. Nx(Nrwl)

  7. Pants(Pants Build 社区)

  8. Rush(由 Microsoft)

  9. Turborepo(由 Vercel)

各个库目前所支持的功能

        生态社区中Nx,Lerna,Turborepo脱颖而出。

Monorepo生态

        核心技术:

包管理方案:npmyarnpnpm

包版本方案:LernaChangesets

包构建方案:TurborepoNx

        辅助技术:

代码规范工具:EslintPrettier

提交规范工具:CommitlintCommitizen

六、Monorepo——>观念转变

  Monorepo不仅仅是代码和工具。 monorepo 改变您的组织 以及您思考代码的方式。通过增加一致性、减少创建新项目和执行大规模重构的摩擦、促进代码共享和跨团队协作,它将使您的组织更加高效地工作。

七、Monorepo工作空间

  MonoRepo 的主要构件是工作空间。您构建的每个应用程序和包都将在它自己的工作区中,有它自己的package.json。工作空间可以相互依赖。

您还将拥有一个根工作区在代码库的根文件夹中。这个地方用来:

  1. 指定整个monorepo中存在的依赖关系
  2. 添加在上操作的任务全部monorepo,不仅仅是个人工作区
  3. 添加关于如何使用monorepo的文档

八、包管理方案

1、Npm

        npm 在 v7 才支持了 workspaces,属于终于能用上了但是并不好用的情况,重点是比较慢,通常无法兼容存量的 monorepo 应用,出来的时间太晚了,不能像 yarn 支持自定义 nohoist 以应对某些依赖被 hoist 到 monorepo root 导致的问题,也没有做到像 pnpm 以 link 的方式共享依赖,能显著的减少磁盘占用,除了 npm 自带之外没有其他优点。

2、Yarn

yarn 1.x

        最早支持 workspaces 模式的包管理器配合 lerna 占据了大部分 monorepo,在比较长的一段时间里是 monorepo 的事实标准,缺点是 yarn 的共享包才会提升到 root node_modules 下,其他非共享库都会每个地方留一份,占用空间比较多,还有提升到 root 这一行为也会带来兼容性问题(有些包的 require 方式比较 hack)。

yarn berry(2 ~ 3)

        比较新的点就是 pnp 模式,pnp 模式是为了解决 node_modules 臃肿、复杂度过高的问题而来的,但是比较激进,所以很难支持现有的项目。不过 yarn 3 基本上把各个包管理的功能都支持了(nodeLinker 配置),从功能上可以算是最多,比较复杂,概念好多。吸收了部分竞争对手的优点,并开辟了许多有趣的功能特性。

3、Pnpm
pnpm是什么?

        全称是 “Performant NPM”,即高性能的 npm。

        如它在官方文档介绍的所说:“Saving disk space and boosting installation speed”,Pnpm 是一个能够提高安装速度、节省磁盘空间的包管理工具,并天然支持 Monorepo 的解决方案。它将node_modules从一个单一的可寻址存储连接(links)起来,使得安装速度更快,node_module的目录更小。它有像 yarn workspaces 这样的工作空间功能,当涉及到 node 应用时,它是 monorepo 开发的一个很好的选择。除此之外,它也解决了很多令人诟病的问题,其中,比较经典的就是 Phantom dependencies(幻影依赖)。

pnpm的优缺点:

优点

  1. 天然支持Monorepo(在根目录给所有空间安装依赖、在根目录单独给子包安装依赖)。

  2. 安装依赖速度快,软/硬链接结合。

  3. 安装过的依赖缓存全局复用,缓存逻辑基于文件块,不同版本的依赖可以只缓存 diff。

  4. 自身支持 workspaces 相关。

缺点

  1. 需要手动提升公共依赖。

  2. 需要手动指定任务(dev,build)执行,任务不支持并行执行,影响构建速度。

  3. 不支持自动版本控制,需要依赖第三方工具,官方推荐两个工具changesets、Rush。

  4. 没有通用的脚手架模板。

  5. 不支持缓存。

  6. 不支持依赖分析。

  7. ...

4、各个包管理工具复用 packages:workspace

接下来,简单罗列一下各个包管理工具的 workspaces 特性和注意点(主观观点:从上至下,由差到好)。

包管理工具

准备工作

依赖统一管理

添加本地子项目为依赖

注意点

yarn2

配置workspaces字段

✅ 支持

⚠️ 支持,但使用的是 yarn2 的 workspace 协议

功能很多,基本可以和大型的 monorepo 解决方案媲美。 但 yarn2 自身可用性和稳定性尚待考验,请谨慎选择。

yarn

配置workspaces字段

⚠️ 默认不支持(需要关闭nohoist

⚠️ 支持,但第一次添加依赖需要指定版本号

不支持子项目为依赖 yarn 的 bug 太多了,官方不怎么维护。 如果没有太多需求需要实现,可以一试。

npm

配置workspaces字段

⚠️ 支持(子项目需要通过专门的指令)

✅ 支持

起步较晚,需要很新版本的 npm 但是功能稳定且实用,值得一试

pnpm

添加pnpm-workspace.yaml文件

✅ 支持

⚠️ 支持,但使用的是 pnpm 的 workspace 协议

好使 👍

使用 MonoRepo 策略后,收益最大的两点是:
  1. 避免重复安装包,因此减少了磁盘空间的占用,并降低了构建时间;

  2. 内部代码可以彼此相互引用;

        这两项好处全部都可以由一个成熟的包管理工具来完成,对前端开发而言,如果是 yarn(1.0 以上)或 npm(7.0 以上),则通过名为 workspaces 的特性实现的;推荐使用 pnpm,添加 pnpm-workspace.yaml 文件来实现;

为了实现前面提到的两点收益,您需要在代码中做三件事:
  1. 调整目录结构,将相互关联的项目放置在同一个目录,推荐命名为 packages

  2. 在项目根目录里的 package.json 文件中,设置 workspaces 属性,属性值为之前创建的目录;

  3. 同样,在 package.json 文件中,设置 private 属性为true(为了避免我们误操作将仓库发布);

        经过修改,您的项目目录看起来应该是这样:

.
├── package.json    // `devDependencies`and`scripts`for the monorepo
└── packages/ 
    ├── @mono/project_1    // 社区推荐使用 `@<项目名>/<子项目名>` 的方式命名    
    │   ├── index.js    
    │   └── package.json    // `dependencies`, unique`devDependencies`and`scripts`for the package
    └── @mono/project_2/        
    │   ├── index.js        
    │   └── package.json

九、包版本方案

1、Lerna

1.1、Lerna 工具介绍

        官网:https://lerna.js.org/

   MonoRepo最出名的是使用 Lerna管理 workspacesLerna目前已经交给Nx公司。

        Lerna 是一个管理工具,用于管理包含多个软件包(package)的 JavaScript 项目。它可以优化使用 git 和 npm 管理多包存储库的工作流程Lerna主流应用在处理版本、构建工作流以及发布包等方面都比较优秀,既兼顾版本管理,还支持全量发布和单独发布等功能。在前端领域,它是最早出现也是相当长一段时间 Monorepo 方案的事实标准,具有统治地位,很多后来的工具的概念或者 workspaces 结构都借鉴了 Lerna,是 Lerna的延续。在业界实践中,比较多的时间上,都是采用 Yarn 配合 Lerna组合完整的实现了 Monorepo 中项目的包管理、更新到发布的全流程。

        Lerna后来停更了相当长一段时间,至今还是不支持 pnpm 的 workspaces(pnpm下有 workspace:protocol,lerna 并没有支持),与 yarn 强绑定。

        最近由 nx 的开发公司 nrwl 接手维护,不过新增的 features 都是围绕 nx 而加,nrwl 目前似乎还并没有其他方向的 bug fix 或者新增 features 的计划。不过社区也出现了 lerna-lite ,可以作为 Lerna 长久停滞的补充和替代,主要的新 features 就是支持在 pnpm workspaces。

1.2、Lerna 的使用
  1. 使用 lerna bootstrap --hoist 可以将子项目的node_modules提升到顶层,解决node_modules重复的问题。

  2. 但是 lerna bootstrap --hoist 在提升时如果遇到各个子项目引用的依赖版本不一致,会提升使用最多的版本,从而导致少数派那个找不到正确的依赖,发生错误。

  3. 为了解决提升时版本冲突的问题,引入了 yarn workspace,他也会提升用的最多的版本,但是会为少数派保留自己的依赖在自己的node_modules下面。

  4. 发布的时候使用 lerna publish ,它会自动更新内部依赖,并更新各个子项目自己的版本号。

  5. 子项目的版本号规则可以在 lerna.json 里面配置,如果配置为固定版本号,则各个子项目保持一致的版本,如果配置为 independent 关键字,各个子项目可以有自己不同的版本号。

lerna默认npm,支持yarn,pnpm

  • 克隆下官方demo

git clone https://github.com/lerna/getting-started-example.git

cd getting-started-example

npm install
  • 初始化

npx lerna@latest init
  • 工作区可视化,能形象看到项目之间依赖关系图

npx nx graph

项目依赖关系图

  • 公共包提升根目录

lerna bootstrap --hoist
  • 构建所有项目(自动识别build并以正确的顺序构建)

npx lerna run build
  • 给某个package单独build,使用--scope

npx lerna run build --scope=package1
  • 开启缓存

npx lerna add-caching
  • 自带版本控制

lerna publish --no-private (过滤私有包,不发布)
  • 添加子项目,lerna内置了一些项目模版,例如react

lerna create <pkgName>
1.3、Lerna优缺点

优点

  1. 依赖自动提升

  2. 交给Nx公司之后,支持开启缓存、内部依赖分析、任务分析

  3. 检测改动commit影响的包,区分提示

  4. 自带版本控制(能分析出 private:false 的包,引导版本号提升)。

缺点

  1. 默认npm, npm@7版本以下不支持Monorepo,只能安装根目录的依赖,需要使用 lerna bootstrap --hoist,@npm@7版本以上支持,可以直接npm安装所有子包的依赖。

2.2、turborepo优缺点

优点

  1. 多任务并行处理

  2. 云缓存:多人开发共享缓存

  3. 任务管道

  4. 约定配置:显式声明,执行顺序

缺点

  1. 版本控制不支持,官方推荐Changesets。

2、Changesets

        Changesets 是一个用于 Monorepo 项目下版本以及 Changelog 文件管理的工具。在 Changesets 的工作流会将开发者分为两类人,一类是项目的维护者,还有一类为项目的开发者,开发者在Monorepo项目下进行开发,开发完成后,给对应的子项目添加一个changeset文件。项目的维护者后面会通过changeset来消耗掉这些文件并自动修改掉对应包的版本以及生成CHANGELOG文件,最后将对应的包发布出去。

十、包构建方案

1、Turborepo

1.1、Turborepo 工具介绍

        上述提到传统的 Monorepo 解决方案中,项目构建时如果基于多个应用程序存在依赖构建,耗时是非常可怕的。Turborepo 的出现,正是解决 Monorepo 慢的问题。

        Turborepo 是一个用于 JavaScript 和 TypeScript 代码库的高性能构建系统。通过增量构建、智能远程缓存和优化的任务调度,Turborepo 可以将构建速度提高 85% 或更多,使各种规模的团队都能够维护一个快速有效的构建系统,该系统可以随着代码库和团队的成长而扩展。

1.2、Turborepo核心的功能点如下:
  • 包管理:支持npm,yarn,pnpm

  • turbo运行任务 turbo hello,它必须在turbo.json

{
  "pipeline": {
    "build": {
      //   ^^^^^
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "lint": {},
    "dev": {
      //   ^^^
      "cache": false
    }
  }
}
  • 默认使用缓存

  • 自动识别项目中的 dev, build 命令,并全量运行

turbo build
  • 仅运行某个单独的包,使用--filter

turbo dev --filter package1

2、Nx

        定位上是  Smart, Fast and Extensible build system,出现得比较早,发展了挺久,功能特别多,基本上 cover 了各种应用场景,文档也比较详细,是现在几个 Monorepo 工具里比较接近完整的解决方案和框架的。

        最近的话他们也接手了 lerna 的维护,不过给 lerna 加的东西都是围绕 nx 而来。

十一、其它生态工具,及Monorepo 最终选用的工具链

1、其它生态工具

1.1、Bolt

        和 lerna 类似,更像是一个  Task Runner,用于执行 workspaces 下的各种 script,用法上和 npm 的 workspaces 类似,已经停更一段时间。

1.2、Preconstruct

        Monorepo 下统一的 Dev/Build 工具。亮点是 dev 模式使用了执行时的 require hook,直接引用源文件在运行时执行转译(babel),不需要在开发时 watch 产物实时构建,调试很方便。用法上比较像 parcel、microbundle 那样 zero-config bundler,使用统一的 package.json 字段来指定输出产物,缺点是比较死板,整个项目的配置都得按照这种配置方式,支持的选项目前还不多,不够灵活。

1.3、Rushstack

        体感上比较像 Lerna + Turborepo + 各种东西的工具链,比较老牌,但是没用过,也很少见到有用这个的。

1.4、Lage

        Microsoft 出的,定位上是一个 Task Runner in JS Monorepos ,亮点是 pipeline 的任务模式,构建产物缓存,远程缓存等。

2、Monorepo 最终选用的工具链

        最终选用工具链: pnpm + lerna-lite + turborepo

2.1、Pnpm

        pnpm 的依赖全局缓存(global store and hard link)与安装方式即是天然的依赖共享,相同版本的依赖只会安装一次,有效地节约空间以及节省安装时间,在 monorepo 场景下十分切合。

2.2、Lerna-lite

        pnpm 推荐的方案其实是 changesets,但是 changesets 的发版流程更贴近 Github Action Workflow,以及打 changeset 然后 version 的概念和流程相对 lerna 会复杂一些。

        不直接使用 lerna 的原因是 lerna 并不支持 pnpm 的 workspace protocol 。

        同时 lerna 比较久没有更新,虽然最近被 nx 的组织 nrwl 接管了,但是 nrwl 只扩展了针对 nx + lerna 场景的功能,并没有对 lerna 的其他功能进行维护,所以社区里出现了 lerna-lite,真正意义上的延续了 lerna 的发展,目前比较有用的新功能是其 publish 命令支持了 pnpm 的 workspace protocol (workspace:),支持了 pnpm workspace 下 lerna 的发布流程。

2.3、Turborepo

        如果有高速构建缓存需求则使用 turborepo。Turborepo 的基本原则是从不重新计算以前完成的工作,Turborepo 会记住你构建的内容并跳过已经计算过的内容,把每次构建的产物与日志缓存起来,下次构建时只有文件发生变动的部分才会重新构建,没有变动的直接命中缓存并重现日志。在多次构建开发时,这也就意味更少的构建耗时。

十二、统一配置:合并同类项

( - Eslint,Typescript 与 Babel 等)

        您一定同意,编写代码要遵循 DRY 原则(Don't Repeat Yourself 的缩写)。那么,理所当然地,我们应该尽量避免在多个子项目中放置重复的 eslintrc,tsconfig 等配置文件。幸运的是,Babel,Eslint 和 Typescript 都提供了相应的功能让我们减少自我重复。

TypeScript

        我们可以在 packages 目录中放置tsconfig.settting.json文件,并在文件中定义通用的 ts 配置,然后,在每个子项目中,我们可以通过extends属性,引入通用配置,并设置compilerOptions.composite的值为true,理想情况下,子项目中的tsconfig文件应该仅包含下述内容:

{  
    "extends": "../tsconfig.setting.json",    // 继承 packages 目录下通用配置  
    "compilerOptions": {
        "composite": true,    // 用于帮助 TypeScript 快速确定引用工程的输出文件位置   
        "outDir": "dist",    
        "rootDir": "src"  
    },  
    "include": ["src"]
}

Eslint

        对于 Eslint 配置文件,我们也可以如法炮制,这样定义子项目的.eslintrc文件内容:

{  
    "extends": "../../.eslintrc", // 注意这里的不同  
    "parserOptions": {   
     "project": "tsconfig.json"  
    }
}

        注意到了吗,对于通用的 eslint 配置,我们并没有将其放置在 packages 目录中,而是放在整个项目的根目录下,这样做是因为一些编辑器插件只会在项目根目录寻找.eslintrc文件,因此为了我们的项目能够保持良好的「开发环境一致性」,请务必将通用配置文件放置在项目的根目录中。

Babel

Config Files · Babel

        Babel 配置文件合并的方式与 TypeScript 如出一辙,甚至更加简单,我们只需在子项目中的.babelrc文件中这样声明即可:

{ "extends": "../.babelrc" }

        当一切准备就绪后,我们的项目目录应该大致呈如下所示的结构:

.
├── package.json
├── .eslintrc
└── packages/    
│   ├── tsconfig.settings.json    
│   ├── .babelrc    
├── @mono/project_1/    
│   ├── index.js    
│   ├── .eslintrc    
│   ├── .babelrc    
│   ├── tsconfig.json    
│   └── package.json
└───@mono/project_2/      
    ├── index.js        
    ├── .eslintrc      
    ├── .babelrc        
    ├── tsconfig.json        
    └── package.json

 

十三、Monorepo有哪些缺点

  1.  性能问题:当仓库的代码规模非常的巨大,达到GB/TB的级别,会增大开发环境的代码git clone、pull成本,以及安装成本,本地硬盘的压力,执行git status也可能需要花费数秒甚至数分钟的时间。
  2. 打包构建需要专门优化,否则会出现打包时间过长。
  3. 权限管理问题:项目粒度的权限管理较为困难,github、gitlab权限目前不支持文件夹级别。
  4. 特定开发工具和框架限制,灵活性差。

十四、未来趋势

        期望Nx 或者 Turborepo 这样的库往完整的框架发展,或者包管理器pnpm/npm自身就逐步支持相应的功能,不需要过多的三方依赖。

  • 19
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值