从零搭建Monorepo项目与引入Nx加快项目流程

733 阅读13分钟

1. Monorepo是什么

Monorepo是一种软件开发的策略模式,它代表"单一代码仓库"(Monolithic Repository)。在 monorepo模式中,所有相关的项目和组件都被存储在一个统一的代码仓库中,而不是分散在多个独立的代码仓库中。

2. Monorepo演进

阶段一:单仓库巨石应用

一个 Git 仓库维护着项目代码,随着迭代业务复杂度的提升,项目代码会变得越来越多,越来越复杂,大量代码构建效率也会降低,最终导致了单体巨石应用,这种代码管理方式称之为 Monolith。

阶段二:多仓库多模块应用

于是将项目拆解成多个业务模块,并在多个 Git 仓库管理,模块解耦,降低了巨石应用的复杂度,每个模块都可以独立编码、测试、发版,代码管理变得简化,构建效率也得以提升,这种代码管理方式称之为 MultiRepo。

阶段三:单仓库多模块应用

随着业务复杂度的提升,模块仓库越来越多,MultiRepo这种方式虽然从业务上解耦了,但增加了项目工程管理的难度,随着模块仓库达到一定数量级,会有几个问题:

  1. 跨仓库协作不便:
    • 开发者在进行跨项目的工作时,可能需要在多个仓库中切换,这可能会降低工作效率。需要协调不同的代码库和分支策略。
  2. 分散在单仓库的模块依赖管理复杂:
    • 底层模块升级后,其他上层依赖需要及时更新,否则有问题。
  3. 协调和版本控制困难:
    • 当需要协调多个仓库的变更时,可能会变得复杂且容易出错。版本之间的协调和发布管理可能会变得更加棘手。
  4. 构建和测试复杂:
    • 为每个仓库配置和维护独立的构建和测试流程。整体构建和测试过程可能会变得复杂且难以统一。
  5. 访问和权限控制复杂:
    • 在多仓库环境中,管理不同仓库的访问权限可能变得更加复杂。需要确保正确的权限设置,以保护代码的安全性。
  6. 重复工作:
    • 在不同的仓库中,可能会出现重复的配置文件或代码,导致维护成本增加。每个仓库可能需要单独配置和更新工具链和开发环境。

于是将多个项目集成到一个仓库下,共享工程配置,同时又快捷地共享模块代码,成为趋势,这种代码管理方式称之为 MonoRepo。

3. Monorepo优缺点

3.1 优点

  1. 简化依赖管理
    • 所有相关代码都在一个仓库中,管理和更新依赖变得更为直接和一致。这可以减少不同项目之间依赖版本的不一致性。
  2. 统一版本管理
    • 版本控制系统能够同步管理所有项目的版本。这使得在进行版本发布时,跨项目的一致性和协调变得更加简便。
  3. 跨项目协作
    • 团队成员可以在同一代码库中进行跨项目的更改,使得跨团队协作更加顺畅。变更的追踪和代码审查也变得更加集中和一致。
  4. 一致的构建和测试
    • 可以统一配置构建和测试工具,确保所有项目都遵循相同的标准和流程。这有助于保持代码质量的一致性。
  5. 简化代码共享
    • 内部库和工具可以被多个项目共享,无需在多个仓库中复制或同步代码。这有助于减少重复代码和简化维护工作。
  6. 更好的代码可见性
    • 所有代码都集中在一个地方,团队可以更容易地了解和查找项目中的所有部分,促进知识共享和代码复用。

3.2 缺点

  1. 仓库规模大
    • 随着项目和代码的增加,单一仓库可能变得非常庞大,导致操作变慢,尤其是对版本控制系统性能的要求更高。
  2. 管理复杂性
    • 大型代码库需要更多的管理工作,例如权限控制、代码审查、分支策略等。随着仓库的增长,这些管理任务可能变得更加复杂。
  3. 工具和性能限制
    • 一些版本控制工具在处理非常大的仓库时可能表现不佳,导致性能问题,例如慢速操作或高资源消耗。
  4. 构建和测试时间
    • 整个代码库的构建和测试可能会需要更长的时间,因为所有项目和模块都在一个仓库中,这可能导致长时间的构建和测试周期。
  5. 团队协作的挑战
    • 如果团队人数较多,或者多个团队在同一仓库中工作,可能会遇到更多的合并冲突和协调问题。需要有效的分支管理和合并策略来应对这些挑战。
  6. 更高的学习曲线
    • 新成员可能需要花更多时间熟悉整个仓库和其中的所有项目,特别是在仓库结构复杂的情况下。

总体来说,Monorepo 是一种适合某些组织和项目的管理策略,特别是那些需要强大的一致性和协作支持的情况。然而,是否采用 Monorepo 策略应根据具体的项目需求、团队规模和工具能力来决定。

