Monorepo
Monorepo可以理解为:利用单一仓库来管理多个packages的一种策略或手段,与其相对的是我们接触最多的Multirepo。
可以使用项目目录结构来区分这两种模式:
# monorepo目录结构
|-- monorepo-demo
| |-- packages # packages目录
| | |-- compiler # compiler子包
| | | |-- package.json # compiler子包特有的依赖
| | |-- reactivity # reactivity子包
| | | |-- package.json # reactivity子包特有的依赖
| | |-- shared # shared子包
| | | |-- package.json # shared子包特有的依赖
| |-- package.json # 所有子包都公共的依赖
# multirepo-a目录结构
|-- multirepo-a
| |-- src
| | |-- feature1 # feature1目录
| | |-- feature2 # featrue2目录
| |-- package.json # 整个项目依赖
# multirepo-b目录结构
|-- multirepo-b
| |-- src
| | |-- feature3 # feature3目录
| | |-- feature4 # featrue4目录
| |-- package.json # 整个项目依赖
可以很清楚的看到他们之间的差异:
Monorepo目录中除了会有公共的package.json依赖以外,在每个sub-package子包下面,也会有其特有的package.json依赖。Multirepo更倾向与在项目制中,将一个个项目使用不同的仓库进行隔离,每一个项目下使用独有的package.json来管理依赖。
关于这两者的对比和Monorepo的优缺点,我们会在Monorepo特点这个章节进行介绍,在下一节我们来学习如何搭建一个Monorepo应用。
Monorepo项目搭建
目前,搭建Monorepo项目主要有两种方式:
Lerna + yarn workspace方式。pnpm方式。
在Vue3.2.22版本中,是使用pnpm来搭建Monorepo项目的,所以我们直接采用第二种方式。
搭建项目
全局安装pnpm
提示
# 安装pnpm
$ npm install pnpm -g
# 安装完毕后查看pnpm版本
$ pnpm -v
6.24.1
# 查看node版本
$ node -v
v16.13.0
安装完毕后,我们创建如下目录结构:
|-- monorepo-demo
| |-- packages # packages目录
| | |-- compiler # compiler子包
| | |-- reactivity # reactivity子包
| | |-- shared # shared子包
随后,在根目录以及每一个子包目录下都执行一遍npm init -y命令,让其创建一个package.json文件。全部执行完毕后,其目录结构如下所示:
|-- monorepo-demo
| |-- packages # packages目录
| | |-- compiler # compiler子包
| | | |-- package.json # compiler子包特有的依赖
| | |-- reactivity # reactivity子包
| | | |-- package.json # reactivity子包特有的依赖
| | |-- shared # shared子包
| | | |-- package.json # shared子包特有的依赖
| |-- package.json # 所有子包都公共的依赖
接着,修改根目录下的package.json文件:
{
"name": "MyVue", // 避免pnpm安装时重名
"private": true, // 标记私有,防止意外发布
"version": "1.0.0",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
}
接下来,进入到每一个子包中,依次修改package.json,我们以compiler这个包为例。
{
"name": "@MyVue/compiler", // 避免安装时跟@vue/* 重名
"version": "1.0.0",
"description": "@MyVue/compiler",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
最后回到根目录,创建pnpm-workspace.yaml文件,并撰写如下内容:
packages:
- 'packages/*'
至此,Monorepo项目结构已经初步搭建完毕,此时的目录结构如下:
|-- monorepo-demo
| |-- packages # packages目录
| | |-- compiler # compiler子包
| | | |-- package.json # compiler子包特有的依赖
| | |-- reactivity # reactivity子包
| | | |-- package.json # reactivity子包特有的依赖
| | |-- shared # shared子包
| | | |-- package.json # shared子包特有的依赖
| |-- package.json # 所有子包都公共的依赖
| |-- pnpm-workspace.yaml # pnpm配置文件
安装依赖
依赖分为两部分,第一部分是公共依赖,第二部分是特有依赖。
公共依赖
公共依赖指的是为所有子包共享的包,例如:eslint、typescript或者prettier等等。
# 在根目录安装eslint 和 typescript
$ pnpm install eslint typescript --save-dev
当执行以上命令后,控制台会报如下错误:
$ pnpm install eslint typescript --save-dev
ERR_PNPM_ADDING_TO_ROOT Running this command will add the dependency to the workspace root,
which might not be what you want - if you really meant it, make it explicit by running this command again with the -w flag (or --workspace-root).
If you don't want to see this warning anymore, you may set the ignore-workspace-root-check setting to true.
上面的意思时:如果我们确定要安装的依赖包需要安装到根目录,那么需要我们添加-w参数,因此修改我们的命令如下:
# 在根目录安装eslint 和 typescript
$ pnpm install eslint typescript --save-dev -w
安装完毕后,可以在根目录的package.json文件中看到devDependencies依赖包信息:
"devDependencies": {
"eslint": "^8.5.0",
"typescript": "^4.5.4"
}
特有依赖
现在,假设我们有这样一个场景:
packages/shared依赖包有:lodash。packages/reactivity依赖包有:@MyVue/shared。packages/compiler依赖包有:@MyVue/shared和@MyVue/reactivity
基于以上场景,我们该如何添加特有依赖?
- 给
packages/shared添加依赖:
提示
-r表示在workspace工作区执行命令,--filter xxx 表示指定在哪个包下执行。
$ pnpm install lodash -r --filter @MyVue/shared
添加完毕后,可以在packages/shared目录下的package.json文件看到如下dependencies信息:
"dependencies": {
"lodash": "^4.17.21"
}
- 给
packages/reactivity添加依赖:
提示
因为@MyVue/shared属于本地包依赖,所以带有前缀workspace。
$ pnpm install @MyVue/shared -r --filter @MyVue/reactivity
添加完毕后,可以在packages/reactivity目录下的package.json文件看到如下dependencies信息:
"dependencies": {
"@MyVue/shared": "workspace:^1.0.0"
}
同样的道理,当我们在packages/compiler安装完依赖后,可以在package.json文件中看到如下dependencies信息:
"dependencies": {
"@MyVue/reactivity": "workspace:^1.0.0",
"@MyVue/shared": "workspace:^1.0.0"
}`
最后,项目的基础结构已经搭建完毕,在下一节我们来介绍一下Monorepo的特点。
Monorepo特点
Monorepo 和 Multirepo
一般而言,大型开源库,例如Babal以及Vue3等等都会选择使用Monorepo,而日常业务中,通常都是项目制的,通常会选择Multirepo,那么这两者之前有什么区别呢?
- 规范、工作流的统一性:在使用
Multirepo时,我们通常在遇到一个新项目的时候,会利用现有的脚手架或者手动重新搭建一套项目结构,这就使得不同的项目往往存在于不同的仓库中,而又因为种种原因无法做到代码规范、构建流程、发布流程等的统一性。使用Monorepo则不会存在这个问题,因为所有的packages包全部都在一个仓库中,自然而然就可以做到代码规范、构建流程和发布流程的统一性。 - 代码复用和版本依赖:想象一下这样一个场景:当你的A项目依赖了B项目中的某个模块,你必须等到
B项目重新发布以后,你的A项目才能正常开发或发布。如果B项目是一个基础库的话,那么B的每次更新都会影响到所有依赖B的项目。对那些没有提取复用逻辑,但又会CV在各个项目中函数、组件等,如果存在改动情况,则需要在每一个项目中都改动。这是使用Multirepo必须要去解决的两个问题造:代码复用问题和版本依题。如果使用的是Monorepo则可以很容易的解决这个问题,对于那些需要复用的逻辑,可以选择把它们都提取到一个公共的packages下,例如packages/shared。而对于版本依赖问题,则更好解决。因为所有packages都在一个仓库,无论是本地开发或者发布都没有问题。 - 团队协作以及权限控制:根据
Monorepo的特点,各个packages之间相对独立,所以可以很方便的进行职责划分。然而正是因为所有packages都在一个仓库下,所以在代码权限控制上很难像Multirepo那样进行划分,这无疑提高了Monorepo的门槛,它必须严格要求所有开发者严格遵守代码规范、提交规范等。 - 项目体积:对于使用
Monorepo的项目来说,随着项目的迭代,在代码体积和git提交方面都会比Multirepo项目增长快的很多,甚至会出现启动一个项目、修改后热更新非常慢的情况。不过随着打包工具的发展,这些都不再是问题。
这一章、对于Monorepo的介绍就到这里,在下一章我们将介绍Monorepo如何进行rollup打包。