Monorepo在国际的实践与总结

2,970 阅读20分钟

随着软件开发项目的复杂性和规模的不断增长,越来越多的公司开始采用Monorepo(单一代码仓库)的开发方式。Monorepo作为一种管理代码的方式,通过将所有相关的代码存储在一个单一的版本控制库中,为团队提供了许多优势和便利。本文将探讨Monorepo在国际前端的落地与实践,并分享Monorepo实践中的关键要素和成功经验。

一、背景

由于国际这边的项目有这样一个特点,项目繁多、资源集中、系统阶段分层、业务逻辑递进,很多项目他们是有依赖关系的,每个系统承接不同的用户群体,业务阶段明确、上下游相对紧凑。因此很多系统他们之间在同一个业务流程中存在很多共性。

从业务流程上,其实它们数据是同宗同源,只是系统不一样,针对的用户群体不一样,数据流转和操作的阶段不一样。当然,由于人力资源的关系,它们往往并不是在同一迭代完成所有的开发,跑通所有流程,也许是在未来的某个迭代需要跑通后续的流程。

从研发流程上,其实他们往往是界面UI一样、展示的数据不一样。通常在PRD中最简单直接的体现是,请参考XX系统的XX页面。对于研发同学来说,它们往往需要付出双倍的人力在不同的系统开发相同的页面,而这些往往都是相同类似的。

因此,一开始我们是打算开发一个国际公共中后台的子项目,将所有跨系统的页面都放在这个子应用系统中。后来,在老大的建议下,可以朝着monorepo方向研究一下,于是就有了应用级别的monorepo在国际的落地与实践。

image.png

二、Monorepo介绍

  1. 简介

Monorepo(或称为单一代码库、单一仓库)是指将一个项目的所有代码、模块、组件等相关资源存储在一个单一的版本控制仓库中的软件开发管理方法。它与传统的多个分散的代码库相对。

在传统的多库项目中,不同的模块或组件可能被拆分为独立的代码库,每个库都有自己的版本控制和发布过程。而在Monorepo中,所有模块和组件都存储在同一个代码库中,可以统一进行版本控制、构建、测试和部署等操作。

Monorepo模式可以包括以下几个关键特点:

  1. 集中式代码库: 所有项目代码和相关资源都集中存储在一个单一的代码库中,而不是分散在多个独立的代码库中。

  2. 共享依赖: 不同的项目、模块或组件可以共享依赖库,避免重复的依赖安装和维护,提高代码复用和开发效率。

  3. 一致的 版本控制 所有代码在同一个版本控制系统中管理,便于跟踪变更、版本控制和回滚。

  4. 统一的构建和测试: 通过Monorepo可以统一管理构建和测试过程,确保不同模块或组件之间的兼容性和一致性。

  5. 协作和共享: Monorepo促进了团队成员之间的协作和交流,提高代码共享、知识传递和团队合作能力。

Monorepo的使用有助于简化项目管理和维护过程,减少不必要的复杂性,提高开发效率和代码质量。然而,适用于Monorepo的项目类型和规模可能会有所不同,需要根据具体项目的需求和特点来决定是否采用Monorepo开发管理方法。

  1. 例子

Monorepo在许多知名的公司和开源项目中被广泛使用。以下是一些使用Monorepo的知名公司的例子:

  1. Google:Google是Monorepo的早期倡导者之一,他们使用一个巨大的Monorepo来管理几乎所有的代码。他们的Monorepo包含了数百个项目,涵盖了各种产品和服务。
  2. Facebook:Facebook也使用Monorepo作为他们的代码管理策略。他们的Monorepo中包含了各种前端和后端项目。
  3. Twitter:Twitter使用Monorepo来管理他们的代码库。他们将所有的代码存储在一个仓库中,包括前端、后端、工具和库等。
  4. Airbnb:Airbnb也采用Monorepo的方式来管理他们的代码。他们的Monorepo中包含了多个项目和服务,包括前端、后端和基础设施。
  5. Microsoft:Microsoft在一些项目中采用了Monorepo的模式。例如,TypeScript、Visual Studio Code和Office 365等项目都使用了Monorepo来管理代码。

这些公司的使用案例证明了Monorepo在大型团队和复杂项目中的实用性和有效性。它们通过Monorepo实现了代码的共享和重用,加强了团队协作和沟通,简化了构建和部署流程,并提高了整体项目的质量和交付速度。这些成功的实践经验进一步推动了Monorepo在开发社区中的流行和应用。

