近期,团队在做技术升级,新的模式下我们选择 pnpm workspace 搭建 monorepo 的方式来管理前端业务代码,一个业务方向的多个项目使用统一 git 仓库,具备依赖共享、构建统一等优势。结构如下:
my-business/
├── apps/
│ ├── web/ # 前端应用
│ └── mobile/ # 移动端应用
├── packages/
│ ├── ui/ # 组件库
│ └── utils/ # 工具库
|── pnpm-workspace.yaml
└── package.json
使用 Monorepo 管理的好处:
- 一致的工具链:整个仓库使用统一的工具和配置,如构建工具、代码风格、测试框架等;一次修改,所有相关项目都能受益。
- 统一的版本管理:所有项目使用同一套依赖版本,便于管理,减少因版本不一致导致的问题【借助 pnpm Catalog 特性】。
- 简化依赖管理:内部依赖可以直接通过工作空间链接,而不需要发布到包管理器【借助 pnpm workspace 工作空间协议】。
项目选型使用了 pnpm workspace:
- 节省磁盘空间:依赖会被存储在内容可寻址的存储中,而不是每个项目单独存储,从而节省磁盘空间。
- 提升构建速度:安装过的依赖项都会直接从存储区中获取并链接到 node_modules。
- 非扁平的 node_modules 目录:禁止幽灵依赖,未声明依赖无法被偷偷使用。
Workspace 工作空间
工作空间根目录必须有一个 pnpm-workspace.yaml 文件
yaml
# pnpm-workspace.yaml
packages:
- "packages/*"
- "apps/*"
在工作空间内,一个包可以依赖另一个包,使用 workspace:* 协议(如 web 项目引用了 ui 包)。
json
{
"name": "web",
"version": "0.1.0",
"dependencies": {
"ui": "workspace:*"
}
}
- 在根目录运行
pnpm install,它会安装所有包的依赖,并链接工作空间内的包; - 可以使用
pnpm run --filter <package_name> <script>来在特定包中运行脚本或者使用pnpm -r run <script>在所有包中运行脚本。
Catalogs 将依赖项版本定义为可复用常量
在根目录声明 catalog:,即可为团队钉住一组"可被采用"的依赖版本。子包写 "react": "catalog:" 即自动对齐,不再在每个 package.json 里手动同步版本号。
有两种定义方式:
- 使用 (单数) catalog 字段创建名为 default 的目录。
- 使用 (复数) catalogs 字段创建任意命名的目录。
yaml
# pnpm-workspace.yaml
catalogs:
# 可以通过 "catalog:react18" 引用
react18:
react: 18.2.0
react-dom: 18.2.0
# 可以通过 "catalog:umi" 引用
umi:
"@umijs/max": "4.4.12"
"@umijs/utils": "4.4.12"
json
// apps/web/package.json
{
"dependencies": {
"@umijs/max": "catalog:umi" # 等价于 4.4.12
}
}
优势:
- 维护唯一版本:通常希望在工作空间中共同的依赖项版本一致。 Catalog 让工作区内共同依赖项的版本更容易维护。 减少重复的依赖关系可能的冲突及重复依赖项打包体积增大。
- 易于更新:升级或者更新依赖项版本时,只需编辑 pnpm-workspace.yaml 中的目录,而不需要更改所有用到该依赖项的 package.json 文件。 这样可以节省时间 --- 只需更改一行,而不是多行。
- 减少合并冲突:由于在升级依赖项时不需要编辑 package.json 文件,所以这些依赖项版本更新时就不会发生 git 冲突。
需要注意的是:要更新 pnpm-workspace.yaml 中定义的依赖项,需要手动选择较新的版本范围(pnpm update 暂不支持)。
peerDependencies 共享宿主环境的依赖
json
// packages/ui/package.json
{
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
对等依赖(Peer Dependencies) 是一种特殊的依赖关系,这里用于组件库&工具库之间共享相同的核心依赖(如 React、ReactDOM 等)。
filter 指定包运行脚本
- 可以使用
pnpm run --filter <package_name> <script>来在特定包中运行脚本
shell
# 运行 web 项目的 dev 脚本
pnpm run --filter web dev
- 执行某包及依赖包的脚本(执行 web 及 ui、utils 项目的 dev 脚本)
shell
# 运行 web 及其依赖项的 build 脚本
pnpm --filter web... build
注意:pnpm --filter 的默认行为是:串行执行,前一个任务不退出,下一个不会开始。
shell
pnpm --filter web... dev
并不会按顺序执行完成,原因是:前一个 dev 不退出,导致下一个不会开始。
PS:其他匹配方式,详见官方文档:https://www.pnpm.cn/filtering
上述 dev 怎么并行执行?
方式一:使用 package.json 的 pre 和 post 钩子
json
{
"scripts": {
"dev": "pnpm --filter web dev",
"predev": "pnpm --filter ui dev"
}
}
方式二:使用第三方包 concurrently
json
{
"scripts": {
"dev": "concurrently \"pnpm --filter web dev\" \"pnpm --filter ui dev\""
}
}
[推荐]方式三:使用 turbo 管理
json
{
"scripts": {
"dev": "turbo run dev --filter web --filter ui"
}
}
PS:后续文章专门介绍 turbo 管理 monorepo 项目。