4. Monorepo应用场景

  1. 多个互相关联的项目
    • 项目之间有紧密的依赖关系,需要同步更新。例如,前端和后端代码、共享库和工具。
  2. 统一的构建和测试流程
    • 需要一个统一的构建和测试系统来确保所有项目的一致性和质量。例如,包含多个模块的开源框架。
  3. 跨团队协作
    • 多个团队需要在同一代码库中进行协作和共享代码。便于跨团队协调和集成工作。
  4. 频繁的跨项目变更
    • 需要频繁地对多个项目进行变更或发布。例如,软件产品的多个组件和服务需要同步更新和发布。
  5. 代码共享和复用
    • 需要在不同项目之间共享代码和工具,避免重复开发和维护。
  6. 简化依赖管理
    • 需要集中管理依赖关系,减少不同项目间的版本冲突。

选择 Monorepo 时,确保团队和工具能够处理大规模代码库和管理复杂性,以充分发挥其优势。

5. 项目搭建

5.1 安装 pnpm

pnpm 是一个非常轻量的 monorepo 管理工具,自身支持workspaces。
npm install -g pnpm

在项目根目录下执行以下命令,初始化 monorepo 项目:

pnpm init

5.2 声明工作空间

创建[pnpm-workspace.yaml](about:blank),声明工作空间.
packages:
  - 'packages/*'

pnpm 内置了对 monorepo的支持workspace,workspace的概念是实现 monorepo的一种手段。Workspace的本质是建立起本地文件和node_modules依赖之间的链接,从而实现不同的package之间的引用。

5.3 初始化子项目

这里采用vite脚手架构建子项目,具体过程跟着[官方文档](about:blank)走就行,创建四个项目,TS+Vue,过程如下:

终端输入:

pnpm create vite

最终目录结构如下:

其中vite-project为主应用,app1和app2为辅应用,common为工具包,components为公共组件库, 可能的依赖关系为:主应用vite-project会依赖app1、app2、common以及components;app1和app2则会依赖于common以及components。

修改monorepo目录下的packages.json为:

{
  "name": "monorepo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
   "dev": " cd packages && cd vite-project && pnpm run dev",
   "build": "vue-tsc -b && vite build",
   "preview": "vite preview"
 },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

5.4 workspace配置

如果当前包有依赖于当前仓库下的其它包的话,可以做如下配置:
{
  "name": "vite-project",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.4.37",
    "common": "workspace:*" // 依赖工作区的包
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.1.2",
    "typescript": "^5.5.3",
    "vite": "^5.4.1",
    "vue-tsc": "^2.0.29"
  }
}

5.5 启动项目

在项目目录下输入pnpm dev即可,则自动会启动主应用vite-project

6. 引入NX

6.1 NX是什么

Nx官网是这样介绍的:Nx 是一个功能强大的开源构建系统,它提供用于提高开发人员工作效率、优化 CI 性能和保持代码质量的工具和技术。

6.2 Why Nx

官网:我们创建 Nx 是因为开发人员难以配置、维护,尤其是集成各种工具和框架。设置一个既适合少数开发人员又能轻松扩展到整个组织的系统是很困难的。这包括设置低级构建工具、配置快速 CI 以及保持代码库健康、最新和可维护。

可见,,像Nx这种构建系统(TurborepoRush)非常适合管理monorepo这样的大型项目。

6.3 核心功能

6.3.1 高效运行任务
Nx 运行任务使用以下语法:

看一下NX任务运行特点

  • 轻松并行运行多个项目的多个目标
  • 定义任务管道以正确的顺序运行任务
  • 仅为受给定更改影响的项目运行任务
  • 通过缓存加快任务执行速度

6.3.1.1 并行运行多个项目的多个目标

1、为多个项目运行单个任务

这里为多个项目运行构建任务

npx nx run-many -t build

为多个项目运行build、lint和test任务

npx nx run-many -t build lint test

为指定项目运行任务,对header和footer项目运行build、lint和test任务

npx nx run-many -t build lint test -p header footer

指定线程数并行运行任务

 nx run-many -t test -p proj1 proj2 --parallel=5

--parallel=5代表开启五个线程执行任务

2、自定义任务管道

任务管道可以简单理解为项目的依赖关系,比如项目的主应用依赖于其它应用,那么在构建的时候,就需要根据依赖关系,从后往前一次构建。

使用nx graph命令可以可视化项目依赖关系

这是Nx提供的可视化系统,当前显示的是所有项目,可以发现主应用vite-project依赖于commom,这跟我们配置是一致的。

同时Nx也允许定义任务依赖

{
  ...
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build", "prebuild"]
    },
    "test": {
      "dependsOn": ["build"]
    }
  }
}

当运行test时,发现会依赖于build,此时会先去执行build任务,当执行build任务时,build又依赖于prebuild以及所有依赖的构建,这时就会优先去执行prebuild任务,在执行所有依赖的的构建

6.3.2 在 CI 中分配任务

随着工作区的增长,重新测试、重新构建和重新检查所有项目变得太慢了。为了解决这个问题,Nx 附带了一个 “affected” 命令。使用此命令,Nx

  • 确定受更改影响的最小项目
  • 仅在受影响的项目上运行任务

这大大提高了 CI 的速度,并减少了所需的计算量。

6.3.2.1 确定受更改影响的最小项目集

image.png

