作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
亚历山大·诺埃尔的头像

Alexander Noel

Alexander是一位经验丰富的前端开发人员,曾开发过电子商务和企业网站以及web和移动应用程序.

Years of Experience

8

Share

最近有很多文章都是关于独角鲸的利弊,但大多数都是有偏见的. 本单线程架构指南解释了如何以及何时有效地使用这种风格的代码管理.

What Is a Monorepo?

A mono repository 是一个建筑概念,基本上包含了其标题的所有含义. 而不是管理多个存储库, 您将所有独立的代码部分保存在一个存储库中. 记住这个词 isolated-这意味着单块架构与单块应用程序没有任何共同之处. You can keep many kinds of logical apps inside one repo; for example, a website and its iOS app.

单回购、单回购和多回购的比较

这个概念相对较老,大约出现在十年前. 谷歌是最早采用这种方法管理代码库的公司之一. 你可能会问,如果它已经存在了十年,那为什么直到现在才成为一个热门话题? 大多数情况下,在过去的5到6年里,许多事情都发生了巨大的变化. ES6、SCSS预处理器、任务管理器、npm等.—nowadays, 来维护一个基于react的小应用, 你必须与项目捆绑者打交道, test suites, CI/CD scripts, 码头工人配置, 谁知道还有什么. 现在想象一下,不是一个小的应用程序, 您需要维护一个由许多功能区域组成的巨大平台. 如果你在考虑建筑, 您将需要做两件主要的事情:分离关注点并避免代码重复.

To make this happen, 你可能想要将大型功能隔离到一些包中,然后通过主应用程序中的单个入口点使用它们. 但是如何管理这些包呢? 每个包都必须有自己的工作流环境配置, 这意味着每次您想要创建一个新包时, 您必须配置一个新环境, 复制所有配置文件, and so on. Or, for example, 如果你必须改变你的构建系统中的一些东西, 你必须把每个回购都看一遍, do a commit, 创建拉取请求, 等待每一次构建, 这会让你慢下来吗. 在这一步,我们遇到的是独角兽.

而不是有很多仓库与他们自己的配置, 我们将只有一个真实的来源——单一的:一个测试套件运行者, 一个Docker配置文件, 和Webpack的一个配置. 而且你还有可扩展性, 分离关注点的机会, 与通用包共享代码, 还有很多其他的优点. Sounds nice, right? Well, it is. 但也有一些缺点. 让我们仔细看看在野外使用monorepo的确切利弊.

Monorepo Advantages:

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

Monorepo缺点:

  • 没有办法限制只能访问应用程序的某些部分. Unfortunately, 您不能只共享单个代码库的一部分,您必须允许访问整个代码库, 这可能会导致一些安全问题.
  • 在处理大型项目时,Git的性能很差. 这个问题开始只出现在 huge 每天有超过100万次提交和数百名开发人员同时在同一个repo上工作的应用程序. 当Git使用有向无环图(DAG)来表示项目的历史时,这变得特别麻烦. 有大量的提交, 任何遍历图的命令都可能随着历史的加深而变慢. 由于refs的数量(i),性能也会减慢.e., branches or tags, 可以通过删除你不再需要的refs来解决)和跟踪的文件数量(以及它们的重量), 即使重文件问题可以解决使用 Git LFS).

    Note: 如今,Facebook试图用VCS的可扩展性来解决问题 通过修补水星 而且,可能很快,这将不是一个大问题.

  • Higher build time. 因为您将在一个地方拥有大量源代码, 为了批准每一个PR,你的CI将花费更多的时间来运行所有的事情.

Tool Review

管理单一资产的工具集在不断增长, and currently, 我们很容易迷失在各种各样的建筑系统中. 您可以随时了解流行的解决方案 this repo. 但是现在,让我们快速浏览一下现在JavaScript中大量使用的工具:

  • Bazel b谷歌的构建系统是面向单点的吗. More on Bazel: awesome-bazel
  • Yarn 是一个JavaScript依赖管理工具,通过工作区支持单线程.
  • Lerna 是一个工具,用于管理JavaScript项目与多个包,建立在Yarn.

大多数工具使用非常相似的方法,但也有一些细微差别.

单仓库git的CI/CD过程的说明

由于这是一个相当大的主题,我们将在本文的第2部分中更深入地研究Lerna工作流以及其他工具. 现在,让我们来大致了解一下里面是什么:

Lerna

这个工具在处理语义版本时确实很有帮助, 建立构建工作流, 推销你的包裹, etc. Lerna背后的主要思想是你的项目有一个packages文件夹, 哪个包含所有独立的代码部分. 除了包之外,你还有一个主应用程序,例如它可以位于src文件夹中. Lerna中几乎所有的操作都遵循一个简单的规则——遍历所有的包, 然后对它们做一些动作, e.g.,增加包版本,更新所有包的依赖,构建所有包等.

