前端代码 Monorepos 指南

已发表: 2022-03-11

Monorepos 是一个热门话题。 最近有很多关于为什么应该和不应该在项目中使用这种类型的架构的文章,但其中大多数都以某种方式存在偏见。 本系列试图收集和解释尽可能多的信息,以了解如何以及何时使用 monorepos。

Monorepository是一个架构概念,它基本上包含了其标题中的所有含义。 您无需管理多个存储库,而是将所有隔离的代码部分保存在一个存储库中。 请记住隔离这个词——这意味着 monorepo 与单体应用程序没有任何共同之处。 您可以在一个 repo 中保存多种逻辑应用程序; 例如,一个网站及其 iOS 应用程序。

单一仓库、单一仓库和多仓库的比较

这个概念比较古老,大约在十年前就出现了。 谷歌是最早采用这种方法来管理其代码库的公司之一。 你可能会问,如果它已经存在了十年,那为什么到现在才这么火? 大多数情况下,在过去的 5-6 年中,许多事情都发生了巨大的变化。 ES6、SCSS 预处理器、任务管理器、npm 等——如今,要维护一个基于 React 的小型应用程序,您必须处理项目捆绑程序、测试套件、CI/CD 脚本、Docker 配置,还有谁知道还有什么。 现在想象一下,您需要维护一个由许多功能区域组成的巨大平台,而不是一个小应用程序。 如果您正在考虑架构,您将需要做两件主要的事情:分离关注点并避免代码重复。

为了实现这一点,您可能希望将大型功能隔离到一些包中,然后通过主应用程序中的单个入口点使用它们。 但是你如何管理这些包? 每个包都必须有自己的工作流环境配置,这意味着每次你想创建一个新包时,你都必须配置一个新环境,复制所有配置文件等等。 或者,例如,如果您必须更改构建系统中的某些内容,您将不得不检查每个 repo、进行提交、创建拉取请求并等待每个构建,这会大大减慢您的速度。 在这一步,我们遇到了 monorepos。

与其拥有大量带有自己配置的存储库,我们将只有一个事实来源——monorepo:一个测试套件运行器、一个 Docker 配置文件和一个 Webpack 配置。 而且您仍然拥有可扩展性、分离关注点的机会、与通用包共享代码以及许多其他优点。 听起来不错,对吧? 嗯,是的。 但也有一些缺点。 让我们仔细看看在野外使用 monorepo 的确切利弊。

Monorepo 优势:

  • 一个存储所有配置和测试的地方。 由于所有内容都位于一个存储库中,因此您可以配置一次 CI/CD 和捆绑器,然后只需重新使用配置来构建所有包,然后再将它们发布到远程。 单元测试、e2e 和集成测试也是如此——您的 CI 将能够启动所有测试,而无需处理额外的配置。
  • 使用原子提交轻松重构全局功能。 无需为每个 repo 执行拉取请求,找出构建更改的顺序,您只需要发出一个原子拉取请求,该请求将包含与您正在处理的功能相关的所有提交。
  • 简化包发布。 如果您计划在依赖于另一个具有共享代码的包的包中实现新功能,则可以使用单个命令来完成。 这是一个需要一些额外配置的功能,稍后将在本文的工具评论部分讨论。 目前,有丰富的工具可供选择,包括 Lerna、Yarn Workspaces 和 Bazel。
  • 更容易的依赖管理。 只有一个package.json 。 每当您想更新依赖项时,都无需在每个 repo 中重新安装依赖项。
  • 通过共享包重用代码,同时保持它们的隔离。 Monorepo 允许您从其他包中重用您的包,同时保持它们彼此隔离。 您可以使用对远程包的引用并通过单个入口点使用它们。 要使用本地版本,您可以使用本地符号链接。 此功能可以通过 bash 脚本或引入一些其他工具(如 Lerna 或 Yarn)来实现。

Monorepo的缺点:

  • 无法限制仅访问应用程序的某些部分。 不幸的是,你不能只共享你的 monorepo 的一部分——你必须授予对整个代码库的访问权限,这可能会导致一些安全问题。
  • 处理大型项目时 Git 性能不佳。 这个问题开始只出现在具有超过一百万次提交和数百名开发人员每天在同一个 repo 上同时工作的大型应用程序上。 这变得特别麻烦,因为 Git 使用有向无环图 (DAG) 来表示项目的历史。 随着大量提交,任何遍历图表的命令都可能随着历史的加深而变慢。 由于引用的数量(即,分支或标签,可通过删除不再需要的引用来解决)和跟踪的文件数量(以及它们的重量,尽管可以使用解决重文件问题),性能也会降低Git LFS)。

    注意:如今,Facebook 试图通过修补 Mercurial 来解决 VCS 可扩展性问题,而且可能很快,这不会是一个大问题。

  • 更高的构建时间。 因为您将在一个地方拥有大量源代码,所以您的 CI 将需要更多时间来运行所有内容以批准每个 PR。

工具审查

用于管理 monorepos 的工具集在不断增长,目前,很容易迷失在 monorepos 的各种构建系统中。 通过使用此存储库,您始终可以了解流行的解决方案。 但是现在,让我们快速浏览一下现在大量使用 JavaScript 的工具:

  • Bazel 是 Google 的面向 monorepo 的构建系统。 更多关于 Bazel:awesome-bazel
  • Yarn 是一个 JavaScript 依赖管理工具,通过工作空间支持 monorepos。
  • Lerna 是一个基于 Yarn 构建的用于管理具有多个包的 JavaScript 项目的工具。