6.3.2.2 仅在受影响的项目上运行任务
执行命令
nx affected -t <task>

当运行任务时,Nx 将:

  • 使用 Git 确定您在 PR 中更改的文件。
  • 使用项目图表确定文件属于哪些项目。
  • 确定哪些项目依赖于您修改的项目。

确定项目后,Nx 将运行您在该项目子集上指定的任务。这里就只会运行上图中高亮的项目,大大加快ci的构建。之前项目中一个MR的CI快半小时,目前差不多十多分钟,跑CI流水线的时间有很大缩减。

以下命令可以可视化受影响的项目

nx graph --affected
6.3.3 本地和远程缓存

通过本地远程缓存,Nx可以防止不必要的任务重复运行,加快任务执行速度。

6.3.3.1 本地缓存
重复重新构建和重新测试相同的代码成本很高。Nx 提供了一个复杂且经过实战检验的计算缓存系统,可确保**代码永远不会重新构建两次**。Nx **会缓存终端输出和**通过运行任务生成的文件(例如,您的 build 或 dist 目录)。

缓存原理:

缓存的原理实际上就是看该项目是否做了更改,其包括所有的源文件以及依赖项、外部依赖项的版本、包括Node版本,Nx会通过这些计算出一个hash,如果计算出的hash被命中,则复用缓存的文件。

6.3.3.2 远程缓存

image.png

远程缓存就是会在云端缓存CI的运行结果,并在个开发之间共享缓存结果。比如build任务,一旦其他组员有跑过一次CI,并开启了云端缓存,那么其他组员的build如果命中缓存,则不会重复构建。

6.4 项目引入Nx
6.4.1 安装Nx
pnpm install nx -g
6.4.2 初始化Nx
npx nx@latest init
6.4.3 Nx.json配置文件介绍
{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "npmScope": "myorg",
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "lint", "test"],
        "cacheDirectory": ".nx/cache"
      }
    }
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"]
    }
  },
  "workspaceLayout": {
    "appsDir": "apps",
    "libsDir": "libs"
  },
  "defaultBase": "main"
}

nx.json 文件会在项目的根目录中生成,用于配置 Nx 的全局设置。这个文件定义了 monorepo 的一些通用配置,包括项目的默认任务、缓存策略、工作流和生成器等。

  1. **$schema**
  • 描述:此字段定义了 nx.json 文件所使用的 JSON 架构,帮助提供代码补全和验证规则。
  • 默认值"./node_modules/nx/schemas/nx-schema.json"
  • 作用:确保 nx.json 文件的结构和内容符合 Nx 规范。
  1. **npmScope**
  • 描述:定义项目的命名空间,在 Nx 中用于标识包的范围。
  • 默认值myorg
  • 作用:这个值会作为工作区中所有包的前缀。例如,在 npmScopemyorg 的工作区中,包的名称可能是 @myorg/app1
  1. **tasksRunnerOptions**
  • 描述:定义任务运行器的配置选项。任务运行器负责管理 Nx 中的任务(如 buildlinttest 等),options将指定可缓存的任务和缓存目录
  • 默认配置
{
  "default": {
    "runner": "nx/tasks-runners/default",
    "options": {
      "cacheableOperations": ["build", "lint", "test"],
      "cacheDirectory": ".nx/cache"
    }
  }
}

  1. **targetDefaults**
  • 描述:用于为所有项目的特定目标任务(例如 buildlinttest 等)定义默认行为。
  • 默认配置
json


复制代码
{
  "build": {
    "dependsOn": ["^build"]
  }
}
  • 解释
    • build: 这是 build 目标的默认配置。
    • dependsOn: 这个设置表示在当前项目执行 build 时,它将依赖于其他项目的 build 任务。"^build" 表示依赖于该项目的其它项目(例如依赖关系中的库)的 build 任务。
  1. **workspaceLayout**
  • 描述**:定义工作区中的目录结构,决定应用和库的存放路径。**
  • 默认配置**:**
{
  "appsDir": "apps",
  "libsDir": "libs"
}
  • 解释**:**
    • **appsDir**: 定义存放应用的默认目录,默认值为 **apps**
    • **libsDir**: 定义存放库的默认目录,默认值为 **libs**

作用:这有助于 Nx 了解你的工作区项目目录结构,并正确组织应用和库。如果你有自定义的文件结构,可以调整这两个字段。

  1. **defaultBase**
  • 描述:指定工作区的默认分支名。
  • 默认值main
  • 作用:Nx 会使用这个字段来确定默认的 Git 分支,用于工作流中的基准分支。在 nx affected 等命令中,Nx 会以这个分支为基准,来确定哪些项目受到代码变动的影响。
6.5 build体验
清除缓存
nx reset

执行build命令

pnpm build

build时间在4s

再次进行build

可以发现,再次build,直接命中了任务缓存,build时间在66ms

参考文献

NPM Workspaces Tutorial | Nx

带你了解更全面的 Monorepo - 优劣、踩坑、选型带你了解更全面的 Monorepo,本文涵盖了 Monoreo - 掘金 (juejin.cn)