pnpm 在 v9.5 加入了 catalogs 功能,用于对 monorepo 依赖进行分类管理。这将大大降低 monorepo 内部对于多个项目之间依赖版本统一管理的难度。为了清楚地认识到 catalogs 功能对于 monorepo 依赖管理的便捷性,下面我们从 monorepo 的介绍以及 catalogs 的使用两方面来探讨。
monorepo 中 mono 表示单个,repo 表示 repository,monorepo 也就是单一仓库的意思。monorepo 最初跟随着分布式版本控制系统 Git 兴起(2005年)而被 Google 使用。后来随着前端开源社区的发展,Lerna 在 2015 年发布,成为了第一个在 JavaScript 社区普及 Monorepo 概念的标志性工具。Lerna 解决了两个核心问题:
后来又有了 Meta 开源的 yarn workspace(2017),以及后来更完善的 pnpm Workspace(2020)。
首先我个人认为,对于业务领域内子项目 < 5 个,维护人数 < 5 的前端项目应该使用 monorepo,可以获得以下好处:
components 和 utils,如果走传统的多仓库发包更新,流程冗长,非常不利于团队内部共享使用;并且如果大家各自都开发一份,又会增加时间和人力成本。使用 monorepo 的优势明显,但是缺陷也存在:
overrides 强行锁定 monorepo 内 React 的版本就会出问题。所以,monorepo 内部的项目应该定期对稳定的基础库做升级,大型稳定库例如 React 都是向下兼容,这种升级成本不会太高但是远期收益很高。首先 npm v7(Nodejs v15)开始支持 workspaces 功能,使用步骤如下:
{
"name": "my-monorepo",
"workspaces": [
"packages/*", // 匹配 packages 文件夹下的所有子项目
"apps/*" // 匹配 apps 文件夹下的所有子项目
]
}
-w (或 --workspace) 和 --ws (或 --workspaces) 标志来筛选执行脚本的目录,参考npm-workspace-demo 仓库示例# 在 my-app 子项目中运行 dev 脚本
npm run dev -w my-app
# 在所有子项目中运行 test 脚本
npm run test --ws
npm 会将多个项目的依赖统一安装到项目根目录的 node_modules 目录下,即使是单个项目自己用到的依赖也是如此,而针对项目间相同依赖不同版本则安装到项目内部 node_modules。所以会 npm workspace 会带来幽灵依赖以及占用磁盘空间的问题(现在的存储价格可是寸土寸金)。
lerna 的缺陷在于它只是一个项目管理工具,没有和 npm、yarn 结合。
使用 lerna 需要自己管理依赖关系,也就是使用 npm 或者 yarn 安装依赖,然后使用 lerna bootstrap 来在 monorepo 内部的包之间创建符号链接。由于 lerna 依赖 npm,所以会暴露和 npm 一样的幽灵依赖等问题。
yarn v1 虽然将 monorepo 的项目管理和 yarn cli 命令结合在了一起,但是依然存在依赖提升,性能和磁盘空间占用的问题。
而 yarn v2 以后增加了 pnp 模式,不创建 node_modules,所有包直接从一个 .pnp.cjs 索引文件中获取,优点是极速解析、无重复依赖。但是生态兼容性问题严重,IDE、ESLint、ts-node、Webpack 等工具需特殊配置。参考 yarn-workspace-demo
pnpm workspace 主要解决了以下问题:
.pnpm-store 全局缓存,所有本地使用的包只会安装一次,后续使用硬链接绑定。不仅减少后续安装时间,而且大大减少 monorepo 仓库体积;为什么 monorepo 内部要保证依赖版本的统一性呢?
pnpm 支持在 pnpm-workspace.yaml 中使用 catalog 定义一个名为 default 的目录。这些版本范围可以通过 catalog:default 引用。仅有默认目录时,也可以使用特殊的 catalog: 简写。 将 catalog: 视为可扩展为 catalog:default 的简写。
packages:
- packages/*
catalog:
react: ^18.2.0
react-dom: ^18.2.0
在 monorepo 内的项目可以在 package.json 中通过 catalog:default 引用默认依赖。仅有默认目录时,也可以使用特殊的 catalog: 简写。
{
"name": "@example/app",
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
}
}
也可以使用 catalogs 自定义多个名称的 catalog:
catalogs:
# 可以通过 "catalog:react17" 引用
react17:
react: ^17.0.2
react-dom: ^17.0.2
# 可以通过 "catalog:react18" 引用
react18:
react: ^18.2.0
react-dom: ^18.2.0
然后项目内部的 package.json 使用 catalog:<name> 来表示依赖版本。
{
"name": "@example/components",
"dependencies": {
"react": "catalog:react18",
}
}
对于需要发布的包,pnpm 也会在发布的时候替换到 catalog:
pnpm 在 v10.12.1 版本对 pnpm add 命令支持了 --save-catalog 和 --save-catalog-name 两个选项。这两个选项主要有两个作用:
catalog 或者指定名称的 catalogs,并且使用 catalog:[name] 作为目标项目依赖的版本写入 package.json;# 将 lodash 添加到 utils 并添加到默认 catalog
pnpm add lodash --filter utils --save-catalog
# 将 lodash 添加到 components 并添加到目录 app-utils
pnpm add lodash --filter components --save-catalog-name app-utils
catalog 中定义的依赖,如果没有指定依赖的版本,使用 pnpm add 命令时会直接使用 catalog: 作为版本写入项目的 package.json;如果带上 --save-catalog-name 选项,则会从指定 catalog 读取并作为要安装的依赖版本,没有则添加到指定名称的 catalog 下。在没有 catalog 的时候,如果要保证多个项目安装同一依赖的版本唯一性,大概有以下几种方式:
overrides 强制约定整个依赖树 (包括直接和间接/传递依赖)的版本唯一。这会导致限制的依赖版本低时无法使用一些使用高版本 peerDependencies 的库,比如很多 React 库只会兼容最新的 React 版本。pnpmfile.cjs 的 hook,自行编写脚本,在安装依赖的时候扫描所有其他项目依赖版本,保证安装版本的统一;在有了 catalogs 之后,monorepo 内部统一版本依赖只需要在使用 pnpm add 时带上 --save-catalog 和 --save-catalog-name 两个选项即可,很轻松地就共享了项目依赖的版本,同时在 pnpm-workspace.yaml 也能很明显地查看到项目依赖的情况。
对于 catalog 分类,我个人倾向于不是那么实用,只要项目能够共享的依赖版本,大可以直接使用 --save-catalog 保存到默认目录下即可,后续使用 pnpm add 也能快速在其他项目内部安装。
catalog 的唯一缺点就是在项目的 package.json 中定义时无法直接显示依赖版本,这里就推荐一个 antfu 的 vscode 插件 —— Catalog Lens. 能够在 vscode 中将 package.json 中使用 catalog: 的版本替换成具体对应的版本号。

pnpm 在 v9.5 加入了 catalogs 功能,用于对 monorepo 依赖进行分类管理。这将大大降低 monorepo 内部对于多个项目之间依赖版本统一管理的难度。为了清楚地认识到 catalogs 功能对于 monorepo 依赖管理的便捷性,下面我们从 monorepo 的介绍以及 catalogs 的使用两方面来探讨。
monorepo 中 mono 表示单个,repo 表示 repository,monorepo 也就是单一仓库的意思。monorepo 最初跟随着分布式版本控制系统 Git 兴起(2005年)而被 Google 使用。后来随着前端开源社区的发展,Lerna 在 2015 年发布,成为了第一个在 JavaScript 社区普及 Monorepo 概念的标志性工具。Lerna 解决了两个核心问题:
后来又有了 Meta 开源的 yarn workspace(2017),以及后来更完善的 pnpm Workspace(2020)。
首先我个人认为,对于业务领域内子项目 < 5 个,维护人数 < 5 的前端项目应该使用 monorepo,可以获得以下好处:
components 和 utils,如果走传统的多仓库发包更新,流程冗长,非常不利于团队内部共享使用;并且如果大家各自都开发一份,又会增加时间和人力成本。使用 monorepo 的优势明显,但是缺陷也存在:
overrides 强行锁定 monorepo 内 React 的版本就会出问题。所以,monorepo 内部的项目应该定期对稳定的基础库做升级,大型稳定库例如 React 都是向下兼容,这种升级成本不会太高但是远期收益很高。首先 npm v7(Nodejs v15)开始支持 workspaces 功能,使用步骤如下:
{
"name": "my-monorepo",
"workspaces": [
"packages/*", // 匹配 packages 文件夹下的所有子项目
"apps/*" // 匹配 apps 文件夹下的所有子项目
]
}
-w (或 --workspace) 和 --ws (或 --workspaces) 标志来筛选执行脚本的目录,参考npm-workspace-demo 仓库示例# 在 my-app 子项目中运行 dev 脚本
npm run dev -w my-app
# 在所有子项目中运行 test 脚本
npm run test --ws
npm 会将多个项目的依赖统一安装到项目根目录的 node_modules 目录下,即使是单个项目自己用到的依赖也是如此,而针对项目间相同依赖不同版本则安装到项目内部 node_modules。所以会 npm workspace 会带来幽灵依赖以及占用磁盘空间的问题(现在的存储价格可是寸土寸金)。
lerna 的缺陷在于它只是一个项目管理工具,没有和 npm、yarn 结合。
使用 lerna 需要自己管理依赖关系,也就是使用 npm 或者 yarn 安装依赖,然后使用 lerna bootstrap 来在 monorepo 内部的包之间创建符号链接。由于 lerna 依赖 npm,所以会暴露和 npm 一样的幽灵依赖等问题。
yarn v1 虽然将 monorepo 的项目管理和 yarn cli 命令结合在了一起,但是依然存在依赖提升,性能和磁盘空间占用的问题。
而 yarn v2 以后增加了 pnp 模式,不创建 node_modules,所有包直接从一个 .pnp.cjs 索引文件中获取,优点是极速解析、无重复依赖。但是生态兼容性问题严重,IDE、ESLint、ts-node、Webpack 等工具需特殊配置。参考 yarn-workspace-demo
pnpm workspace 主要解决了以下问题:
.pnpm-store 全局缓存,所有本地使用的包只会安装一次,后续使用硬链接绑定。不仅减少后续安装时间,而且大大减少 monorepo 仓库体积;为什么 monorepo 内部要保证依赖版本的统一性呢?
pnpm 支持在 pnpm-workspace.yaml 中使用 catalog 定义一个名为 default 的目录。这些版本范围可以通过 catalog:default 引用。仅有默认目录时,也可以使用特殊的 catalog: 简写。 将 catalog: 视为可扩展为 catalog:default 的简写。
packages:
- packages/*
catalog:
react: ^18.2.0
react-dom: ^18.2.0
在 monorepo 内的项目可以在 package.json 中通过 catalog:default 引用默认依赖。仅有默认目录时,也可以使用特殊的 catalog: 简写。
{
"name": "@example/app",
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
}
}
也可以使用 catalogs 自定义多个名称的 catalog:
catalogs:
# 可以通过 "catalog:react17" 引用
react17:
react: ^17.0.2
react-dom: ^17.0.2
# 可以通过 "catalog:react18" 引用
react18:
react: ^18.2.0
react-dom: ^18.2.0
然后项目内部的 package.json 使用 catalog:<name> 来表示依赖版本。
{
"name": "@example/components",
"dependencies": {
"react": "catalog:react18",
}
}
对于需要发布的包,pnpm 也会在发布的时候替换到 catalog:
pnpm 在 v10.12.1 版本对 pnpm add 命令支持了 --save-catalog 和 --save-catalog-name 两个选项。这两个选项主要有两个作用:
catalog 或者指定名称的 catalogs,并且使用 catalog:[name] 作为目标项目依赖的版本写入 package.json;# 将 lodash 添加到 utils 并添加到默认 catalog
pnpm add lodash --filter utils --save-catalog
# 将 lodash 添加到 components 并添加到目录 app-utils
pnpm add lodash --filter components --save-catalog-name app-utils
catalog 中定义的依赖,如果没有指定依赖的版本,使用 pnpm add 命令时会直接使用 catalog: 作为版本写入项目的 package.json;如果带上 --save-catalog-name 选项,则会从指定 catalog 读取并作为要安装的依赖版本,没有则添加到指定名称的 catalog 下。在没有 catalog 的时候,如果要保证多个项目安装同一依赖的版本唯一性,大概有以下几种方式:
overrides 强制约定整个依赖树 (包括直接和间接/传递依赖)的版本唯一。这会导致限制的依赖版本低时无法使用一些使用高版本 peerDependencies 的库,比如很多 React 库只会兼容最新的 React 版本。pnpmfile.cjs 的 hook,自行编写脚本,在安装依赖的时候扫描所有其他项目依赖版本,保证安装版本的统一;在有了 catalogs 之后,monorepo 内部统一版本依赖只需要在使用 pnpm add 时带上 --save-catalog 和 --save-catalog-name 两个选项即可,很轻松地就共享了项目依赖的版本,同时在 pnpm-workspace.yaml 也能很明显地查看到项目依赖的情况。
对于 catalog 分类,我个人倾向于不是那么实用,只要项目能够共享的依赖版本,大可以直接使用 --save-catalog 保存到默认目录下即可,后续使用 pnpm add 也能快速在其他项目内部安装。
catalog 的唯一缺点就是在项目的 package.json 中定义时无法直接显示依赖版本,这里就推荐一个 antfu 的 vscode 插件 —— Catalog Lens. 能够在 vscode 中将 package.json 中使用 catalog: 的版本替换成具体对应的版本号。