大多数工具都使用非常相似的方法,但存在一些细微差别。

monorepo git 仓库的 CI/CD 流程图解

我们将深入探讨 Lerna 工作流程以及本文第 2 部分中的其他工具,因为它是一个相当大的主题。 现在,让我们大致了解一下里面的内容:

勒纳

这个工具在处理语义版本、设置构建工作流、推送包等方面确实很有帮助。Lerna 背后的主要思想是你的项目有一个包文件夹,其中包含所有独立的代码部分。 除了包之外,您还有一个主应用程序,例如可以位于 src 文件夹中。 Lerna 中几乎所有的操作都通过一个简单的规则来工作——你遍历所有的包,并对它们执行一些操作,例如,增加包版本、更新所有包的依赖关系、构建所有包等。

使用 Lerna,您有两种使用包的选择:

  1. 无需将它们推送到远程(NPM)
  2. 将您的包裹推送到远程

在使用第一种方法时,您可以为您的包使用本地引用,并且基本上并不真正关心符号链接来解析它们。

但是如果您使用第二种方法,您将被迫从远程导入您的包。 (例如, import { something } from @yourcompanyname/packagename; ),这意味着您将始终获得软件包的远程版本。 对于本地开发,您必须在文件夹的根目录中创建符号链接,以使捆绑程序解析本地包,而不是使用node_modules/中的包。 这就是为什么在启动 Webpack 或您最喜欢的捆绑程序之前,您必须启动lerna bootstrap ,它会自动链接所有包。

在单个节点包中对模块进行命名空间的图示

Yarn 最初是 NPM 包的依赖管理器,最初不是为了支持 monorepos 而构建的。 但在 1.0 版本中,Yarn 开发人员发布了一个名为Workspaces的功能。 在发布时,它并不是那么稳定,但过了一段时间,它就可以用于生产项目了。

Workspace基本上是一个包,它有自己的package.json并且可以有一些特定的构建规则(例如,如果您在项目中使用 TypeScript,则需要一个单独的tsconfig.json 。)。 实际上,您可以使用 bash 以某种方式在没有 Yarn Workspaces 的情况下进行管理,并具有完全相同的设置,但是此工具有助于简化每个包的安装和更新依赖项的过程。

一目了然,Yarn 及其工作区提供了以下有用的功能:

  1. 所有包的根目录中的单个node_modules文件夹。 例如,如果您有packages/package_apackages/package_b — 带有它们自己的package.json — 所有依赖项将仅安装在根目录中。 这是 Yarn 和 Lerna 工作方式的不同之处之一。
  2. 依赖符号链接以允许本地包开发。
  3. 所有依赖项的单个锁定文件。
  4. 如果您只想为一个包重新安装依赖项,则重点关注依赖项更新。 这可以使用-focus标志来完成。
  5. 与 Lerna 集成。 您可以轻松地让 Yarn 处理所有安装/符号链接,并让 Lerna 负责发布和版本控制。 这是迄今为止最流行的设置,因为它需要较少的精力并且易于使用。

有用的链接:

  • 纱线工作区
  • 如何构建 TypeScript mono-repo 项目

巴泽尔

Bazel 是一个大型应用的构建工具,可以处理多语言依赖,支持很多现代语言(Java、JS、Go、C++等)。 在大多数情况下,将 Bazel 用于中小型 JS 应用程序是大材小用,但在大规模上,由于其性能,它可能会提供很多好处。

从本质上讲,Bazel 看起来类似于 Make、Gradle、Maven 和其他允许基于包含构建规则和项目依赖项描述的文件构建项目的工具。 Bazel 中的同一个文件称为BUILD ,位于 Bazel 项目的工作空间内。 BUILD文件使用它的 Starlark,这是一种人类可读的高级构建语言,看起来很像 Python。

通常,您不会与BUILD打交道,因为有很多样板可以在 Web 上轻松找到,并且已经配置好并准备好进行开发。 每当您想构建项目时,Bazel 基本上都会执行以下操作:

  1. 加载与目标相关的BUILD文件。
  2. 分析输入及其依赖关系,应用指定的构建规则,并生成操作图。
  3. 对输入执行构建操作,直到生成最终构建输出。

有用的链接:

  • JavaScript 和 Bazel – 从头开始​​为 JS 设置 Bazel 项目的文档。
  • Bazel 的 JavaScript 和 TypeScript 规则 – JS 的样板。

结论

Monorepos 只是一个工具。 关于它是否有未来有很多争论,但事实是,在某些情况下,这个工具可以有效地完成它的工作并处理它。 在过去几年的过程中,这个工具已经发展,获得了更多的灵活性,克服了很多问题,并在配置方面移除了一个复杂层。

还有很多问题需要解决,比如 Git 性能不佳,但希望这会在不久的将来得到解决。

如果您想学习为您的应用程序构建强大的 CI/CD 管道,我推荐如何使用 GitLab CI 构建有效的初始部署管道

相关:增强的 Git 流程解释