下面我们从实际流程中看下单仓模式与多仓模式的区别。

  1. 单仓vs多仓

从研发流程上,我们可以对比一下单仓模式和多仓模式

image.png

从上面我们可以看出,不同的模式各有优缺点,monorepo优缺点概括如下:

三、落地与实践

  1. 技术调研

当时调研了其他公司的方案,目前最流行的两种方案是lerna + yarn workspace方案和pnpm workspace方案,并且做了分别做了简单的demo链接地址(需要切换不同分支),我们这里采用的pnpm workspace方案

  1. 技术架构

  1. 目录结构

  • apps/ 目录存放各个应用程序的子目录。

    • abroad-crm-micro/ 是第一个应用程序目录。
    • cborder-crm-micro/ 是第二个应用程序的目录。
    • ...
  • packages/ 目录存放多个 Turbo 应用程序共享的代码、组件、工具函数等。

      • hooks: hook相关
    • - pages: 公共页面相关
      • service: 接口相关
      • tracks: 埋点相关
      • types: 全局类型和枚举
      • ui: 组件UI相关
      • utils: 公共函数
  • .gitignore 是 Git 版本控制系统的忽略文件配置。

  • package.json 是整个大仓的依赖和配置文件。

  • tsconfig.json TS配置文件。

  • turbo.json turbo配置文件。

  • pnpm-workspace.yaml pnpm workspace配置文件。

  • README.md 是项目的说明文档。

  1. 关键技术

  1. pnpm的workspace,它的作用是实现整个大仓的依赖包管理,是实现monorepo最核心的配置。它的配置也相当简单,如下:
// pnpm-workspace.yaml
packages:
  - "packages/*"
  - "apps/*"

这种写法在小团队的仓库写法是没啥问题的,但其实并不是最优的写法,尤其是要建立大仓的话。最优的写法是精确到具体的项目,具体的包,查找依赖关系的速度会更快。

  1. turbo构建工具,它并不是monorepo的必要工具,只是会让你的打包构建速度更快而已。它的官方原话是:Turborepo 是一个用于 JavaScript 和 TypeScript 代码库的高性能构建系统。

    1.   本质上,它是利用缓存和高并发机制,使你的打包构建速度更快,它拥有以下优点:
    2. 简化部署流程:Turbo-Repo 提供了统一的部署配置和工具,简化了构建、测试和部署的流程。开发人员可以通过一致的方式进行部署,减少了手动操作和人为错误的风险。
    3. 提高开发效率:通过使用 Monorepo 和 Turbo-Repo,不同模块和服务可以共享依赖、代码和资源,减少了重复劳动和开发时间。同时,一致的构建和部署流程可以提高团队的开发效率。
    4. 提升代码质量和一致性:通过自动化测试和部署流程,可以确保代码的质量和一致性。所有的代码变更都经过自动化测试,并按照相同的流程进行构建和部署,减少了潜在的问题和错误。

当然,它并不是完美的,也有副作用,后文会提到。

  1. Changesets工具,一款用于 Monorepo 项目下版本以及 Changelog 文件管理的工具。它具备以下优点:

    1. 版本一致性:Changesets 可以帮助确保 Monorepo 项目中的版本一致性。它使用统一的方式来管理项目中的版本和变更,确保各个子项目之间的版本关系保持一致。

    2. 变更管理:Changesets 提供了一种结构化的方式来记录和管理项目中的变更。通过定义变更集(changesets),开发人员可以清晰地描述每个变更的类型、影响范围和目的,从而更好地理解和跟踪项目的变更历史。

    3. 自动化版本控制:Changesets 可以与持续集成(CI)工具集成,实现自动化的版本控制和发布流程。当代码合并到主分支时,Changesets 可以自动为项目生成新的版本,并更新子项目之间的依赖关系。

    4. 灵活性和可定制性:Changesets 可以根据项目的需求进行定制和扩展。开发人员可以定义自己的变更类型、规则和版本策略,以适应不同项目的特定需求和工作流程。

  2. 规范

大仓的建立代码会越来越多,为了降低维护成本,我们需要建立一套科学合理的代码管理和分支管理规范。

  1. 分支规范

