普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月25日首页

pnpm install 全流程解读(Monorepo + 子包级处理)

作者 胡一闻
2026年1月25日 17:26

在现代前端 monorepo 中,pnpm install 看似一句命令,背后却有一套精密的 全局视图 + 子包依赖管理 + 扁平化安装策略。以下从根目录到每个子包的完整流程拆解。

假设项目结构如下:

pnpm-workspace.yaml
packages/
  ui/
apps/
  web/
  api/

执行命令:

pnpm install

整个流程可拆解为 八大阶段


阶段 0:初始化 & Workspace 扫描

目标:建立 monorepo 全局视图,识别所有子包。

根目录处理:

  1. 读取 pnpm-workspace.yaml 配置:

    packages:
      - packages/*
      - apps/*
    
    • 标识哪些目录属于 workspace。
  2. 使用 Glob 展开匹配目录:

    • packages/*packages/ui
    • apps/*apps/web, apps/api
  3. 遍历每个目录:

    • 检查是否存在 package.json
  4. 构建 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"
  }
}
  1. workspace 依赖:

    • 查 Package Map 找本地路径
    • 标记为 link 本地
  2. catalog 依赖:

    • 查 catalog 表确定版本,如 "^18.2.0"
  3. 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:版本冲突解决 & 扁平化优化

目标:保证依赖版本一致,尽量共享,减少重复安装。

根目录处理:

  1. 构建 全局依赖图

    • 整合所有子包解析计划
    • 记录依赖来源、版本、子包依赖关系
  2. 版本冲突解决:

    • 相同依赖不同版本 → 尝试单一兼容版本
    • 不兼容版本 → 子包隔离安装
  3. 扁平化优化:

    • 相同版本依赖只存一份在 .pnpm
    • 子包 node_modules 通过 symlink 指向共享位置
  4. 生成最终安装计划

子包处理

  • 每个子包根据依赖解析计划和全局优化结果生成自己的 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 → 记录最终依赖版本与来源
 └─> 最终检查 → 子包依赖完整,安装完成

核心理解

  1. Package Map → workspace 全局视图,子包位置 + 版本
  2. workspace 协议 → 强制本地依赖,保证 link
  3. catalog 协议 → 统一第三方依赖版本
  4. node_modules 构建 → 扁平化 + symlink,每个子包看到完整依赖
  5. 缓存管理 → 下载一次,全局复用
  6. lockfile & lifecycle scripts → 保证安装一致性 + 初始化完成

一句话总结
pnpm install = 根目录扫描 → 协议解析 → 版本冲突优化 → 缓存下载 → 子包 node_modules 构建 → 生命周期脚本执行 → lockfile 更新 → 子包可直接使用。

小白理解Catalog 协议

作者 胡一闻
2026年1月25日 16:37

在 monorepo 项目里,你可能见过这样的写法:

{
  "dependencies": {
    "react": "catalog:"
  }
}

很多小伙伴第一眼都会问:

  • catalog: 是什么?
  • 为什么不直接写版本号?
  • 它和 workspace: 有什么区别?

这篇文章会帮你一次性搞懂 Catalog 协议的设计初衷、使用方法和原理


一、Catalog 协议的核心结论

Catalog 协议是 pnpm 提供的一种“集中管理依赖版本号”的机制
用来统一管理 monorepo 中的第三方依赖版本,解决版本分散、升级难的问题。

用一句话总结 Catalog 的定位:

  • workspace: 决定“这个依赖从哪来”(来源)
  • catalog: 决定“这个依赖用哪个版本”(版本)

二、为什么要有 Catalog?

在大型 monorepo 中,通常有很多包依赖同一个库,比如 reactlodash

// apps/web
"react": "^18.2.0"

// apps/admin
"react": "^18.2.0"

// packages/ui
"react": "^18.1.0" // 不小心写错了

问题:

  • 多处重复写版本号,维护成本高
  • 很容易出现版本不一致
  • 升级某个依赖,需要改很多包
  • 新增包不知道该用哪个版本

Catalog 的设计初衷就是解决这些问题——
让版本号集中管理,子包只负责声明“我要用这个依赖”。


三、Catalog 的写法

1️⃣ 在 workspace 根部声明

# pnpm-workspace.yaml
packages:
  - apps/*
  - packages/*

catalog:
  react: ^18.2.0
  react-dom: ^18.2.0
  axios: ^1.6.0
  • 所有依赖的“版本来源”都在这里集中声明
  • 子包无需关心版本号

2️⃣ 在子包里使用

{
  "dependencies": {
    "react": "catalog:",
    "axios": "catalog:"
  }
}

特点:

  • 不写具体版本号
  • pnpm 会自动去 workspace catalog 中查找对应版本
  • 默认使用依赖名作为 key(也可以写成 catalog:react 显式指定)

四、Catalog 的原理(通俗理解)

可以把 Catalog 想象成一个 “依赖版本宏”

  1. pnpm 解析依赖时,看到 "catalog:"
  2. 去 workspace 的 catalog 表里查对应依赖的版本号
  3. 展开成普通 semver,比如 "^18.2.0"
  4. 后续安装流程就像普通依赖一样

⚠️ Catalog 只解决版本号,不影响依赖来源
本地包仍然用 workspace 协议管理


五、为什么写成 catalog:

  • package.json 中依赖值只能是字符串
  • pnpm 内部通过 协议前缀: 来识别依赖类型
  • 类似 workspace:*file:git:
  • catalog:延迟解析版本号的协议,不会破坏 JSON 规范

简化写法:

"react": "catalog:"   // 默认 key = react

显式写法:

"react": "catalog:react"

两者效果相同。


六、Catalog 与 workspace 的区别

维度 workspace catalog
解决的问题 依赖从哪来 版本由谁定
主要对象 本地包 第三方包
安装行为 强制使用本地包 展开成指定版本号
发布行为 workspace:* 替换成真实版本号 展开后就是普通依赖
是否指向本地路径

一句话总结:

workspace 决定“用谁”,catalog 决定“用哪个版本”。


七、什么时候使用 Catalog?

✅ 适合:

  • 多个包共享大量第三方依赖
  • 希望依赖版本统一,易于升级
  • 中大型团队协作的 monorepo

❌ 不太需要:

  • 单包项目
  • 实验性仓库
  • 每个包技术栈完全不同

八、完整示例

# pnpm-workspace.yaml
packages:
  - apps/*
  - packages/*

catalog:
  react: ^18.2.0
  axios: ^1.6.0
// apps/web/package.json
{
  "dependencies": {
    "react": "catalog:",
    "axios": "catalog:"
  }
}
  • pnpm 安装时,会把 catalog: 展开成 "^18.2.0"
  • 子包无需关心版本号,只管使用
  • 保证整个 workspace 的版本一致

九、总结

Catalog 协议不是安装机制,而是版本管理机制。
它在 monorepo 中提供了一个“单一版本源头”,
子包只负责声明依赖,版本号由根目录统一控制。
与 workspace 协议配合使用,可以实现来源锁定 + 版本统一的高效管理。

小白理解workspace 协议

作者 胡一闻
2026年1月25日 16:29

在使用 pnpm 的 monorepo 项目时,你可能见过这样的写法:

{
  "dependencies": {
    "@my-org/ui": "workspace:*"
  }
}

很多初学者都会疑惑:

  • workspace: 是什么?
  • 不写它行不行?
  • 它和版本号有什么关系?

这篇文章会用最直观、最少前置知识的方式,带你一次搞懂 pnpm workspace 协议的原理和使用


一、先说结论(一句话版本)

workspace: 协议的作用是:
强制 pnpm 使用当前 workspace 里的本地包,而不是去 npm 仓库下载。

它解决的不是“版本问题”,而是一个更基础的问题:

👉 这个依赖“从哪来”?


二、没有 workspace: 会发生什么?

先看一个最常见的 monorepo 结构:

packages/
  ui        (name: @my-org/ui)
apps/
  web

apps/web/package.json 中写:

{
  "dependencies": {
    "@my-org/ui": "^1.0.0"
  }
}

这时 pnpm 的行为是:

  1. 在 workspace 中查找 @my-org/ui
  2. 如果本地存在,并且版本满足 ^1.0.0
  3. 👉 默认使用本地包

也就是说:

即使你不写 workspace:,pnpm 也“通常”会用本地包

那问题来了——
既然这样,workspace: 有什么用?


三、workspace: 到底解决了什么问题?

1️⃣ 明确表达“这是一个本地依赖”

当你写:

{
  "@my-org/ui": "workspace:*"
}

你是在向 pnpm 明确声明一件事:

这个依赖必须来自当前 workspace,本地没有就直接报错。

它的意义不是“让 pnpm 能用本地包”,
而是 防止 pnpm 用错包


2️⃣ 防止“误用远程同名包”

假设有一天:

  • 你的 workspace 里还没有 @my-org/ui
  • 但 npm 仓库里刚好有一个同名包

如果你写的是:

"@my-org/ui": "^1.0.0"

pnpm 会:

👉 去 npm 仓库下载那个包

但如果你写的是:

"@my-org/ui": "workspace:*"

pnpm 会直接:

❌ 报错:workspace 中不存在该包

👉 这就是 workspace: 的安全价值


四、workspace 协议的“真实原理”(人话版)

pnpm 在安装依赖时,会先把每个依赖解析成一种“类型”:

  • 普通 semver(^1.0.0
  • workspace 协议
  • file / git / link 等

当 pnpm 看到:

"@my-org/ui": "workspace:*"

它内部会把这个依赖标记为:

“只能从 workspace Package Map 中解析”

接下来的规则非常简单:

  • ✅ workspace 里有 → 用本地包
  • ❌ workspace 里没有 → 直接失败
  • 🚫 不会查 npm registry

所以可以这样理解:

workspace: 是一种“来源锁定协议”


五、workspace:* / workspace:^ / workspace:~ 有什么区别?

这是很多人第一次看到会懵的地方。

结论先给出来:

它们在“安装阶段没有区别”,
区别只体现在“发布阶段”。


1️⃣ 安装阶段(最重要的阶段)

无论你写:

workspace:*
workspace:^
workspace:~

pnpm 都会:

  • 忽略版本判断
  • 直接使用本地包

👉 安装行为完全一致


2️⃣ 发布阶段(publish 到 npm 时)

workspace 协议 不会被发布,而是会被替换:

假设本地包版本是 1.2.3

写法 发布后
workspace:* ^1.2.3
workspace:^ ^1.2.3
workspace:~ ~1.2.3

所以:

workspace 后面的符号,
本质是在帮你决定将来别人怎么依赖你


六、什么时候“应该”用 workspace:?

✅ 推荐使用的场景

  • monorepo 内部包互相依赖
  • 希望明确表达“这是内部依赖”
  • 不希望误用远程同名包
  • 希望发布时自动生成合理版本号

👉 团队项目 / 长期维护项目:强烈推荐


❌ 可以不写的场景

  • 个人实验项目
  • 确定不会 publish
  • 对依赖来源不敏感

👉 即使不写,pnpm 多数时候也能正常工作
不写等于放弃了一层安全约束


七、一个完整示例

packages/ui/package.json

{
  "name": "@my-org/ui",
  "version": "1.0.0"
}

apps/web/package.json

{
  "dependencies": {
    "@my-org/ui": "workspace:*"
  }
}

pnpm 的理解是:

@my-org/ui 必须来自当前 workspace,
安装时用本地包,
发布时替换成真实版本号。


八、总结

workspace: 协议不是版本控制工具,而是依赖来源控制工具。
它的作用是明确告诉 pnpm:
“这个依赖只能来自当前 workspace,而不是外部仓库。”

pnpm+pnpm-workspace怎么关联本地包?

作者 胡一闻
2026年1月25日 15:55

一、最核心的问题先回答

为什么我只在 pnpm-workspace.yaml 里写了 apps/*
pnpm 就能知道 apps/webapps/api 这些包,并把它们用起来?

答案只有一句话:

因为 pnpm 在启动时,
先用 Glob 规则找目录 → 再识别哪些是真正的包 → 在内存里记住它们的 name 和路径


二、什么是 Glob?(一定要先懂这个)

1️⃣ Glob 是什么

Glob 是一种“用模式匹配文件路径”的规则

你每天其实都在用,比如:

ls *.js
ls apps/*

这里的 * 就是 Glob。


2️⃣ Glob 的“全称”是啥?

  • 没有严格官方全称
  • 约定俗成理解为:Global Pattern Matching
  • 起源于 Unix Shell

👉 它不是 pnpm 发明的,是整个操作系统层面的东西。


3️⃣ Glob 能干什么?

Glob 只做一件事

👉 在磁盘上找出“路径长得像”的文件或目录

⚠️ 重要:

  • Glob 不懂什么是包
  • Glob 不看 package.json
  • 它只认路径形状

三、apps/* 到底是什么意思?

packages:
  - apps/*

这句话的真实含义是:

“请在项目根目录下,
找出所有路径形状像 apps/某个名字 的目录。”

比如磁盘上有:

apps/web
apps/api
apps/docs

Glob 匹配结果就是这三个。


四、pnpm 是怎么一步步工作的?(重点)

第一步:pnpm 判断是不是 Workspace

pnpm 启动时先看:

有没有 pnpm-workspace.yaml

  • 有 → Workspace 模式
  • 没有 → 单包模式

⚠️ pnpm 只认这个文件


第二步:Glob 展开(找目录)

pnpm 读取:

packages:
  - apps/*

然后:

  • 使用 Glob 规则
  • 遍历文件系统
  • 找到所有匹配的目录:
apps/web
apps/api
apps/docs

⚠️ 此时 pnpm 还不知道谁是包


第三步:识别“真正的包”

pnpm 接下来会逐个检查:

  • apps/web → 有 package.json
  • apps/api → 有 package.json
  • apps/docs → 没有 ❌

只有package.json 的目录才算 workspace 包。


第四步:建立 Package Map(最关键)

pnpm 会读取每个包的 package.json 里的:

{
  "name": "@my-org/web",
  "version": "1.0.0"
}

然后在内存中建立一张表

@my-org/web  -> apps/web
@my-org/api  -> apps/api
@my-org/ui   -> packages/ui

👉 这一步非常重要:

  • pnpm 认的是 name
  • 不是目录名
  • 不是路径

五、pnpm 是什么时候把包“连起来”的?

❌ 不是扫描时

✅ 是安装依赖时

比如 apps/web/package.json

{
  "dependencies": {
    "@my-org/ui": "^1.0.0"
  }
}

pnpm 在安装时会想:

  1. 我要找 @my-org/ui
  2. Workspace 里有没有同名包?
  3. 有 → 用本地的
  4. 没有 → 去 npm 仓库下载

👉 所谓“连起来”,本质是:

把依赖指向本地 workspace 包,而不是远程包


六、如果没有 pnpm-workspace.yaml 会怎样?

pnpm 会:

  • ❌ 不扫描其他目录
  • ❌ 不建立包映射表
  • ❌ 不知道本地还有同名包

结果就是:

即使你本地有 packages/ui
pnpm 也会去 npm 仓库下载一个同名包。


七、mac 上怎么自己“看到” Glob 在干嘛?

macOS 默认用的是 zsh,它天生支持 Glob。

你可以直接在终端试:

# 看 Glob 匹配了哪些目录
ls apps/*

再试:

# 看哪些是真正的包
ls apps/*/package.json

