Monorepo 是一种项目代码办理方法,指单个库房中办理多个项目,有助于简化代码同享、版别操控、构建和布置等方面的复杂性,并供给更好的可重用性和协作性。Monorepo 提倡了开放、透明、同享的组织文明,这种办法现已被许多大型公司广泛运用,如 Google、Facebook 和 Microsoft 等。

mono 来源于希腊语 意味单个的,而 repo,清楚明了地,是 repository 的缩写。将不同的项目的代码放在同一个代码库房中,这种把鸡蛋放在同一个篮子里的做法或许乍看之下有些古怪,但实践上,这种代码办理方法有许多长处。

在咱们前端开发当中运用的 Vue 和 React 都是在 Monorepo 战略库房中开发出来的。

Monorepo 的进化

从单库房巨石运用(Monolith),到多库房多模块运用(MultiRepo),最后转向单库房多模块运用(MonoRepo)。每个阶段都有其优势和应战,挑选哪种方法取决于项目的具体需求和团队的作业流程。

  1. 单库房巨石运用(Monolith):这种结构在项目初期比较常见,由于一切都在一个库房中,所以便于办理和布置。但跟着项目的增加,这种结构的缺陷逐步显现,包含但不限于构建时刻的增加、代码抵触的频频、以及难以保护。

  2. 多库房多模块运用(MultiRepo):为了战胜巨石运用的缺陷,项目或许被拆分成多个较小的模块,每个模块运用独自的库房办理。这样做可以提高模块的独立性,便于团队并行开发和保护,但也带来了新的应战,比方跨库房的依靠办理、版别同步问题以及作业流程的复杂性增加。

  3. 单库房多模块运用(MonoRepo):为了处理多库房办理带来的问题,有些团队和项目转向运用单库房来办理多个模块。这种方法可以简化跨模块的依靠办理,提高代码同享的功率,而且可以一致构建和测验流程。不过,MonoRepo 也有其应战,比方需求更精密的权限操控、大规模库房的功能优化等。

每种办法都有其适用场景,没有肯定的好坏。例如,小到中型项目或许会更倾向于运用 Monolith 或 MultiRepo,而大型项目和大型团队或许会从 MonoRepo 中获益,尤其是当需求频频地跨模块协作时。在挑选最合适自己项目的战略时,需求权衡各种因素,包含团队规模、项目复杂度、构建和测验流程的需求等。

为什么PNPM可以完结Monorepo

为什么PNPM可以完结Monorepo
一个真实的 Monorepo 不仅仅是将多个项目的代码放在同一个代码库中。它还需求这些项目之间有明晰界说的联系。假如这些项目之间没有杰出界说的联系,那么就不能称之为 Monorepo。

类似地,假如一个代码库中包含了一个巨大的运用,而没有对其进行切割和封装,那么这仅仅一个大型的代码库,而不是真实的 Monorepo。即便你给它取一个花里胡哨的名字,也不能改动它的本质。

Monorepo 中的各个项目(或模块、组件)之间应该有明晰、明晰的依靠联系和接口界说。这有助于保证模块之间可以高效协作,一起坚持一定程度的独立性和可重用性。

Monorepo 好坏

为什么PNPM可以完结Monorepo

