1. 起步
什么是Monorepo
?首先monorepo并不是一个框架也不是某个技术,而是一种代码管理策略,指的是在一个单一的代码库中维护多个项目或模块。
目前已经有很多知名的开源项目采用了monorepo的方式去管理项目,例如Vue 3.0、Next.js、Vite、Element Plus等,以vue 3.0为例,我们点进他的观察他的项目工程目录,如下:
我们发现,vue项目的目录似乎和我们传统的项目目录不一样,下面是使用vite提供的脚手架初始化的项目目录:
在vue项目的根目录甚至找不到我们最熟悉的src目录,而是当我们进入packages目录再进compiler-core
这个包才能看到src目录,而且我们查看其它packages包下的目录,它们都有自己的src目录,以及自己的package.json文件。所以,到这里我们应该能意识到好像packages包下的每个目录我们都能把他看成一个“项目”,它们分别负责自己的业务,那么这么做有什么好处呢?
试想一下,我们现在要做一个电商系统,这个系统包括客户端和管理端,按照传统的方式来说,我们会创建两个文件夹,然后一个用来写客户端另外一个用来写管理端像这样:
每个项目的代码互相独立,互不影响,那么这种方式会有什么不足吗?我们来列举两个感知比较强烈的例子:
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
文件来管理我们的包。
之后在根目录分别创建文件夹apps
和packages
,apps目录存放我们的项目,我们将已有的项目拉入到这个目录下,packages用来存放一些包(如utils工具函数,components组件等),之后编辑pnpm-workspace.yaml
文件,填充packages字段,指定需要管理的包是apps及package下的包:
2.2 启动项目
目录创建完成之后,想要启动项目我们要分别到apps/project-admin及apps/project-client下安装依赖,然后npm run build
,这样明显很麻烦,那么有没有便捷方式呢?有,在上面我们在pnpm-workspace.yaml
文件中指定了apps及packages下的所有包,我们只需要在根目录执行pnpm install
,pnpm就会为我们所配置的所有包安装其package.json中的依赖,我们来试下:
执行完成之后,我们发现,在根目录,及我们的两个项目中都生成了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
,之后我们的管理端项目就成功启动了。
为了方便起见,我们修改根目录下的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中导出:
导出之后,我们如何和其它包中安装并使用呢?有三种方式
第一种:
在需要使用这个工具函数的项目package.json中加手动入依赖,然后重新pnpm install
第二种:
切换到需要安装的目录下,执行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);
发现控制台能够正确输出,那么我们的utils代码就算完成复用了。
2.4 依赖共享
例如,我的电商系统的管理端和客户端都需要用到axios
来发送网络请求来和后端交互,在monorepo模式下,我们只需要在根目录下执行pnpm install axios
即可实现apps/目录下全部项目的共享。
安装完成之后,我们发现在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
测试:
配置完catalog后,我们发现pnpm都能按照我们指定的版本进行依赖的安装,需要注意的是catalog协议需要你的pnpm版本大于9,否则会报错。
3. 最后
演示的案例我托管到了码云:monorepo构建: monorepo快速入门