nvm — 在同一台机器上轻松切换 Node 版本
是什么
nvm(Node Version Manager)是一个 bash 脚本,让你能在同一台机器上装多个 Node.js 版本,并随时按 shell 会话切换。日常类比:像衣柜里挂着几件不同尺码的衣服,出门前选一件穿——不用每次买新的再扔旧的。
你输入:
nvm install 20nvm use 18node --version # v18.19.0这台机器上两个 Node 都在。一个项目用 18,另一个用 20,互不干扰。
这件事看起来平平无奇,但前端开发里几乎没人能绕开它。
为什么重要
不用 nvm 之前,前端工程师常被这些问题困扰:
- 老项目锁了 Node 14,新项目要 Node 20——全局只能装一个怎么办
- 团队里有人 Node 16、有人 Node 18,跑出来结果不一样——怎么对齐
- 想试试 Node 22 新特性,但又怕装坏了系统的 Node
- CI 上要用某个特定版本,本地复现不了——根因是版本不一致
nvm 一句 nvm use 14 全部解决。它是 Node 生态里最早被广泛接受的版本管理工具,至今仍是事实标准。
核心要点
nvm 干的事可以拆成 三步:
-
每个 Node 装在独立目录:
~/.nvm/versions/node/v20.10.0/、~/.nvm/versions/node/v18.19.0/……每个版本一个完整的 Node 安装,互不覆盖。 -
改 PATH 实现切换:
nvm use 18不挪文件,只是把当前 shell 的PATH环境变量改成~/.nvm/versions/node/v18.../bin:其它路径。从此 shell 里输入node时,OS 沿PATH查到的第一个就是 Node 18。 -
它不是程序而是 shell 函数:nvm 必须被
source进 shell(写在~/.zshrc或~/.bashrc里)。因为只有 shell 函数才能改父进程的环境变量——一个外部命令做不到这件事。
理解第三点,你就懂为什么 nvm “只对当前 shell 生效”——开个新终端可能就回到默认版本了。
实践案例
案例 1:项目锁定 Node 版本
在项目根目录创建 .nvmrc:
18.19.0然后 cd 进项目,执行:
nvm use# Found '/path/to/.nvmrc' with version <18.19.0># Now using node v18.19.0团队里每个人 cd 进来 nvm use 一下,版本立刻对齐。CI 里也读这个文件,本地和线上彻底一致。
案例 2:临时跑命令不切全局
nvm exec 16 npm test这条命令只在 Node 16 下跑 npm test,跑完 shell 还是原来的版本。比 nvm use 16 → npm test → nvm use default 三步省事。
案例 3:装最新长期支持版
nvm install --ltsnvm alias default lts/*第一行装最新 LTS,第二行把它设为默认。以后开新 shell 自动用这个版本。
踩过的坑
-
shell 启动变慢:nvm.sh 有 1000+ 行 bash,每开一个终端都 source 一次,可能加 100-300ms 延迟。解决办法:用 lazy-load 包装,第一次输入
nvm/node/npm时再加载。 -
~/.npmrc里设 prefix 会破坏 nvm:很多老教程教你npm config set prefix ~/.npm-global,这会让全局包装到 nvm 接管的目录之外,一切混乱。装 nvm 前先删掉这一行。 -
Homebrew 装的 nvm 是坑:官方明确不支持。Homebrew 把 nvm 装到
/usr/local/opt/nvm,但 nvm 自己希望在$HOME/.nvm。混着用 PATH 会乱。建议用官方 install 脚本。 -
fish shell 不原生支持:nvm 是 bash 函数,fish 语法不兼容。要么换 fnm,要么装 fish-nvm 之类的 plugin。
-
.nvmrc不会自动 use:进项目目录不会自动切版本。要么手动nvm use,要么在~/.zshrc加chpwdhook。 -
Apple Silicon 老版本要 Rosetta:Node 14 之前没出 ARM64 二进制,M1/M2/M3 上装 14 需要先开 Rosetta 2。
适用 vs 不适用场景
适用:
- macOS / Linux / WSL 上管理多个 Node 版本
- 团队用
.nvmrc对齐版本 - 想试新版 Node 不想动系统的
不适用:
- Windows 原生:nvm 是 bash 脚本,原生 Windows 跑不了。用 nvm-windows(同名但完全不同的 Go 项目)或 fnm。
- shell 启动速度敏感:fnm(Rust 编译的二进制)启动快 30-100 倍。
- 项目级钉死版本:volta 在
package.json里写volta.node字段,进项目自动切,免手动use。 - 容器里:直接
FROM node:20-alpine,nvm 反而是负担。 - 多语言版本管理:需要同时管 Python / Ruby / Erlang 时,asdf 一个工具搞定全部。
历史小故事(可跳过)
- 2010 年:Tim Caswell(早期 Node.js 核心贡献者,也是 howtonode.org 作者)在 GitHub 上 Creationix 命名空间下发布第一版 nvm。原始动机就是他自己同时在维护几个 Node 项目,受不了反复重装。
- 2014 年前后:项目搬到
nvm-sh组织,社区接管维护。 - 2018 年:以色列开发者 Schniz 用 Rust 写了 fnm,启动开销近乎为零,兼容
.nvmrc,开始抢 nvm 的份额。 - 2019 年:LinkedIn 团队公开 Volta(也是 Rust 写的),主打
package.json里钉死版本,进项目自动切。 - 2026-01-29:nvm 发布 v0.40.4。尽管有 fnm / volta / asdf 等更现代的替代品,nvm 仍是最广为人知的那个——大量教程、CI 模板、Dockerfile 默认就用它。
学到什么
-
PATH 是 Unix 最强的”配置开关”:不动文件、不改全局,只改环境变量就能让”哪个 node 是 node”换人。这个思路在 pyenv / rbenv / asdf / direnv 上反复出现。
-
shell 函数 vs 二进制的区别:一个外部程序改不了调用它那个 shell 的环境变量(子进程改环境不影响父进程)。这就是 nvm 必须 source 不能直接执行的根本原因。
-
“够用就是事实标准”:nvm 性能不是最好、设计不是最优雅、Windows 还不支持,但它最早出现 + 文档够全 + 装起来简单,就一直是默认选项。fnm / volta 技术上更好但份额超不过它。
-
工具链的代际更替很慢:2018 年 fnm 比 nvm 快 30 倍,但 2026 年仍有大半项目用 nvm。开发者迁移成本远比想象大。
延伸阅读
- 官方仓库:nvm-sh/nvm(README 极详细,遇到问题先查这里)
- fnm(Rust 替代品):Schniz/fnm
- volta(项目级钉死):volta.sh
- 启动加速 lazy-load:搜 “lazy load nvm zsh” 有大量配置片段
- homebrew —— Homebrew 装 nvm 不被官方支持的原因
- pnpm —— Node 包管理器现代选择,nvm 管 Node 版本,pnpm 管包
关联
- homebrew —— macOS 包管理器;很多人会用 Homebrew 装 nvm,但官方不推荐
- pnpm —— nvm 管 Node 版本,pnpm 管 npm 包,两者职责不重叠
- react-server-components —— RSC 对 Node 版本要求严格(Node 18.17+),是 nvm 价值的典型场景
- nix —— 把”包管理 + 版本管理”做成纯函数;思想上是 nvm 的”扩大无数倍”版本
反向链接
- nix —— Nix — 把每个软件包当成纯函数的输出
- pnpm —— pnpm — 全机器只存一份的 Node 包管理器
- react-server-components —— React Server Components — 让组件自己决定在哪台机器跑