10分钟快速入门Monorepo

1,255 阅读8分钟

1. 起步

什么是Monorepo?首先monorepo并不是一个框架也不是某个技术,而是一种代码管理策略,指的是在一个单一的代码库中维护多个项目或模块。 目前已经有很多知名的开源项目采用了monorepo的方式去管理项目,例如Vue 3.0Next.jsViteElement Plus等,以vue 3.0为例,我们点进他的观察他的项目工程目录,如下:

1.jpg 我们发现,vue项目的目录似乎和我们传统的项目目录不一样,下面是使用vite提供的脚手架初始化的项目目录:

2.jpg

在vue项目的根目录甚至找不到我们最熟悉的src目录,而是当我们进入packages目录再进compiler-core这个包才能看到src目录,而且我们查看其它packages包下的目录,它们都有自己的src目录,以及自己的package.json文件。所以,到这里我们应该能意识到好像packages包下的每个目录我们都能把他看成一个“项目”,它们分别负责自己的业务,那么这么做有什么好处呢?

试想一下,我们现在要做一个电商系统,这个系统包括客户端和管理端,按照传统的方式来说,我们会创建两个文件夹,然后一个用来写客户端另外一个用来写管理端像这样:

3.jpg

每个项目的代码互相独立,互不影响,那么这种方式会有什么不足吗?我们来列举两个感知比较强烈的例子:

1. 代码复用困难

例如,我们先做管理端的项目,在管理端中我们封装一些utils函数,或者封装了一些组件,那么等待管理端开发完成,我们来到客户端的编码工作,发现管理端的一些utils工具函数或者组件我们的客户端也需要用到,这时候我们只能再去拷贝一份,非常麻烦。

2. 依赖管理复杂

例如,我们的管理端和客户端都需要用到了Element Plus组件库,如果想要修改Element Plus的版本,要到每个项目中都修改一次。

那么如果我们把这些代码都放到一块儿,使用一个仓库来管理,那么我们的代码是不是就能复用了呢?我们的依赖管理是不是就能简化了呢?答案是肯定的。

上面我们提到了几个关键词,每个项目的代码互相独立,互不影响,各自拥有一个git仓库,这种代码管理的方式叫做Multirepo,而使用一个仓库来管理项目的方式就叫Monorepo,下面再来列举下这两种代码管理方式的区别:

特性Monorepo(单体仓库)MultiRepo(多仓库)
定义将多个项目存储在同一个代码仓库中。将每个项目存储在独立的代码仓库中。
代码共享易于重用和共享代码,减少重复劳动。跨项目复用困难,需要额外工作保持同步。
依赖管理简化依赖管理,减少依赖冲突。依赖管理复杂,可能需要额外的工具和配置。
权限控制需要精细化的权限管理以确保不同项目的访问控制。更容易对每个项目进行精细化权限管理。
构建时间构建时间可能较长,需要优化构建流程。构建时间较短,因为只涉及单个项目的构建。
协作效率团队成员可以在同一个仓库中协作,提升协作效率团队成员需要在多个仓库之间切换,可能降低协作效率
版本控制统一版本控制,便于管理和协调每个仓库有自己的版本控制,管理复杂性增加
适用场景大型项目、频繁共享代码、一致性要求高的项目。独立性强的项目、灵活性需求高、规模较小的项目。

根据上面的对比,Monorepo和Multirepo各有优缺点,适用于不同的项目和团队。选择哪种模式取决于具体项目的需求、团队的工作方式,但是每种代码的管理方式都是我们需要了解的。

2. 实践

接下来,我们就使用pnpm Workspace工作空间(Workspace)的方式来实现Monorepo的代码管理方式,需要说明这并不是唯一实现的方式,你也可以选择使用Yarn Workspaces或npm结合相关的库实现。

2.1 目录创建

在起步中,我们已经使用vite提供的脚手架初始化了电商系统的管理端和客户端,接下来我们在根目录下执行pnpm init来生成根目录下的package.json文件,并创建 pnpm-workspace.yaml文件来管理我们的包。

之后在根目录分别创建文件夹appspackages,apps目录存放我们的项目,我们将已有的项目拉入到这个目录下,packages用来存放一些包(如utils工具函数,components组件等),之后编辑pnpm-workspace.yaml文件,填充packages字段,指定需要管理的包是apps及package下的包:

5.jpg

