使用 pnpm 构建 Monorepo 项目

博物志1000字 3分钟阅读

前言

很久以前就听过"Monorepo"这个词,也大致明白它是干嘛的。只是自己手上那些小项目还用不上这种多项目仓库的形式,就一直没试。

Monorepo 是什么

Monorepo,即单一代码库,就是把多个相关项目或模块的代码放进同一个仓库里管理。

想象一下,一个大型应用可能由多个子模块组成,传统做法会给每个子模块单独建一个仓库(MultiRepo)。 Monorepo 则反过来,把所有相关代码都集中到一个地方。

为什么要使用 Monorepo

代码复用

在 Monorepo 里,所有代码都在同一个仓库,不同项目或模块之间共享代码变得特别容易。 一个模块开发出通用的功能或组件,其他模块直接引用复用就行,不用重复造轮子,既省事又少冗余。

比如说,一个企业级应用里可能有多个项目要用同一套身份验证逻辑。在 Monorepo 下,写一次,各个项目直接引用即可。

版本管理

统一的版本管理是 Monorepo 的另一大优势。某个共享模块更新或修复后,所有依赖它的项目立刻就能用上,整个系统保持一致。

假设一个底层库做了性能优化或修了 bug,在 Monorepo 里,所有用到它的项目拉取最新代码时就自动拿到了改进,不用逐个项目更新部署。

开始实践

先确保装好了 pnpm,没有的话在终端跑一句:

npm install -g pnpm

创建一个新文件夹作为 Monorepo 的根目录,并pnpm init初始化仓库。

在根目录新建 pnpm-workspace.yaml,内容大致如下

packages:
  - packages/**
  • packages 是路径数组,用来指定包含工作区包的目录。
  • 通常子项目会统一放在 root/packages/** 目录下
  • packages 可以指定多个目录

packages 通常作为多项目的存储路径,里面存放的一般是公共代码。在 pnpm 中,它会把 packages 下的子目录都看作一个项目。 另外,如果项目里有文档或者在线演示的项目(这些不属于核心库),放在 packages 里不是很合适,此时就可以按照下面的方式来配置 workspace。

packages:
  - packages/**
  - docs
  - examples

安装依赖

有些依赖需要装到根目录,比如常用的编译依赖 rollup、typescript 等

pnpm add rollup typescript -Dw

其中 -w 表示装在 workspace 根目录,而不是当前目录

给子包装依赖,总不能傻乎乎地 cd 到每个目录一个个 pnpm install。pnpm 提供了过滤参数 --filter 或 -F 来指定命令作用范围。

假如要在 utils 包下装 dayjs,可以这样:

pnpm add dayjs -r --filter @monoremp/utils

假设现在写好了 utils 模块,@monorepo/web 准备使用 utils 模块,可以按照如下操作

pnpm -F @monorepo/web add @monorepo/utils@*

这个命令表示在 @monorepo/web 安装 @monorepo/utils,其中的 @* 表示默认同步最新版本。

之后,打开 packages/web/package.json 发现 dependencies 中多了一行

{
  "name": "@monorepo/web",
  "version": "1.0.0",
  "description": "...",
  "main": "index.tsx",
  "scripts": {
    "dev": "vite"
  },
  "keywords": [],
  "author": "Hsinyau",
  "license": "ISC",
  "dependencies": {
    "@monorepo/utils": "workspace:^1.0.0"
  }
}

最终代码仓库的项目结构大致如下

monorepo
├── package.json
├── packages
│   ├── ui
│   │   ├── index.tsx
│   │   └── package.json
│   ├── web
│   │   ├── index.ts
│   │   ├── ...
│   │   └── package.json
│   ├── utils
│   │   ├── index.ts
│   │   ├── ...
│   │   └── package.json
├── docs
│   │   ├── index.ts
│   │   ├── index.md
│   │   └── package.json
├── examples
│   │   ├── astro
│   │   ├── react
│   │   └── vue
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
└── tsconfig.json

启动和打包项目

在根目录编写 npm scripts 如下所示

"scripts": {
  "postinstall": "pnpm run build",
  "build:ui": "pnpm -C packages/ui run build",
  "build:utils": "pnpm -C packages/utils run build",
  "build": "turbo run build",
  "dev:web": "pnpm -C packages/web dev",
  "...": "..."
}

其中 "-C path" 表示在指定路径下运行 npm 脚本,而不是当前工作路径。

例如在根目录执行 "pnpm dev:web",实际跑的就是 "packages/web/package.json" 里的 "dev" 脚本。

参考

工作空间(Workspace) | pnpm

pnpm cli | pnpm

Create a monorepo using PNPM workspace