对于Lerna,你有两种选择:

  1. 不把它们推到远程(NPM)
  2. 将您的包推送到远程

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

但是,如果使用第二种方法,则必须从远程导入包. (e.g., 从@yourcompanyname/packagename导入{东西};),这意味着您将始终获得软件包的远程版本. 促进地方发展, 您必须在文件夹的根目录中创建符号链接,以使打包器解析本地包,而不是使用您的 node_modules/. 这就是为什么在启动Webpack或您最喜欢的捆绑器之前,您必须启动 lerna bootstrap,它将自动链接所有包.

说明在单个节点包中设置模块的名称空间

Yarn

Yarn最初是NPM包的依赖管理器, 哪一个最初不是为了支持单点pos而构建的. But in version 1.0, Yarn developers 发布了一个叫做 Workspaces. 在发布时,它不是那么稳定,但过了一段时间,它就可以用于生产项目了.

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

乍一看,Yarn和它的工作区提供了以下有用的特性:

  1. Single node_modules 文件夹中的所有包. 例如,如果你有 packages/package_a and packages/package_b—with their own package.json-所有依赖项将只安装在根目录下. 这是Yarn和Lerna工作方式的区别之一.
  2. 依赖符号链接允许本地包开发.
  3. 单个锁文件用于所有依赖项.
  4. 集中的依赖更新,以防您只想重新安装一个包的依赖. 可以使用 -focus flag.
  5. 与勒纳的整合. 你可以很容易地让Yarn处理所有的安装/符号链接,让Lerna负责发布和版本控制. 这是迄今为止最流行的设置,因为它需要较少的工作并且易于使用.

Useful links:

Bazel

Bazel是一个用于大规模应用程序的构建工具, 它可以处理多语言依赖关系,并支持许多现代语言(Java, JS, Go, C++, etc.). In most cases, 在中小型JS应用程序中使用Bazel是多余的, 但是规模很大, 由于它的性能,它可能会提供很多好处.

By its nature, Bazel看起来和Make很像, Gradle, Maven, 以及其他允许基于包含构建规则和项目依赖项描述的文件进行项目构建的工具. 调用Bazel中的相同文件 BUILD 位于Bazel项目的工作空间内. The BUILD file uses its Starlark一种人类可读的高级构建语言,看起来很像Python.

通常情况下,你不会遇到很多 BUILD 因为有很多样板可以很容易地在网上找到,并且已经配置好并准备好进行开发. 每当你想要构建你的项目时,Bazel基本上会做以下事情:

  1. Loads the BUILD 与目标相关的文件.
  2. Analyzes 输入和它们的 dependencies,应用指定的生成规则,并生成一个 action graph.
  3. Executes 对输入执行构建操作,直到生成最终的构建输出.

Useful links:

Conclusion

独角兽只是一个工具. 关于它是否有未来,有很多争论, 但事实是,在某些情况下, 这个工具完成它的工作,并以一种有效的方式处理它. 在过去的几年里, 这个工具已经进化了, 获得了更多的灵活性, 克服很多问题, 并且在配置方面去掉了一个复杂层.

还有很多问题需要解决, 比如糟糕的Git性能, but hopefully, 这将在不久的将来得到解决.

如果你想学习为你的应用构建一个强大的CI/CD管道,我建议 如何建立一个有效的初始部署管道与GitLab CI.

关于总博客的进一步阅读:

了解基本知识

  • 单体架构是什么意思?

    软件开发中的单块架构意味着一种方法,在这种方法中,您将应用程序作为一组紧密耦合的组件/功能组合成一个整体. 你不能使用应用范围之外的任何组件.

  • 什么是源代码存储库?

    存储库是存储和检索源代码以安装/开发源代码的地方. Repos存储具有相关元数据的所有版本的数据, 哪一个允许历史修订或在同一项目的独立并行版本上工作.

  • 为什么模块化在编程中很重要?

    松耦合代码已被证明是更可靠的,因为它允许我们引入更改而不必担心在其他地方破坏东西. 这使得开发更安全,并简化了重构.

  • What are symlinks?

    符号链接(符号链接)是以相对或绝对路径的形式引用原始文件夹/文件的文件,而不是实际内容. 在这种情况下,它们被用来影响路径名解析到正确的位置.

  • 在JS中制作模块最常见的方法是什么?

    现在最流行的将代码隔离到单独模块的方法是使用npm包(可以通过Yarn或npm完成)。. 它们与自己的配置文件一起打包到一个文件夹中,然后推送到远程服务器.

就这一主题咨询作者或专家.
Schedule a call
亚历山大·诺埃尔的头像
Alexander Noel

Located in Valencia, Spain

Member since November 2, 2016

About the author

Alexander是一位经验丰富的前端开发人员,曾开发过电子商务和企业网站以及web和移动应用程序.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Years of Experience

8

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.