Files
Qiniu/technical/mac-dev-env-setup/node-nvm-pnpm.md
T

209 lines
6.6 KiB
Markdown
Raw Normal View History

2026-07-01 23:09:21 +08:00
---
tags: [nodejs, nvm, pnpm, dev-env]
create time: 2026-07-01 00:00
---
# Node.js + nvm + pnpm 详解
## 概述
Node.js 开发环境中版本管理和包管理的核心链路:nvm 控制运行时版本、Corepack 提供标准化的包管理器入口、pnpm 以高效磁盘模型取代传统的 flat install。三者配合形成完整的 JS/TS 工程基础。
## 核心概念
### nvm 版本管理机制
nvm(Node Version Manager)的核心设计是通过 shell 函数劫持 `node` / `npm` / `npx` 命令:
```bash
# nvm 工作原理示意
which node
# → $(nvm_alias)/versions/node/*/bin/node ← 实际是符号链接
# ↑ 由 nvm 动态切换
```
| 操作 | 命令 | 说明 |
|------|------|------|
| 安装 LTS | `nvm install --lts` | 自动检测最新 Long Term Support 版本 |
| 切换版本 | `nvm use <version>` | 修改 `.nvmrc` 或版本号 |
| 查看已装 | `nvm ls` | 列出版本树 |
| 设置默认 | `nvm alias default 20` | 每次打开终端自动激活 |
| 全局包复用 | `--reinstall-packages-from=<version>` | 切换时迁移全局包 |
```bash
# 在项目根目录创建 .nvmrc
echo "20" > .nvmrc
# nvm use 不加版本号时自动读 .nvmrc
cd my-project && nvm use
# → Found '/path/to/my-project/.nvmrc' with version <20>
# → Now using Node.js v20.18.0
```
> [!tip] nvm 与 corepack 的交互
> `corepack prepare pnpm@latest --activate` 安装的是 `corepack` 自己,nvm 切换 Node 版本后,corepack 依然存在——因为它是随 Node.js bundled 的(Node ≥ 16.13)。这意味着 **不需要** 在切换 Node 版本后重新安装 corepack。
### Corepack 的作用
Corepack 是 Node.js 官方提供的零开销包管理器调度器,从 Node v16.13 起内置:
```bash
# Corepack 支持的包管理器
corepack enable # 注册 pnpm / yarn / npm 的 shim 入口
corepack prepare pnpm@latest --activate # 锁定 pnpm 版本
# 查看 corepack 管理的版本
corepack --version
corepack --enable
```
核心思想:**让团队统一使用包管理器版本,而非各自在全局安装不同版本的 npm/yarn/pnpm。**
```bash
# 项目中可指定 pnpm 版本
echo "pnpm@8.15.0" > .npmrc # Corepack 读取
# 或直接通过 package.json
# (package.json 中添加 "packageManager": "pnpm@8.15.0")
```
CI 跑 `corepack enable` 即可自动下载指定版本,无需额外安装步骤。
### pnpm 的存储模型
pnpm 最核心的创新在于 **硬链接 + 内容寻址存储**,解决了 npm/yarn 的两个经典问题:
1. **磁盘浪费**:npm 在每个 node_modules 下复制依赖副本
2. **隐形依赖**:flat tree 结构中子包能意外访问父包的依赖
```
传统 npm 安装(flat tree,symlink 树)
project-A/node_modules/
├── lodash/ ← symlink -> project-A/node_modules/lodash
├── express/ ← symlink -> project-A/node_modules/express
│ └── node_modules/
│ └── qs/ ← 可能被 project-B 重复安装
pnpm 安装(strict 模式)
pnpm-store/
├── .stores/v8/
│ └── abc123/ ← 内容哈希索引,全局唯一
│ ├── lodash.tgz
│ └── express.tgz
project-A/node_modules/
├── lodash/ ← hard link (硬链接) → .stores/v8/abc123/lodash.tgz
├── express/ ← hard link → .stores/v8/abc123/express.tgz
└── .pnpm/
└── express@4.x/ ← nested symlink 满足 express 自身的 require('qs')
```
**硬链接的优势**:同样的文件在磁盘上只存一份,`node_modules` 中的硬链接不占用额外空间;而 symlink 在 Windows 上需要管理员权限才能创建。
> [!warning] Windows 上的 pnpm
> Windows 10+ 支持硬链接,但默认可能需要开启"开发者模式"。如果遇到 permission denied,以管理员身份运行终端,或在系统设置中启用开发者模式。
### pnpm workspace(Monorepo 支持)
```json
{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*"]
}
```
workspace 模式下,同仓库内的包互相引用时直接使用硬链接,无需 publish:
```bash
cd packages/shared && pnpm add ../libs/core
# 等价于在 shared 的 package.json 中写了 "core": "link:../libs/core"
```
## 代码示例
### 项目级 .npmrc 推荐配置
```ini
# .npmrc
auto-install-peers=true
side-effects-cache=true # 缓存 noSideEffects=true 的包,加快安装
shamefully-hoist=false # 禁止非 hoist 提升到顶层
strict-peer-dependencies=true # peerDependency 缺失时报错而非警告
```
### 快速诊断 node_modules 大小
```bash
# macOS/Linux
du -sh node_modules/
# 找出最大的前 10 个包
pnpm why --recursive | head -20
```
## 常见陷阱与最佳实践
### 1. nvm 与 Node.js 全局 CLI 丢失
切换 Node 版本后,之前在全局安装的 CLI 工具(如 `typescript`、`eslint`)不再可用,因为每个 Node 版本有独立的全局目录:
```bash
# 切换时保留全局包(仅同大版本间安全)
nvm use 20 --reinstall-packages-from=18
# 或者通用做法:CLI 工具放在工作区 root 的 devDependencies 中
pnpm add -D typescript eslint
npx tsc --version # 始终能用
```
> [!tip] 黄金法则:CLI 工具不进 global,进 devDependencies
> 所有团队工具的版本应该锁在 `package.json` 中,通过 `npx <tool>` 调用。这是避免"我的能跑你不行"问题的最根本策略。
### 2. Corepack 版本冲突
如果你的团队中有成员安装了独立的全局 pnpm,会与 Corepack 冲突:
```bash
# 检查当前 pnpm 来源
which pnpm
# → 应该是:$(corepack_dir)/pnpm → .corepack/pnpm → 目标二进制
# 卸载独立安装的全局 pnpm
npm uninstall -g pnpm
# 重新用 corepack 激活
corepack enable
corepack prepare pnpm@latest --activate
```
### 3. pnpm store 膨胀
pnpm 的内容寻址存储在反复安装/删除不同版本后会产生碎片:
```bash
# 清理未被任何 package.json 引用过的 store 条目
pnpm store prune
# 定期检查 store 大小
du -sh $(pnpm store path)
```
建议每月执行一次 `pnpm store prune`,通常能释放数 GB 空间。
### 4. pnpm 与 Native Addon
带有 C++ 原生编译绑定的包(如 `sharp`、`bcrypt`)在 pnpm 环境下有时会因为找不到头文件路径而编译失败:
```bash
# 临时关闭 strict-deps 解决 header 路径问题
pnpm install --strict-peer-dependencies=false
# 长期方案:在 package.json 中声明正确的 dependencies 层级
```
## 延伸阅读
- [[mac-dev-env-setup/homebrew-intro]] — 通过 Homebrew 安装 nvm
- [[mac-dev-env-setup/git-ssh-config]] — SSH 配置(用于 git clone private repos)
- [[mac-dev-env-setup/go-module-proxy]] — Go 生态的依赖管理对比参考