问题
  • monorepo多个应用都关联一个git地址库,创建的分支都是共用的,当多个应用共用同一个分支的时候,在发布时会出现很多问题,如下:

    • 分支重建影响大-当某个应用出现需求不上线时,需要重建release分支时,其他应用已经mr过的,需要重新创建mr;
    • 分支找不到-同一个release,在其他应用绑定了分支a,在当前应用是无法绑定分支a的;
    • 分支管理混乱-当项目越来越大,分支记录追踪难度大,分支合并进来就会越来越多,共用一个release的话,分支记录追踪会比较困难;

原则:可以将不同的项目或模块分别放在不同的分支中,开发完成后再将分支合并到主分支中

目标
  • 各应用分支要独立,不会受彼此影响
  • 可溯源追踪分支记录,更快定位
方案

以517的发布分支为例,比如要创建【release-5.17.5】的分支,我们各自应用就要变成【release-5.17x.5】,x代表项目编号。

应用编号创建分支例子
app11release-5.17.5 -> release-5.171.5
app22release-5.17.5 -> release-5.172.5
app33release-5.17.5 -> release-5.173.5
app44release-5.17.5 -> release-5.174.5
app55release-5.17.5 -> release-5.175.5
app66release-5.17.5 -> release-5.176.5

更多monorepo分支管理-分支规范

  1. 代码规范

1、遵循原则:安全性、可靠性、一致性和可维护性

2、国际团队以@global开头命名,不同的团队应该用不同的命名区分

A. 创建
  1. 复制 - 复制@global/date文件夹,粘贴到packages/目录下,与date平级,并修改文件名(如ui),同时修改package.json中的name(所有依赖包name必须是【@global/xxx】),这里的name为【@global/ui】
B. 共享

暂时不需要发布,跨项目共享

  1. 引入,比如在项目poizon-deal-pc中引入@global/ui
   // poizon-deal-pc/package.json
  "dependencies": {
      ...
      "@global/ui": "workspace:*",
      ...
  }

2. 安装,执行命令pnpm i

  1. 使用,在对应的业务代码中直接使用import xx from '@global/ui'
C. 遵循原则
  1. 不要有项目相关的变量

  2. 可共享任何代码片段,包括但不限于函数、页面、模块等

  3. 公共模块添加需要规范化,如下

    1. 1)头部必须以/***/方式注释符
    2. 2)函数/模块名字必须填写
    3. 3)参数param需要补充完整
/**
 * 替换字符串字符
 * @param {*} str 字符串
 * @param {*} reg 替换的正则
 * @param {*} rep 替换的字符
 * @returns
 */
export function replaceStr(str: any, reg: RegExp, rep: any) {
  return [undefined, null].includes(str) ? str : str.toString().replace(reg, rep);
}

3. #### TS规范

建立TS规范是为了保持各个应用的ts规则保持统一,同时不会引起冲突,这里以poizon-deal项目为例

  1. 结构

  1. 继承
// tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "noImplicitAny": false,
    "suppressImplicitAnyIndexErrors":false,
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

上面是根目录下的公共ts配置文件,需要建立子应用的ts配置文件

