pnpm 通过「内容寻址存储 + 非扁平 node_modules + workspace 本地链接 」
同时解决了 磁盘浪费、幻影依赖、monorepo 复用 三个核心问题。
pnpm 核心设计思想
1. 存储分层
内容寻址存储 + 硬链接 + 软链接
-
~./pnpm-store/ 内容寻址存储
全局、跨项目
tsx~/.pnpm-store/ └── v3/ └── files/ └── <hash>...在用户主目录下(不是项目里)是跨项目的
是全机器唯一的 content-addressable store
以内容 hash 存储,是真正源文件所在地,确保了内容唯一性
-
.pnpm/ 硬链接
指向全局 store 文件的 硬链接reflink
以及描述依赖关系的 软链接结构symlink
-
node_module/ 软链接
在项目根目录下,为 Node / bundler 提供传统 node_modules 解析入口
本身是不存放文件内容的
2. 链接策略
tsx
registry 依赖
workspace 本地包
当遇到
tsx
{
"dependencies": {
"@repo/utils": "workspace:*"
}
}
时,不去 store 实现硬链接到项目 .pnpm
而是
-
解析阶段
tsx@repo/utils └── 来源:workspace 内的 packages/utils绕过 npm registry 、绕过版本下载、直接认定时本地包
-
链接阶段
tsxapps/web/node_modules/@repo/utils └── symlink → ../../packages/utils并不是像依赖去 store → .pnpm → node_modules
而是直接 symlink 到 workspace 包目录
(workspace包的依赖仍然是走上面的pnpm的三层)
优点
1. 解决了幻影依赖
在依赖配置中并没有声明 但是在源码中却使用了,这是因为声明的直接依赖依赖了这个间接依赖,所以就会拉下来
根因
计算机的文件结构是树结构,但依赖是图结构
早期 npm 是将图转为树(子文件夹),所以是没有幻影依赖的问题的,但是由于存在嵌套层次深、重复包耗散的问题,
所以 npm / yarn 后面对 node_modules 进行扁平化,但是这破坏了依赖边界,导致了幽灵依赖
幽灵依赖导致问题
-
版本问题
直接依赖升级导致间接依赖升级后API不兼容
-
依赖丢失问题
开发依赖包A依赖了包B
在开发后进行上传,上传后下拉方没有恢复开发依赖,只恢复了生产环境的依赖
解决办法 - pnpm
不论直接还是间接都放到统一仓库store(.pnpm)中
而项目中的 node_modules 和 npm 最开始的树形结构一样,但是存的内容不一样,pnpm中存的是 仓库store 对应内容的软链接(指针)
2. monorepo
可以对照着通过monorepo架构支持国际化学习
背景 - 现有问题
钱管家在进行跨国多地部署时,多端都需要很多依赖,搭建成本大,磁盘消耗多。为了复用多项目中都用到的部分
技术考量
主要考虑到了 pnpm 的两个核心特性
-
.pnpm 是硬链接的全局唯一的 ~./pnpm-store,确保了不会有重复的磁盘占用
-
workspace 支持 本地包 自动 link,且是 真实源码级别的 软链接symlink,而不是复制或模拟发布包
tsxpackages/ utils/ ui/ apps/ web/apps/web 里写:
tsx{ "dependencies": { "@repo/utils": "workspace:*" } }pnpm 会直接 软链接本地包 ,不走 registry 、不发 npm 也能用
workspace 并不是共享 node_module , 每个包仍然有自己的依赖边界,但是底层 store 共享,本地包通过 workspace link
jsx
my-monorepo/
├── package.json
├── pnpm-workspace.yaml
├── packages/
│ ├── utils/
│ │ └── package.json
│ ├── ui/
│ │ └── package.json
├── apps/
│ ├── web/
│ │ └── package.json
│ ├── admin/
│ │ └── package.json
└── tsconfig.json
packages/ 放所有可复用模块(组件库、工具库、hooks等)
apps/ 放最终应用(web/PC端、H5、Node服务等
跨包命令
tsx
pnpm -r build # 所有包 build
pnpm -F web dev # 只跑 web
pnpm -F "@repo/*" lint
步骤
- 配置 pnpm-workspace.yaml
jsx
packages:
- "packages/*"
- "apps/*"
pnpm 会自动扫描这些目录,把每个子目录识别为 workspace 包
- 根packages.json(管理脚本)
package.json 通常无需 dependencies ,只写 script
jsx
{
"name": "my-monorepo",
"private": true,
"scripts": {
"dev": "pnpm -r --parallel dev",
"build": "pnpm -r build",
"lint": "pnpm -r lint"
}
}
- 创建子包(例如utils)
在 packages/utils/package.json
jsx
{
"name": "@my/utils",
"version": "1.0.0",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts"
}
}
- 让 apps 引用 workspace 包
如果 apps/web 想用utils
在 apps/web/package.json
jsx
{
"name": "web",
"dependencies": {
"@my/utils": "workspace:*"
}
}
workspace:* 表示依赖来自 workspace 不是 npm registry
- 安装依赖
jsx
pnpm install
3. 速度快
并发拉包
命中缓存直接跳过下载
4. 锁文件lockfile明确
pnpm-lock.yaml 是 全局依赖图快照,不是 node_modules 的结构快照,因此更稳定、跨平台一致
5. CI 友好
- pnpm-store 可缓存
- pnpm-lock.yaml 稳定
- node_modules 结构可预测
npm 升级为 pnpm
tsx
rm -rf node_modules package-lock.json .npmrc
.npmrc
tsx
# 严格模式,防止幻影依赖
node-linker=hoisted
shamefully-hoist=false
strict-peer-dependencies=false
# 使用国内镜像加速
registry=https://registry.npmmirror.com/
pnpm-workspace.yaml
tsx
packages:
- '.'
tsx
{
"filePath": "/Users/a86198/front-sandbox-scaffold/package.json",
"oldString": " \"type\": \"module\",\n \"scripts\": {\n \"start\": \"npm run dev\",\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\"\n },",
"newString": " \"type\": \"module\",\n \"scripts\": {\n \"start\": \"pnpm run dev\",\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\"\n },"
}
tsx
"start": "pnpm run dev",
/*
没装 pnpm 的话得先装
tsx
npm install -g pnpm
*/
tsx
pnpm install