场景 MultiRepo MonoRepo
代码可见性 ✅ 由于项目被涣散在不同的库房中,可以对每个库房施行独立的拜访操控,这有助于保护敏感代码,削减安全危险。
❌ 由于代码涣散在多个库房中,重用通用代码或库变得愈加困难。开发人员或许需求复制代码到他们的库房中,这会导致重复劳动和保护上的困难。
✅ 一切代码都在一个库房中,使得代码的同享和重用变得十分便利。开发人员可以轻松拜访和运用公共库和东西,促进了代码的一起性和功率。
❌ 尽管可以经过精密的权限操控限制对特定代码部分的拜访,但在大型 MonoRepo 中办理这些权限或许会变得复杂和耗时。
依靠办理 ✅ 每个项目可以独立办理自己的依靠版别,这有助于避免因同享依靠导致的版别抵触问题。
❌ 多个项目或许会依靠同一库的不同版别,这或许导致重复的装备作业和保护本钱。
❌当同享的库需求更新时,各个项目需求别离进行更新,这或许导致同步和一起性问题。
❌假如项目间存在依靠,办理这些依靠联系或许会变得复杂。
✅ 一切项目同享相同的依靠库版别,这简化了依靠办理,削减了版别抵触的或许性。
✅ 当同享库需求更新时,整个库房中的一切项目可以一起更新,保证了依靠的一起性。
✅ 由于一切项目运用相同的依靠版别,当发现某个依靠的问题时,可以快速地识别出一切受影响的项目并进行修正。
❌一切项目有必要运用相同版别的依靠,这或许限制了某些项目运用特定版别的才能,特别是当某些项目需求运用较新或较旧版别的依靠时。
开发迭代 ✅ 在多库房形式下,每个库房可以独立进行迭代,不受其他项目进度的影响。这意味着团队可以依据每个项目的需求和优先级安排迭代计划。
✅ 由于每个项目独立办理,团队可以为每个项目挑选最合适的技术栈、东西和流程,提高了迭代进程的灵活性。
❌当需求在多个项目之间进行协作或同享代码时,跨库房的协作或许会增加交流和整合的本钱。
❌在多库房形式下,跨项目的依靠办理或许会变得复杂,需求额定的尽力来保证依靠项的一起性和兼容性,这或许会拖慢迭代速度。
✅ 一切项目和模块同享同一个库房,使得团队可以采用一致的作业流程、构建和测验东西,简化了迭代进程。
✅ 当同享库需求更新时,整个库房中的一切项目可以一起更新,保证了依靠的一起性。
❌ 在 MonoRepo 中,一切项目同享同一个版别历史,这或许会导致版别操控日志变得乱七八糟,使得追寻特定项目的更改动得愈加困难。
❌关于十分大的库房,构建和测验的速度或许会成为问题,尤其是当不需求构建整个库房的一切部分时。尽管有战略如增量构建和缓存可以缓解这个问题,但需求额定的装备和保护作业。
工程装备 ✅ 每个库房可以有其独立的构建、测验和布置装备,这答应项目依据自己的特定需求定制化工程装备,供给了高度的灵活性。
✅ 相关于 MonoRepo,单个项目的装备一般更简略、更直接,由于它只需求关注本身的需求,而不是有必要考虑到与其他项目的协作和兼容性。
❌跟着库房数量的增加,重复的装备和东西链设置或许导致保护本钱增加。每个项目或许需求独自保护构建脚本、依靠办理文件、CI/CD 装备等
❌在多库房环境中,不同项目之间的装备或许会呈现不一起,导致构建、测验和布置流程的差异,增加了团队成员之间协作的复杂性。
✅ 一切项目同享同一个构建体系和东西链,这有助于保证整个代码库的一起性和可保护性,简化了工程装备的办理。
✅ 具和依靠库的版别可以在整个库房中一致办理,削减了版别抵触的或许性,并保证一切项目都运用了正确的东西和库版别。
❌ 由于一切项目运用相同的依靠版别,当发现某个依靠的问题时,可以快速地识别出一切受影响的项目并进行修正。
跟着项目数量和类型的增加,MonoRepo 的装备或许变得复杂,需求更复杂的东西和脚本来支撑不同类型的项目和构建流程。
❌ 关于大型 MonoRepo,构建和测验整个库房或许十分耗时,尽管可以经过各种优化技术(如增量构建和缓存)来缓解这一问题。
构建布置 ✅ 每个库房可以独立构建和布置,这答应项目团队按照自己的时刻表和需求来更新服务,提高了布置的灵活性。
✅ 项目之间的阻隔性削减了构建和布置进程中的相互影响,一个项目的更改不会直接影响到其他项目的构建或稳定性。
❌在多库房结构中,类似的构建和布置流程或许需求在多个项目中重复装备,导致保护本钱和作业量增加。
❌当项目之间存在依靠联系时,协谐和同步不同库房的构建和布置变得愈加复杂,尤其是在进行大规模更新时。
✅ 一切项目同享同一个构建体系,这有助于简化和标准化构建流程,提高功率。
✅ 在 MonoRepo 中,触及多个项目的更改可以在一个提交中完结,这简化了回滚和盯梢更改的进程,提高了布置的可靠性。
✅ 由于一切代码都在同一个库房中,办理和晋级跨项目依靠变得愈加简略,有助于保证依靠的一起性。
❌ 关于大型 MonoRepo,即便只更改了库房中的一小部分,也或许需求重新构建整个库房,导致构建时刻明显增加。尽管可以经过增量构建和其他优化措施来缓解,但这需求额定的装备和资源。
跟着项目数量和类型的增加,MonoRepo 的装备或许变得复杂,需求更复杂的东西和脚本来支撑不同类型的项目和构建流程。
❌ MonoRepo 或许限制了布置粒度,由于一切项目同享相同的构建和布置流程。这或许导致即便只需布置一个小改动,也或许需求重新构建和布置整个代码库中的多个项目。

Monorepo 运用场景

