Files

209 lines
6.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 生态的依赖管理对比参考