👉 这两步,和 pnpm 内部做的事情几乎一模一样。


八、最容易踩的坑(小白必看)

❌ 错误写法

packages:
  - apps

只会尝试:

apps/package.json

✅ 正确写法

packages:
  - apps/*

九、终极一句话总结(背下来就够了)

Glob 只是用来找目录的规则。
pnpm 用 Glob 找到目录后,再通过 package.json 判断哪些是包,
并把它们的 name 和路径记在内存里。
真正把包“连起来”的,是后面的依赖解析,而不是 Glob 本身。


pnpm Workspace 全流程图

┌────────────────────────────┐
│ 你运行 pnpm install        │
└─────────────┬──────────────┘
              │
              ▼
┌────────────────────────────┐
│ ① 是否存在 pnpm-workspace   │
│    .yaml ?                 │
└─────────────┬──────────────┘
      有      │        没有
      ▼       │         ▼
┌──────────────────┐   ┌──────────────────┐
│ Workspace 模式    │   │ 单包模式          │
└────────┬─────────┘   │(不扫描别的包)     │
         │             └──────────────────┘
         ▼
┌────────────────────────────┐
│ ② 读取 pnpm-workspace.yaml │
│    packages:               │
│    - apps/*                │
│    - packages/*            │
└─────────────┬──────────────┘
              ▼
┌────────────────────────────┐
│ ③ Glob 展开(找目录)        │
│ apps/* →                   │
│   apps/web                 │
│   apps/api                 │
│   apps/docs                │
└─────────────┬──────────────┘
              ▼
┌────────────────────────────┐
│ ④ 判断是否是包              │
│   有没有 package.json ?     │
│   web  ✅                  │
│   api  ✅                  │
│   docs ❌                  │
└─────────────┬──────────────┘
              ▼
┌────────────────────────────┐
│ ⑤ 读取 package.json        │
│   name / version           │
└─────────────┬──────────────┘
              ▼
┌────────────────────────────┐
│ ⑥ 建立 Package Map(内存)   │
│   @my-org/web → apps/web   │
│   @my-org/api → apps/api   │
└─────────────┬──────────────┘
              ▼
┌────────────────────────────┐
│ ⑦ 依赖解析(install 阶段)   │
│ web 依赖 @my-org/ui ?       │
│ → workspace 里有吗?         │
│ → 有 → 用本地包              │
│ → 没有 → 去 npm 仓库         │
└────────────────────────────┘

❌
❌