Monorepo(单库房)形式适用于多种场景,特别是在以下情况下,运用 Monorepo 可以带来明显的长处:

  1. 大型团队协作: 当大型团队在多个相关项目上协作时,Monorepo 可以简化协作流程。由于一切项目都位于同一库房中,团队成员可以轻松拜访和修正跨项目的代码,促进了团队间的交流和协作。

  2. 微服务架构: 在微服务架构中,体系由多个小型、独立服务组成。运用 Monorepo 可以便利地办理这些服务的代码,保证服务之间的兼容性,并简化跨服务的重构和同享代码。

  3. 多渠道/多产品开发 关于跨多个渠道(如 Web、iOS、Android)或多个产品线开发的公司,Monorepo 可以供给一个一致的代码基础,使得同享通用库、组件和东西变得简略,一起坚持构建和发布流程的一起性。

  4. 同享库和组件 在开发触及多个同享库或可重用组件的项目时,Monorepo 答应开发人员轻松更新和保护这些同享资源。这有助于提高代码重用率,降低保护本钱。

  5. 一致的东西和流程: 关于期望一致代码风格、构建东西、测验框架和布置流程的团队,Monorepo 供给了一个一起的基础设施,有助于标准化开发实践,简化新成员的入职进程。

  6. 原子性更改和重构: 当需求对跨多个项目或模块的代码进行重构或更新时,Monorepo 使得这些更改可以作为一个原子提交进行,降低了布置和回滚的复杂性。

一致装备:兼并同类项 – Eslint,Typescript 与 Babel

在 Monorepo 项目中一致装备 ESLint、TypeScript 和 Babel 可以协助坚持代码的一起性,简化项目保护,并提高开发功率。

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.js 文件中,运用 extends 字段来继承顶层装备,并增加或覆盖规矩。

module.exports = {
  extends: "../../.eslintrc.js",
  rules: {
    // 重写或增加规矩
  },
};

babel

Babel 装备文件兼并的方法与 TypeScript 如出一辙,甚至愈加简略,咱们只需在子项目中的 .babelrc 文件中这样声明即可:

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

当一切准备结束的时分,咱们项目目录应该大致呈如下所示的结构:

├── package.json
├── .babelrc
├── .eslintrc
├── tsconfig.setting.json
└── 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

为什么 pnpm 能完结 Monorepo

首先咱们来讲一下 pnpm 的中心亮点吧,便是它的软链接和硬链接吧,pnpm 运用一种称为内容寻址存储的办法来保存依靠项。在这种机制下,依靠项的存储方位基于其内容的哈希值,这意味着:

  1. 假如多个项目依靠相同版别的包,这个包在大局存储中只有一份副本,各个项目经过硬链接指向这个副本,然后明显削减了磁盘空间的占用。

  2. 内容寻址机制保证了依靠项的完整性,由于任何对文件内容的更改都会导致哈希值的变化,然后防止了依靠污染和意外更改。

其间一个受咱们比较欢迎的便是咱们打开 pnpm 官网就能直接看到的内容,那便是装置快:

为什么PNPM可以完结Monorepo

pnpm 在装置依靠包时,首要经历了以下三个进程:解析依靠、获取依靠以及链接依靠。这个进程经过优化来保证高效的依靠办理,尤其在处理大型项目或 Monorepo 时。

  1. 解析依靠(Dependency Resolution) 在这个阶段,pnpm 需求确认要装置的每个依靠包的具体版别。它会查看项目的 package.json 文件以及任何现有的锁文件(如 pnpm-lock.yaml),来决定哪些版别的包需求被装置。解析依靠时,pnpm 会遵从以下规矩:

    • 版别兼容性:基于 package.json 中指定的版别规模,挑选与之兼容的最新版别。
    • 锁文件:假如存在锁文件,pnpm 会优先运用锁文件中锁定的版别,以保证依靠的一起性和项目的可重现性。
  2. 获取依靠(Fetching Dependencies) 一旦确认了需求装置的依靠版别,pnpm 将开端获取这些依靠包。这个进程包含以下几个进程:

    • 查看大局存储:pnpm 首先会查看其大局存储中是否现已存在所需版别的依靠包。假如现已存在,就不需求从长途库房下载,直接重用即可。
    • 下载缺失的依靠:关于大局存储中不存在的依靠,pnpm 会从 npm 或其他装备的库房下载它们。下载的依靠包会被存储在大局存储中,以便将来重用。
    • 内容寻址存储:pnpm 运用内容寻址方法来存储依靠包,即依据包内容的哈希值来确认存储路径。这保证了相同内容的包在大局存储中只有一份副本,节省了磁盘空间。
  3. 链接依靠(Linking Dependencies) 获取依靠包之后,pnpm 需求将这些依靠链接到项目的 node_modules 目录中,使得项目可以运用这些依靠。这个进程触及:

    • 创立硬链接和符号链接:关于每个依靠包,pnpm 会在项目的 node_modules 目录中创立指向大局存储中相应包的硬链接。假如是包内部的依靠,还或许创立符号链接来坚持正确的依靠结构。
    • pnpm 经过构建一个虚拟的 node_modules 目录来模拟传统的嵌套依靠结构,但实践上依靠之间是经过符号链接相连的。这样做既坚持了 npm 生态的兼容性,又避免了重复的依靠副本和深层嵌套的问题。
    • 经过这种链接方法,pnpm 保证了项目只能拜访其直接依靠的包,防止了对未声明依靠的意外拜访,提高了项目的稳定性和安全性。

