Monorepo是什么,从0到1带你配置
什么是**Monorepo**
Monorepo 全称 monolithic repository(单个仓库),是指将所有相关代码组织在一个单一的存储库中的开发模式。
如下图所示,左侧我们把不同功能的项目放到了多个仓库中,这就是Multirepo。将其聚合在一个仓库中即为Monorepo。

为什么使用Monorepo
很多同学可能会好奇,为什么要将这些不同功能的仓库放在一起呢?
我们先来梳理一下。当我们本地开发不同仓库的代码时,如果B仓库的某个功能需要以A仓库的代码组件为基础,我们该如何使用呢?
举个通俗的例子。
比如你写业务项目需要用到ElementUI,写React项目需要用到Antd。我们通常在业务仓库install组件库之后,再import进来使用。这种开发前提是你使用的功能都是ElementUI和Antd已经有的。
但是,假如某个需求迭代是基于Antd,但是还没有开发出来,你该怎么办呢?
方法1:开发Antd组件,将其发布到npm库中。然后在编写业务组件时,更新Antd的版本并将其下载安装。
方式2:通过npm link将你的业务仓库和组件库进行链接。
无论是方式1还是方式2,一旦你在开发过程中发现组件库有bug需要重新修改,都需要耗费不少时间。
哪怕是使用link模式,如果我同时业务组件依赖Utils,Apis,UiComponent三个库,要进行三次link。
Menorepo恰恰可以解决上面的问题。
他的优点如下:
- 代码复用变得很容易,因为所有的项目代码都在一个仓库,我们很容易通过工具将其在各个代码文件中引用
- 依赖变得简单
- 发布npm包简单,我们可以基于不同项目的代码,能够再次提取公共部分,快速发到npm库上
- 避免重复安装包,减少磁盘空间,降低构建时间
基于pnpm创建一个monorepo仓库
仓库分析
三个项目h5-vue,web-vue,utils。
h5-vue对应着我们h5的项目,web-vue对应着我们web项目,utils是是这两vue项目都会用到的一些工具方法。
如果不将utils进行抽离的话,我们需要在h5-vue和web-vue都要写一遍,如果后期要改的话,也要改两次,成本很大。
所以,将utils抽离成npm库,并且和我们的两个vue项目组成menorepo很适合。

安装pnpm
npm install -g pnpm
仓库初始化
通过一下命令创建一个项目
# 创建项目目录
mkdir pnpm-monorepo
# 进入项目目录
cd pnpm-monorepo
之后我们在初始化package.json文件
pnpm init 
在目录下,我们生成了一个package.json文件。在其中增加privete:true,表示该根目录不会发布到npm库。
{
  "name": "pnpm-monorepo",
  "version": "1.0.0",
  "private": true,
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
接着我们需要创建workspace,用于定义各个仓库的工作区间,创建pnpm-workspace.yaml,内容如下。
packages:
# packages的子目录
  - 'packages/*'
# utils下的所有目录
  - 'utils/**'
创建项目
创建vue项目
创建packages目录,进入到packages目录,创建vue项目
pnpm create vue@latest
一个项目,名称叫h5-vue,另一个叫web-vue,剩下的内容全部选no,即可得到如下

创建utils项目
因为utils定义为一个工具函数仓库,所以我们用vite创建。进入到根目录,执行以下命令
pnpm create vite
名称为uitls,然后分别选择vue,JavaScript

进入到h5-vue/web-vue。执行pnpm install安装依赖。
增加测试代码
utils
进入到utils/src创建一个index.js文件,创建sum函数
export const sum = (a,b) => {
  return a+b
}
需要将函数export,所以我们进入到utils目录下的package.json,增加修改如下代码
{
  ...
  "name": "@sanmu/utils",
  "main": "./src/index.js",
  ...
}
main的含义是,该项目的入口文件路径是"./src/index.js"
在名称前面增加@sanmu的限制,是因为utils这个名称太常见了,很容易发生重名的情况,因此我们需要进行限制。
这种写法在一些常见的库中也很常见,例如@babel/core、@babel/cli、@babel/parser。
h5-vue/web-vue
进入到我们的业务代码目录,然后执行
pnpm add @sanmu/utils
在打开我们的h5/-vue下面的package.json,就可以发现多了一段
"@sanmu/utils": "workspace:^0.0.0",

因为这个
workspace这个关键字,pnpm在就会从你配置的workspace里查找,而不是从npm仓库。
在h5-vue中,我们执行pnpm dev将项目启动,然后访问http://localhost:5173/
接着在App.vue修改代码如下,将默认的代码都删除,然后引入我们utils下面的sum函数
<script setup>
import { sum } from '@sanmu/utils';
const ans = sum(1, 2);
</script>
<template>1 + 1的结果是{{ ans }}</template>
页面的结果如下,也代表着我们的sum方法引入成功
在web-vue项目下,将上面的步骤重新操作一遍
安装依赖
我们依赖分为两种:
- 公共的依赖,仓库中的大部分都需要使用的依赖,比如babel,ts,lodash等
- 只属于某个项目中使用的依赖,比如拖拽的组件库
公共依赖
对于第一个,我们可以通过-w, --workspace-root 参数,可以将依赖包安装到工程的根目录下,作为所有 package 的公共依赖。
pnpm install typescript -w
某个package的依赖
通过 --filter来限制
pnpm add axios --filter '@sanmu/h5-vue'
打包
在每个项目中的package.json的scripts命令下配置好build命令
然后,我们在根目录的package.json配置
"build": "pnpm -r exec pnpm build",
pnpm exec 表示在项目内执行shell命令,-r代表代表工作区的每个项目中执行,执行的pnpm build就是执行我们在各个项目中scripts下的命令
pnpm是默认按照拓扑排序的,

我们的项目vue项目依赖utils项目,所以打包也应该是utils先打包,然后在打包vue项目,然后更新vue的版本

可以看到这里的打包顺序确实如此。
对于每次发布之前的版本号的更新,也不需要手动去处理,可以用 changeset。
总之,通过pnpm可以很简单的配置出monorepo的仓库,更多的是对于pnpm命令的熟悉。
除此之外还可以通过yarn workspace+learn实现,思想都差不多,无非就是命令和配置不同。
参考文章
https://qwqaq.com/2021/08/what-is-monorepo/
https://juejin.cn/post/7081440800143310884