pnpm 工作区是一种用于管理多个包的依赖关系的特性,它通过高效的存储和链接机制,实现了在单一仓库中对多个项目的统一管理。

简单使用

创建一个使用 PNPM 包管理器及其工作区(workspace)功能的单仓库(mono repo)项目。相较于 Yarn 的工作区,PNPM 工作区的主要优势在于,公共包不会被提升到根目录,从而使所有工作区包完全隔离。

详细步骤解释及示例代码说明

  1. 初始化根项目
bash 复制代码
mkdir my-monorepo
cd my-monorepo
pnpm init -y

这部分代码的作用是创建一个名为 my-monorepo 的新目录,接着进入该目录,最后使用 pnpm init -y 命令快速初始化一个 package.json 文件,-y 选项表示使用默认配置。

  1. 创建工作区配置文件
    创建 pnpm-workspace.yaml 文件,内容如下:
yaml 复制代码
packages:
  - 'packages/**'

这个配置文件表明,所有位于 packages 目录及其子目录下的包都属于这个 PNPM 工作区。

  1. 创建工作区包目录
bash 复制代码
mkdir packages
cd packages
mkdir app shared
cd ..

这组命令创建了一个 packages 目录,然后在其中创建了 appshared 两个子目录,分别用于存放不同的包,最后返回上一级目录。

  1. 初始化工作区包
    对于 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

  1. 添加跨包依赖
bash 复制代码
pnpm add --filter app shared

--filter 选项用于指定操作的目标包,这里表示在 app 包中添加 shared 包作为依赖。

  1. 添加工作区特定依赖
    例如,为所有包添加 TypeScript 作为开发依赖:
bash 复制代码
pnpm add --save-dev typescript -w

-w 选项表示将 TypeScript 安装到工作区的根目录,使得所有包都能使用它。

  1. 在根 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 devpnpm run build 等命令来执行 app 包中的相应脚本。

  1. 目录结构
复制代码
my-monorepo/
├── packages/
│   ├── app/
│   │   ├── package.json
│   │   └── src/
│   └── shared/
│       ├── package.json
│       └── src/
├── .gitignore
├── package.json
└── pnpm-workspace.yaml

展示了整个单仓库项目的目录结构,packages 目录下包含多个包,每个包有自己的 package.json 文件和源代码目录,根目录有根 package.jsonpnpm-workspace.yaml 配置文件。

关键特性说明

  1. 依赖隔离
    PNPM 工作区的每个包都有自己的 node_modules 目录,公共包不会被提升到根目录,这意味着每个包的依赖是明确声明和独立管理的。

  2. 跨包管理

bash 复制代码
# 为特定包安装依赖
pnpm add express --filter app

# 在特定包中运行脚本
pnpm run dev --filter app

# 构建所有包
pnpm run build -r

--filter 选项用于指定操作的目标包,-r 选项表示递归地对所有工作区包执行操作。

  1. 版本管理
    使用 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 确保了整个工作区的依赖项被正确安装和管理,为项目的开发和构建提供了稳定的基础。