pnpm install 全流程解读(Monorepo + 子包级处理)
在现代前端 monorepo 中,pnpm install 看似一句命令,背后却有一套精密的 全局视图 + 子包依赖管理 + 扁平化安装策略。以下从根目录到每个子包的完整流程拆解。
假设项目结构如下:
pnpm-workspace.yaml
packages/
ui/
apps/
web/
api/
执行命令:
pnpm install
整个流程可拆解为 八大阶段:
阶段 0:初始化 & Workspace 扫描
目标:建立 monorepo 全局视图,识别所有子包。
根目录处理:
-
读取
pnpm-workspace.yaml配置:packages: - packages/* - apps/*- 标识哪些目录属于 workspace。
-
使用 Glob 展开匹配目录:
-
packages/*→packages/ui -
apps/*→apps/web,apps/api
-
-
遍历每个目录:
- 检查是否存在
package.json
- 检查是否存在
-
构建 Package Map(全局内存视图):
{ "@my-org/ui": { dir: "packages/ui", version: "1.0.0" }, "@my-org/web": { dir: "apps/web", version: "1.0.0" }, "@my-org/api": { dir: "apps/api", version: "1.0.0" } }
子包处理:
- 此阶段子包仅提供
package.json信息 - 不做实际安装,只被 pnpm 作为依赖源解析
比喻:Package Map 就像 monorepo 的楼层导览图,每个子包办公室的位置和编号都在上面标清楚。
阶段 1:解析子包依赖
目标:明确每个依赖的来源与版本,生成解析计划。
根目录处理:
-
遍历所有子包
package.json -
识别依赖协议:
-
workspace:→ 本地 workspace 包 -
catalog:→ catalog 管理的统一版本 - 普通 semver → registry 第三方依赖
-
子包处理(以 apps/web 为例):
{
"dependencies": {
"@my-org/ui": "workspace:*",
"react": "catalog:",
"axios": "^1.6.0"
}
}
-
workspace 依赖:
- 查 Package Map 找本地路径
- 标记为 link 本地
-
catalog 依赖:
- 查 catalog 表确定版本,如
"^18.2.0"
- 查 catalog 表确定版本,如
-
registry 依赖:
- 标记为需要缓存或下载
输出:每个子包生成 依赖解析计划:
apps/web:
[
{ name: "@my-org/ui", type: "workspace", dir: "packages/ui", version: "1.0.0" },
{ name: "react", type: "catalog", version: "^18.2.0" },
{ name: "axios", type: "registry", version: "^1.6.0" }
]
比喻:每个子包做自己的采购清单,明确“本地拿 / catalog 拿 / registry 拿”。
阶段 2:版本冲突解决 & 扁平化优化
目标:保证依赖版本一致,尽量共享,减少重复安装。
根目录处理:
-
构建 全局依赖图:
- 整合所有子包解析计划
- 记录依赖来源、版本、子包依赖关系
-
版本冲突解决:
- 相同依赖不同版本 → 尝试单一兼容版本
- 不兼容版本 → 子包隔离安装
-
扁平化优化:
- 相同版本依赖只存一份在
.pnpm - 子包 node_modules 通过 symlink 指向共享位置
- 相同版本依赖只存一份在
-
生成最终安装计划
子包处理:
-
每个子包根据依赖解析计划和全局优化结果生成自己的 node_modules 结构:
- workspace → symlink 本地包
- catalog / registry → symlink 全局缓存
-
子包内部仍未写物理文件,只是确定了“依赖最终放哪、版本是多少”
比喻:像仓库协调采购:
- 阶段 1 → 每个子包列清单
- 阶段 2 → 决定哪些物品共享、哪些单独存放,并画好指向箭头(symlink)
阶段 3:下载 & 缓存管理
目标:把 registry / catalog 依赖放到本地缓存。
根目录处理:
- 遍历全局依赖图
- 检查全局缓存
~/.pnpm-store - 缓存没有 → 从 npm registry 下载
- 下载完成 → 写入缓存
子包处理:
- 子包 node_modules 不直接存文件
- symlink 指向全局缓存 / workspace 本地包
比喻:仓库先查库存,没货再买,子包只挂指向箭头。
阶段 4:构建子包 node_modules
目标:把依赖落地到每个子包。
操作:
-
遍历每个子包安装计划:
- workspace → link 本地包
- catalog / registry → link 缓存目录
-
确保扁平化结构、依赖树完整
子包内部示例:
apps/web/node_modules/
@my-org/ui -> ../../packages/ui (symlink)
react -> ../../.pnpm/react@18.2.0/node_modules/react
axios -> ../../.pnpm/axios@1.6.0/node_modules/axios
比喻:像把共享仓库的物品放到办公室桌上,通过 symlink 指向真实物品。
阶段 5:生命周期脚本执行
目标:执行子包初始化与构建脚本。
- preinstall → 安装前准备
- install → 内部安装行为
- postinstall → 构建/生成文件/链接工具
子包处理:
- 每个子包可执行自己的 lifecycle scripts,确保安装完成即可运行
阶段 6:lockfile 更新
目标:记录依赖最终版本,保证一致性。
-
根目录生成/更新
pnpm-lock.yaml -
记录:
- 包名
- 版本
- 来源
- 校验和
-
子包无需单独操作,但 lockfile 决定未来安装一致性
阶段 7:最终检查 & 完成安装
- 校验依赖树完整性
- 确保 symlink、缓存、node_modules 正确
- 每个子包可直接
import/require所有依赖
全流程总结
pnpm install (根目录)
├─> 初始化 & workspace 扫描 → 构建 Package Map
│ └─> 子包提供 package.json
├─> 遍历子包 → 解析依赖 (workspace / catalog / registry)
│ └─> 每个子包生成依赖解析计划
├─> 版本冲突解决 & 扁平化优化 → 构建全局依赖图
│ └─> 子包生成最终 node_modules 安装计划
├─> 下载缺失依赖 → 写入全局缓存
│ └─> 子包 symlink 指向缓存 / workspace
├─> 构建子包 node_modules → symlink workspace + catalog/registry
├─> 执行生命周期脚本 (preinstall / install / postinstall)
│ └─> 子包完成初始化和构建
├─> 更新 lockfile → 记录最终依赖版本与来源
└─> 最终检查 → 子包依赖完整,安装完成
核心理解
- Package Map → workspace 全局视图,子包位置 + 版本
- workspace 协议 → 强制本地依赖,保证 link
- catalog 协议 → 统一第三方依赖版本
- node_modules 构建 → 扁平化 + symlink,每个子包看到完整依赖
- 缓存管理 → 下载一次,全局复用
- lockfile & lifecycle scripts → 保证安装一致性 + 初始化完成
一句话总结:
pnpm install= 根目录扫描 → 协议解析 → 版本冲突优化 → 缓存下载 → 子包 node_modules 构建 → 生命周期脚本执行 → lockfile 更新 → 子包可直接使用。