// apps/poizon-deal-h5/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"],
      "@global": ["../../packages/*"],
      "@module/*": ["../../packages/module/*"],
      "@tracks/*": ["../../packages/tracks/*"],
      "antd-mobile": ["node_modules/antd-mobile/bundle/antd-mobile.es.js"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

当然,代码治理和分支管理涉及的地方还有很多很多,由于篇幅关系这里只是简单介绍几个关键点。

  1. 团队协作沟通

制定完规范后,团队的沟通将变得更加重要,尤其是前期不成熟,比较弱化的规范,很大程度上决定最终规范的执行与否。我们需要建立的团队沟通渠道:

  1. 清晰的沟通渠道:建立明确的沟通群,使团队成员可以及时交流和分享信息。
  2. 规范的命名和文档:为共享代码库中的项目、模块和功能使用规范的命名约定,以便团队成员能够快速理解和识别代码。编写清晰的文档,包括项目结构、依赖关系、构建和部署流程等,以便新成员可以迅速上手并了解项目的工作方式。
  3. 代码审查:实施代码审查流程,确保团队成员之间的代码质量和最佳实践的遵循。通过代码审查,团队成员可以相互学习、提供反馈和建议,从而提高代码的质量和可维护性。
  4. 统一的工作流程:确保团队成员遵循统一的工作流程和开发规范。这包括代码提交流程、分支管理策略、版本发布流程等。通过统一的工作流程,可以降低沟通和冲突的成本,并确保项目的稳定性和可维护性。
  5. 定期会议和跨团队同步:定期组织会议和同步,让团队成员可以分享进展、解决问题和协调工作。跨团队同步可以帮助不同团队之间的协作和对接,确保项目的整体一致性和进展。
  6. 自动化和持续集成:使用自动化工具和持续集成(CI)流程来简化和加速开发过程。自动化构建、测试和部署可以减少手动操作和错误,提高团队的效率和可靠性。
  7. 开放的反馈和改进机制:鼓励团队成员提供反馈和改进建议,并确保这些反馈能够得到认真对待和及时响应。持续改进团队的工作流程和协作方式,以适应不断变化的需求和挑战。

目前还多事项都在持续的完善中,有很多的工具也正在开发。

  1. 依赖管理

在Monorepo中管理依赖关系可能变得复杂。不同项目或模块可能依赖于不同的版本或不同的依赖项。解决依赖冲突和确保整个代码库的依赖关系一致性可能需要额外的努力和工具支持,目前是采用本地pnpm workspace软连进行管理。

目前对关键的依赖包进行锁死,只能解决直接依赖这一层的问题,但无法处理依赖包内部的第三方依赖升级问题。这个问题也是有影响的,最直接的体现是经常出现pnpm-lock的无缘无故升级。

  1. 构建

我们这里采用turbo工具进行构建,以顶层的package.json中作为入口

// package.json
"scripts": {
    "dev": "turbo run dev --parallel",
    "dev:c": "pnpm start:cborder-crm",
    "dev:cborder-crm": "pnpm start:cborder-crm",
    "start:cborder-crm": "turbo run dev --filter cborder-crm-micro",
    "install:cborder-crm": "turbo run re-install --filter cborder-crm-micro",
    "build:dev:cborder-crm": "turbo run build:dev --filter cborder-crm-micro",
    "build:test:cborder-crm": "turbo run build:test --filter cborder-crm-micro",
    "build:pre:cborder-crm": "turbo run build:pre --filter cborder-crm-micro",
    "build:prod:cborder-crm": "turbo run build:prod --filter cborder-crm-micro",
}

同时turbo.json需要做一些简单的配置,具体可参考官方turbo.build/repo/docs

pnpm run build:test:cborder-crm命令说明:

turbo run build:test的意思是告诉turbo执行什么命令

--filter cborder-crm-micro它会进入apps/cborder-crm-micro的目录

因此,完整命令的含义是进入apps/cborder-crm-micro的目录,并运行run build:test指令

但是如果在服务器上单单执行这条指令是构建不成功的。要了解其原因,我们得先了解pnpm workspace构建原理。

pnpm workspace原理解析

PNPM Workspace 是 PNPM 包管理器的一个特性,用于管理 Monorepo 项目中的多个子项目。它的原理主要涉及两个方面:共享依赖和符号链接。

  1. 共享依赖:

    1. PNPM Workspace 允许在 Monorepo 项目中共享依赖。它使用一个根级的 node_modules 目录,存储所有子项目的依赖包。
    2. 当在一个子项目中安装依赖时,PNPM 会将依赖包下载到根级 node_modules 目录下,并使用符号链接将依赖链接到各个子项目的 node_modules 目录中。
    3. 这种方式可以节省磁盘空间,因为依赖只需要在根级 node_modules 目录中保存一份,多个子项目可以共享使用。
  2. 符号链接:

    1. PNPM Workspace 使用符号链接将依赖包链接到各个子项目的 node_modules 目录中。
    2. 当一个子项目引用共享的依赖时,实际上它引用的是根级 node_modules 目录中的符号链接。
    3. 这样做的好处是,子项目可以像使用常规的本地依赖包一样使用共享的依赖,而不必在每个子项目中独立安装和管理这些依赖。

PNPM Workspace 的工作原理简化了 Monorepo 项目的依赖管理和构建过程。它通过共享依赖和符号链接的方式,减少了冗余的依赖下载和磁盘空间占用。同时,它提供了一致的依赖版本和结构,使得子项目之间的依赖关系更加清晰和可控。

需要注意的是,使用 PNPM Workspace 需要使用 PNPM 包管理器,并且在项目的根目录中配置相应的 workspace 配置文件(如 pnpm-workspace.yaml)。这样,PNPM 就能够识别和管理项目中的子项目及其依赖关系。

思考:使用pnpm workspace之后,pnpm i还是原来的pnpm i指令吗?

所以,如果在内部执行pnpm build:test:cborder-crm命令,在服务器上是构建不成功的,原因是它依赖了外部的其他依赖的话,依赖关系没建立起来,会缺少依赖,最终构建失败。

构建的软连如下:

请思考,这种构建方式有什么缺点?

  1. 部署

采用monorepo之后,部署变化了吗?其实并没有太大的变化,部署还是按照原来的部署方式,每个应用应该保持独立且分离。下面我们以cborder-crm-micro为例,我们需要做的仅仅是修改下构建的指令,其余的流程与原来保持一致。

  1. 更改git仓库地址

  1. 更改构建指令

四、总结和展望

国际这边从今年一月份开始,因为中间有其他的业务,断断续续地开发,目前已经落地了两个应用级别的monorepo大仓,分别是国际中后台global-monorepo和全球化独立站poizon-deal。总体来说,实践效果还是不错,尤其在代码复用这一块。

使用 Monorepo 可以提高开发效率、代码共享和重用性,简化依赖管理和构建流程,并促进团队协作和沟通。通过统一的代码库,团队可以更好地跟踪和管理项目间的关联性,减少维护成本,并推动整体代码质量的提升。

但也会遇到很多的挑战:

  1. 系统稳定性:由于系统依赖关系变得复杂,修改公共模块会直接影响其他系统的稳定性,因此需要建立更加灵活的公共依赖关系管理工具以及自动化测试系统。

  2. 构建和部署时间:由于Monorepo中包含多个项目或模块,每次构建和部署都需要处理整个代码库。这可能导致构建和部署时间较长,特别是当代码库变得非常庞大时。需要使用优化技术,如增量构建和部署,以减少构建和部署时间;各个应用查找依赖关系,能够独立打包构建,这也是目前正在做的事情。

  3. 依赖管理:在Monorepo中不同项目或模块可能依赖于不同的版本或不同的依赖项,后续的解决方式不能只依靠pnpm workspace本身的能力,建立更好的依赖管理工具。

  4. 团队协作和冲突管理:后续多个团队在同一个代码库中协作可能导致代码冲突和协调困难。团队成员需要注意代码库中的变更,需要并开发适当的工具和流程来解决冲突,以确保代码的一致性和质量。

  5. 版本控制和发布管理:团队需要定义清晰的版本控制策略,并确保发布过程的一致性和稳定性。这包括确保适当的测试和验证,以及处理版本回滚和紧急修复等情况。

  6. 开发环境和工具支持:使用Monorepo可能需要适应新的开发环境和工具。团队成员需要熟悉Monorepo的工作流程,并开发合适的工具来支持代码开发、构建、测试和部署。这可能需要额外的开发支持。

  7. 自动化测试构建:目前公共模块还缺少完善的自动化集成机制,需要开发合适的工具来支持。

Monorepo 的使用趋势在软件开发领域逐渐增长,未来有望继续发展和演进。以下是一些可能的展望:

  1. 工具和生态系统的改进:随着 Monorepo 的普及,可以预见工具和生态系统会进一步改进,以解决更复杂的需求。可能会有更多的工具和框架出现,以提供更好的依赖管理、构建、部署和版本控制等方面的解决方案。

  2. 自动化和集成:随着技术的进步,可以期待更多自动化和集成的解决方案,以进一步简化和优化 Monorepo 的开发流程。这可能涉及到更智能的依赖解决、自动化的构建和测试、持续集成和部署等方面的改进。

  3. 更好的团队协作和沟通:随着团队对 Monorepo 的使用经验的积累,可以预期在团队协作和沟通方面的最佳实践会不断发展。可能会出现更多的工具、流程和方法论,用于促进团队之间的协作和沟通,提高团队的整体效能。

总的来说,Monorepo 在大型项目和团队中具有巨大的潜力和优势。随着技术的不断演进和经验的积累,Monorepo 的应用将变得更加成熟和普遍。然而,每个项目和团队都应根据自身需求和条件,权衡利弊,选择适合自己的开发模式和工具。

常见问题和解决方案:

Monorepo问题记录