pnpm 工作区是一种用于管理多个包的依赖关系的特性,它通过高效的存储和链接机制,实现了在单一仓库中对多个项目的统一管理。
简单使用
创建一个使用 PNPM 包管理器及其工作区(workspace)功能的单仓库(mono repo)项目。相较于 Yarn 的工作区,PNPM 工作区的主要优势在于,公共包不会被提升到根目录,从而使所有工作区包完全隔离。
详细步骤解释及示例代码说明
- 初始化根项目
bash
mkdir my-monorepo
cd my-monorepo
pnpm init -y
这部分代码的作用是创建一个名为 my-monorepo
的新目录,接着进入该目录,最后使用 pnpm init -y
命令快速初始化一个 package.json
文件,-y
选项表示使用默认配置。
- 创建工作区配置文件
创建pnpm-workspace.yaml
文件,内容如下:
yaml
packages:
- 'packages/**'
这个配置文件表明,所有位于 packages
目录及其子目录下的包都属于这个 PNPM 工作区。
- 创建工作区包目录
bash
mkdir packages
cd packages
mkdir app shared
cd ..
这组命令创建了一个 packages
目录,然后在其中创建了 app
和 shared
两个子目录,分别用于存放不同的包,最后返回上一级目录。
- 初始化工作区包
对于packages/app
包:
bash
cd packages/app
pnpm init -y
npm pkg set name=@my-monorepo/app
cd ../..
先进入 packages/app
目录,使用 pnpm init -y
初始化该包的 package.json
文件,再使用 npm pkg set name=@my-monorepo/app
为这个包设置一个作用域名称,最后返回根目录。
对于 packages/shared
包:
bash
cd packages/shared
pnpm init -y
npm pkg set name=@my-monorepo/shared
cd ../..
操作与 packages/app
包类似,只是将包名设置为 @my-monorepo/shared
。
- 添加跨包依赖
bash
pnpm add --filter app shared
--filter
选项用于指定操作的目标包,这里表示在 app
包中添加 shared
包作为依赖。
- 添加工作区特定依赖
例如,为所有包添加 TypeScript 作为开发依赖:
bash
pnpm add --save-dev typescript -w
-w
选项表示将 TypeScript 安装到工作区的根目录,使得所有包都能使用它。
- 在根
package.json
中添加脚本
修改根目录下的package.json
文件,添加以下脚本:
json
{
"scripts": {
"dev": "pnpm --filter app dev",
"build": "pnpm --filter app build",
"test": "pnpm --filter app test",
"lint": "pnpm --filter app lint"
}
}
这些脚本允许你在根目录下通过 pnpm run dev
、pnpm run build
等命令来执行 app
包中的相应脚本。
- 目录结构
my-monorepo/
├── packages/
│ ├── app/
│ │ ├── package.json
│ │ └── src/
│ └── shared/
│ ├── package.json
│ └── src/
├── .gitignore
├── package.json
└── pnpm-workspace.yaml
展示了整个单仓库项目的目录结构,packages
目录下包含多个包,每个包有自己的 package.json
文件和源代码目录,根目录有根 package.json
和 pnpm-workspace.yaml
配置文件。
关键特性说明
-
依赖隔离
PNPM 工作区的每个包都有自己的node_modules
目录,公共包不会被提升到根目录,这意味着每个包的依赖是明确声明和独立管理的。 -
跨包管理
bash
# 为特定包安装依赖
pnpm add express --filter app
# 在特定包中运行脚本
pnpm run dev --filter app
# 构建所有包
pnpm run build -r
--filter
选项用于指定操作的目标包,-r
选项表示递归地对所有工作区包执行操作。
- 版本管理
使用changesets
进行版本管理:
bash
pnpm add --save-dev changesets
pnpm changeset init
changesets
可以帮助你管理工作区中各个包的版本号和发布信息。
与 Yarn 工作区的比较
特性 | PNPM 工作区 | Yarn 工作区 |
---|---|---|
依赖隔离 | 完全隔离 | 部分提升 |
存储效率 | 硬链接 | 缓存依赖 |
符号链接处理 | 原生支持 | 需要 npm link |
CLI 性能 | 操作更快 | 大型仓库中较慢 |
可以使用以下命令验证你的设置:
bash
pnpm list --depth=0
该命令会显示所有工作区包及其依赖,以确认它们是否被正确隔离。
底层原理
传统包管理与工作区包管理的区别
传统项目中的依赖管理
在传统的单个项目中,当你使用包管理器(如 npm、yarn 或 pnpm)安装依赖时,依赖会被安装到项目根目录下的 node_modules
文件夹中。例如,在一个简单的 Node.js 项目中,执行 pnpm install express
后,express
包及其所有依赖都会被下载并存储在项目根目录的 node_modules
里。
多项目(多包)场景下的问题
当你有多个相互关联的项目(在单仓库项目中就是多个包)时,如果每个项目都单独安装依赖,会造成大量的重复下载和存储,浪费磁盘空间。例如,项目 A 和项目 B 都依赖 lodash
库,如果各自安装,lodash
就会被下载两次。
为了解决这个问题,一些包管理器(如 Yarn)引入了工作区(workspace)功能,它会将公共依赖提升到根目录的 node_modules
中,多个项目可以共享这些依赖,避免重复下载。
PNPM 工作区的依赖隔离机制
每个包有自己的 node_modules
目录
在 PNPM 工作区中,每个包(如前面示例中的 app
包和 shared
包)都有自己独立的 node_modules
目录。这意味着每个包可以精确控制自己所依赖的包及其版本。
例如,app
包依赖 lodash@4.17.21
,而 shared
包依赖 lodash@4.17.20
,在 PNPM 工作区中,这两个不同版本的 lodash
可以分别安装在 app
包和 shared
包各自的 node_modules
中,不会相互影响。
公共包不提升到根目录
与 Yarn 工作区不同,PNPM 不会将公共包提升到根目录的 node_modules
中。即使多个包都依赖同一个版本的某个包,每个包仍然会在自己的 node_modules
中保留该依赖的副本。不过,PNPM 采用了硬链接(hard link)的方式来存储这些依赖,避免了磁盘空间的浪费。
例如,app
包和 shared
包都依赖 axios@0.21.1
,PNPM 会在 app
包和 shared
包的 node_modules
中都创建对 axios@0.21.1
的硬链接,而实际的文件内容只存储一份,这样既保证了每个包的独立性,又节省了磁盘空间。
明确声明和独立管理的好处
依赖明确声明
由于每个包都有自己的 package.json
文件,并且依赖都安装在自己的 node_modules
中,所以每个包的依赖关系都非常明确。你可以清楚地知道每个包具体依赖哪些包以及依赖的版本,这有助于避免因依赖冲突而导致的问题。
例如,在 app
包的 package.json
中,你可以明确看到它依赖的所有包及其版本:
json
{
"name": "@my-monorepo/app",
"dependencies": {
"@my-monorepo/shared": "1.0.0",
"express": "4.17.1"
}
}
独立管理
每个包的依赖可以独立更新和管理,不会影响其他包。如果 app
包需要升级 express
到新版本,你可以直接在 app
包的 package.json
中修改版本号,然后在 app
包目录下执行 pnpm install
来更新依赖,而不会对 shared
包产生影响。
这种独立性使得在单仓库项目中进行开发和维护更加灵活和可靠,尤其是在大型项目中,不同的团队或开发人员可以独立负责不同的包,而不用担心依赖冲突的问题。
在工作区根目录执行pnpm install
的逻辑
在 PNPM 工作区根目录执行 pnpm install
会触发一系列操作,这些操作有助于确保整个工作区的依赖项被正确安装和管理。
1. 解析 package.json
文件
PNPM 会首先读取工作区根目录下的 package.json
文件以及各个子包(位于 packages
目录下,具体根据 pnpm-workspace.yaml
配置)的 package.json
文件。这些文件中定义了每个包所需的依赖项及其版本范围。
2. 构建依赖图
根据读取到的 package.json
文件内容,PNPM 会构建一个完整的依赖图,以确定各个包之间的依赖关系以及所需依赖项的版本。这有助于解决依赖冲突,确保每个包都能获取到正确版本的依赖。
3. 安装根目录依赖
如果根目录的 package.json
中定义了依赖项,PNPM 会将这些依赖项安装到根目录的 node_modules
中。这些依赖通常是整个工作区共享的,例如开发工具、构建脚本等。
4. 安装子包依赖
对于工作区中的每个子包,PNPM 会根据其 package.json
文件中的依赖项列表,将这些依赖安装到各自的 node_modules
目录中。在这个过程中,PNPM 会利用硬链接(hard link)和符号链接(symbolic link)来避免重复安装相同的依赖项,从而节省磁盘空间。
5. 处理工作区内部依赖
如果工作区中的某个子包依赖于另一个子包,PNPM 会自动处理这种内部依赖关系。它会在依赖包的 node_modules
中创建一个符号链接,指向被依赖包的实际位置,确保子包之间的引用能够正常工作。
6. 更新 pnpm-lock.yaml
文件
安装完成后,PNPM 会更新根目录下的 pnpm-lock.yaml
文件。这个文件记录了所有已安装依赖项的确切版本和来源,确保在不同环境中安装的依赖项版本一致,从而保证项目的可重复性。
7. 执行 postinstall
脚本
如果 package.json
中定义了 postinstall
脚本,PNPM 会在安装完成后自动执行这些脚本。这些脚本通常用于执行一些额外的操作,如构建、编译或初始化项目。
示例
假设工作区结构如下:
my-monorepo/
├── packages/
│ ├── app/
│ │ ├── package.json
│ │ └── src/
│ └── shared/
│ ├── package.json
│ └── src/
├── package.json
└── pnpm-workspace.yaml
在根目录执行 pnpm install
后,PNPM 会根据 package.json
文件安装所有依赖项,并在每个子包的 node_modules
中创建相应的依赖结构。同时,根目录下的 pnpm-lock.yaml
文件会被更新,以反映当前的依赖状态。
通过这种方式,pnpm install
确保了整个工作区的依赖项被正确安装和管理,为项目的开发和构建提供了稳定的基础。