2.2 启动项目

目录创建完成之后,想要启动项目我们要分别到apps/project-admin及apps/project-client下安装依赖,然后npm run build,这样明显很麻烦,那么有没有便捷方式呢?有,在上面我们在pnpm-workspace.yaml文件中指定了apps及packages下的所有包,我们只需要在根目录执行pnpm install,pnpm就会为我们所配置的所有包安装其package.json中的依赖,我们来试下:

6.jpg

执行完成之后,我们发现,在根目录,及我们的两个项目中都生成了node_modules,就表明我们的依赖都安装成功了,之后要怎么启动呢?

第一种方式,我们可以cd到对应的目录下执行pnpm run dev

第二种方式(推荐),通过pnpm的过滤功能过滤 | pnpm中文文档,也就是--filter这个选项,可以缩写为-F,指定在具体项目中执行对应的脚本,具体用法为pnpm -F 具体项目package.json中的name run dev

这里我们演示下第二种方式,找到apps/project-admin/package.json中的name,执行pnpm -F project-admin run dev,之后我们的管理端项目就成功启动了。

7.jpg

为了方便起见,我们修改根目录下的package.json加入script字段,指定快速启动的脚本

{
  "name": "project-root",
  "version": "1.0.0",
  "private": true,
  "description": "monorepo root",
  "keywords": [
    "monorepo",
    "vue"
  ],
  "author": "M木",
  "type": "module",
  "scripts": {
    "dev:client": "pnpm -F project-client run dev",
    "dev:admin": "pnpm -F project-admin run dev"
  },
  "dependencies": {}
}

之后,我们只需要在根目录执行pnpm run dev:admin就能启动项目了。

2.3 代码复用

进入packages/components/utils目录,新建src及package.json文件,在package.json中指定包的名称及默认导出的内容

{
  "name": "@m/utils",
  "version": "1.0.0",
  "private": true,
  "description": "m'utils",
  "type": "module",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "default": "./src/index.ts"
    }
  }
}

在src目录下新建index.ts作为入口文件,新建isUtils工具函数文件判断变量类型,并在index中导出:

8.jpg

导出之后,我们如何和其它包中安装并使用呢?有三种方式

第一种

在需要使用这个工具函数的项目package.json中加手动入依赖,然后重新pnpm install

9.jpg

第二种

切换到需要安装的目录下,执行pnpm add @m/utils --workspace -D ,需要说明的是--workspcae这个选项表示仅添加在 workspace 找到的依赖项,-D表示将指定的 packages 安装为 devDependencies

第三种

利用过滤,在任意目录执行pnpm add @m/utils --workspace -D -F project-client,这段代码就表明我要安装@m/utils这个包,-F指定在project-client项目中安装

安装完成之后,我们来测试下能否正常使用,我们在App.vue中引入utils包,并使用工具函数

import { isUtils } from '@m/utils'
console.log('isUtils.isUndefined(undefined)', isUtils.isUndefined(undefined));
console.log("isUtils", isUtils);

10.jpg

发现控制台能够正确输出,那么我们的utils代码就算完成复用了。

2.4 依赖共享

例如,我的电商系统的管理端和客户端都需要用到axios来发送网络请求来和后端交互,在monorepo模式下,我们只需要在根目录下执行pnpm install axios即可实现apps/目录下全部项目的共享。

11.png

安装完成之后,我们发现在apps/project-client的node_modules下并没有axiso的依赖,但是我们在客户端的App.vue中依然能够使用axios,原因是它会先从project-client的node_modules中去找依赖,如果找不到,会上升的上一级也就是根节点的node_modules中去找,这样做我们只需要在根节点安装一次依赖,所有的项目都能共享。

2.5 依赖版本控制

如果我们apps/下的项目很多,有很多项目都需要用到相同的依赖,为了保证依赖版本的统一,我们可以在pnpm-workspace.yaml中配置catalog或catalogs节点[Catalogs | pnpm中文文档 | pnpm中文网],来指定依赖的版本,删除pnpm-lock.ymal及node_modules重新执行pnpm install测试:

12.jpg

配置完catalog后,我们发现pnpm都能按照我们指定的版本进行依赖的安装,需要注意的是catalog协议需要你的pnpm版本大于9,否则会报错。

3. 最后

演示的案例我托管到了码云:monorepo构建: monorepo快速入门