经过上述三个进程,pnpm 完结了对依靠的高效办理,优化了存储空间的运用,加快了依靠装置的速度,一起还保证了项目依靠的一起性和阻隔性。

pnpm 在装置依靠时可以并行执行多个使命,比方解析依靠、下载和链接依靠。这种并行处理机制充分利用了现代多核 CPU 的功能,明显削减了装置进程的总时刻。

pnpm 装置速度快除了上面提到的这些原因之外,它的另一个长处是它支撑增量更新。当你增加或更新项目依靠时,pnpm 只会下载那些实践改动了的包。假如某个包的版别现已存在于大局存储中,pnpm 将重用这个版别,避免了不必要的下载,然后加快了装置进程。

在 Monorepo 中,包之间经常相互依靠。pnpm 经过 Workspace 协议支撑这种内部依靠,答应包在其 package.json 中直接引用 Monorepo 中的其他包,如:

"dependencies": {
  "foo": "workspace:^1.0.0"
}

这种方法使得在本地开发时,包之间可以轻松地相互依靠,而不需求发布到 npm 上。pnpm 会主动处理这些内部依靠,并保证正确的链接和版别匹配。

在 workspace 形式下,项目根目录一般不会作为一个子模块或许 npm 包,而是首要作为一个办理中枢,执行一些大局操作,装置一些共有的依靠,每个子模块都能拜访根目录的依靠,合适把 TypeScript、eslint 等公共开发依靠装在这里,下面简略介绍一些常用的中枢办理操作。

在项目跟目录下运转 pnpm install,pnpm 会依据当前目录 package.json 中的依靠声明装置悉数依靠,在 workspace 形式下会一并处理一切子模块的依靠装置。

装置项目公共开发依靠,声明在根目录的 package.json – devDependencies 中。-w 选项代表在 monorepo 形式下的根目录进行操作。

// 装置
pnpm install -wD xxx
// 卸载
pnpm uninstall -w xxx

执行根目录的 package.json 中的脚本

pnpm run xxx

在 workspace 形式下,pnpm 首要经过 –filter 选项过滤子模块,完结对各个作业空间进行精密化操作的目的。

例如 a 包装置 lodash 外部依靠,-S 和 -D 选项别离可以将依靠装置为正式依靠(dependencies)或许开发依靠(devDependencies):

// 为 a 包装置 lodash
pnpm --filter a add -S lodash // 出产依靠
pnpm --filter a add -D lodash // 开发依靠

指定模块之间的相互依靠。下面的比如演示了为 a 包装置内部依靠 b。

// 指定 a 模块依靠于 b 模块
pnpm --filter a i -S b

pnpm workspace 对内部依靠联系的表示不同于外部,它自己约定了一套 Workspace 协议。下面给出一个内部模块 a 依靠同是内部模块 b 的比如。

{
  "name": "a",
  // ...
  "dependencies": {
    "b": "workspace:^"
  }
}

在实践发布 npm 包时,workspace:^ 会被替换成内部模块 b 的对应版别号(对应 package.json 中的 version 字段)。替换规律如下所示:

{
  "dependencies": {
    "a": "workspace:*", // 固定版别依靠,被转换成 x.x.x
    "b": "workspace:~", // minor 版别依靠,将被转换成 ~x.x.x
    "c": "workspace:^" // major 版别依靠,将被转换成 ^x.x.x
  }
}

参考文献

总结

经过本文咱们学习到了 Monorepo 是什么,以及 Monorepo 的演化,进而学习到了为什么 pnpm 可以完结 Monorepo,在后面的内容中会持续共享构建型的 Monorepo 计划。

最后共享两个我的两个开源项目,它们别离是:

这两个项目都会一直保护的,假如你也喜欢,欢迎 star