Node.js 官网首页给每个大版本都放了一段安装片段,复制粘贴就能用。它长这样(nodejs.org 首页 Node 24 卡片上的原始片段,本地存档 nodejs24):
# Download and install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
# in lieu of restarting the shell
\. "$HOME/.nvm/nvm.sh"
# Download and install Node.js:
nvm install 24
# Verify the Node.js version:
node -v # Should print "v24.14.1".
# Verify npm version:
npm -v # Should print "11.11.0".
⚠️ 别直接复制上面这段。
curl那行的v0.40.4和末尾两个期望输出都已经过期,照抄会拿到一个已知有 CVE 的 nvm 版本,并误判自己的安装”坏了”。修正版在「必须修的一处」,这里保留原样是为了把它当标本讲。
看起来是”下载 → 加载 → 安装 → 验证”4 个步骤、5 条命令(两条验证命令算一步)。但真正决定它能不能在你机器上复现的,藏在这些行背后:
\.那一行用\.而不是source——不是随手写的;nvm install 24里的24不是版本号,默认是一次远程解析,所以每次结果都不同;- 末尾那两个期望输出,是 2026-03-24 那一版的快照,照抄核对会误判”装坏了”;
curl那行 pin 的v0.40.4,恰好是 CVE-2026-10796 的最后一个受影响版本。
前三个机制讲”它怎么跑起来”,后三个讲”它为什么在你机器上不一样”。这篇文章把它当 demo 逐层拆开。
目录
- 机制一:nvm 不是程序,是 shell 函数
- 机制二:为什么是
\.,以及为什么不能./nvm.sh - 机制三:
nvm install 24里的 “24” 是一次远程解析 - 机制四:版本号为什么对不上——可复现性靠 pin,不靠 major
- 机制五:
.nvmrc写24和写24.21.0语义完全不同 - 机制六:安装器往你的 shell 里到底写了什么
- 必须修的一处:nvm 版本与 CVE-2026-10796
- 切完版本别忘了原生模块
- 验证与常用姿势
- 出问题怎么查
- 什么时候别用 nvm
- 版本维护:台账与替换方法
- 总结
机制一:nvm 不是程序,是 shell 函数
nvm 没有可执行文件。安装器往 ~/.nvm/ 里放的是一堆 .sh,其中 nvm.sh 定义了一组 shell 函数。所以会看到这种”看起来不对劲”的现象:
$ which nvm
# 无输出,甚至可能报错
$ command -v nvm
nvm
原因是 which 只在 PATH 里的可执行文件中查找,而 nvm 是当前 shell 内存里的一个函数。command -v 和 type nvm 才是可靠检查:
$ type nvm
nvm is a function
(措辞随 shell 变:bash 打印 nvm is a function,zsh 打印 nvm is a shell function。判断标准是”它说自己是 function”,具体前缀不重要。)
这条性质决定了后面所有事:nvm 的”安装”和”切换”都发生在 shell 层,没有守护进程,没有全局状态中心。新开一个终端标签页,能不能用 nvm 完全取决于那个 shell 有没有加载过 nvm.sh——这正是 demo 里 in lieu of restarting the shell(代替重启终端)那条注释存在的原因,也是 nvm: command not found 类问题的根。
推论:CI、cron、systemd unit、
docker build这些环境不会自动加载交互式 shell 配置,所以 nvm 在里面默认不可用。但”不可用”不等于”用不了”——手动. "$NVM_DIR/nvm.sh"之后照样能装,容器里RUN . "$NVM_DIR/nvm.sh" && nvm install 24也是常见写法。常见,但不推荐:代价是 nvm 要求一个 shell 环境 + 一次仓库 clone,而容器和 CI 的心智模型是”锁死一个可复现的运行时”,理由见「什么时候别用 nvm」。
机制二:为什么是 \.,以及为什么不能 ./nvm.sh
demo 里加载 nvm 的那行是 \. "$HOME/.nvm/nvm.sh"。两个细节(本文讨论 sh / bash / zsh,fish 不是 POSIX shell,不在这套讨论范围内):
为什么不用 source。 . 是 POSIX 内建命令,source 是 bash/zsh 的扩展。在 dash / 通用 sh 里没有 source,只有 .。所以官方片段选可移植的那一个。
那个反斜杠是什么。 反斜杠的通用语义是转义,这里实际利用的机制是抑制 alias 展开。如果有人 alias 过 .(例如 alias .='source -'),\. 保证仍走内建命令而不是 alias。它不是什么”防止 shell 用别的方式解释”——网络教程里常见的那个解释是编的。
为什么不能直接执行。 nvm.sh 要干的事是改当前 shell 的 PATH、定义函数、设 alias。直接 ./nvm.sh 是在子进程里做这些:子进程退出后,函数和 PATH 改动全部蒸发,父 shell 一点感知都没有——比报错更糟,因为它安静地什么都没发生。同理,bash nvm.sh 也不行。
正确的两种写法等价:
\. "$HOME/.nvm/nvm.sh" # POSIX 通用
source "$HOME/.nvm/nvm.sh" # bash / zsh
机制三:nvm install 24 里的 “24” 是一次远程解析
24 不是完整版本号,它是一个版本 specifier(版本规格)。nvm 拿到它之后才去解析出具体版本——默认是拉 nodejs.org 的远端 index 取匹配的最大值。nvm.sh 里的分支大致是:
VERSION="$(nvm_version "${PROVIDED_VERSION}")" # nvm install 24
# 内部走 nvm_ls_remote → nvm_ls_remote_index_tab → 下载 ${MIRROR}/index.tab → 匹配 → 取最大
注意默认取的是 dist/index.tab(制表符分隔的纯文本索引,新版在前,取第一条匹配即最新),不是 index.json。index.json 是同一份数据的 JSON 形式——nvm 默认不用它,但「版本维护」那节的查询命令用它,因为更好喂给 jq。
解析结果还会受几个环境因素影响:NVM_NODEJS_ORG_MIRROR 决定去哪份 index 取、nvm install --offline(v0.40.5+)可以直接用本地缓存、以及 lts/* 这类 alias 也走同一套解析。所以严格说是”默认对远端解析”。
于是这三条命令的语义完全不同:
| 命令 | 解析时机 | 解析范围 | 结果 |
|---|---|---|---|
nvm install 24 |
此刻远程解析 | 远端所有 24.x | 当天最新 24.x |
nvm use 24 |
只看本地 | 本地已装 24.x | 本地最高 24.x;没装就直接报错 |
nvm install 24.21.0 |
不解析 | 精确版本 | 精确版本 |
(表中按默认行为描述;NVM_NODEJS_ORG_MIRROR 与 nvm install --offline 的例外见本节末。)
nvm use 不负责安装。 本地没有匹配版本时它只会失败,把那句报错原样甩给你。下面是实测输出(本文写作机上的 nvm 0.40.4):
$ nvm use 22
N/A: version "v22" is not yet installed.
注意版本会带 v 前缀。如果你给的是一个注册过的 alias(lts/*、node、stable 这类),报文会多一段”alias → 解析结果”:
$ nvm use lts/argon
N/A: version "lts/argon -> v4.9.1" is not yet installed.
判据是“它是不是 alias”,不是”能不能解析出具体版本”——精确版本同样解析得出来,但没装时走的是不带箭头那条:
$ nvm use 4.9.1
N/A: version "v4.9.1" is not yet installed.
对应 nvm.sh 里的两个分支(0.40.4 与 0.40.8 逐字相同,这段路径没被那三个安全修复动过):
if VERSION="$(nvm_resolve_alias "${PROVIDED_VERSION}")"; then
nvm_err "N/A: version \"${PROVIDED_VERSION} -> ${VERSION}\" is not yet installed." # alias
else
nvm_err "N/A: version \"$(nvm_ensure_version_prefix "${PROVIDED_VERSION}")\" is not yet installed." # 其余
fi
看到这类报错就明白该 nvm install 24 了,而不是等着它自己去下载。(如果你的机器上一个 24.x 都没装,同样会看到 nvm use 24 报 N/A: version "v24" is not yet installed.——和上面 nvm use 22 同型。)
nvm install 做的事:拉远端 dist/index.tab 解析出具体版本 → 下载官方 tarball → 用官方 SHASUMS256.txt 校验 checksum → 解压到 ~/.nvm/versions/node/v24.21.0/ → 把该目录的 bin 提到 PATH 最前 → 若是本机第一个版本,设成 default。
校验这步是”下载安全”这条线的起点,和后面 CVE 一节讲的是同一件事的两个方向:checksum 防”下载物被掉包”,版本号防”工具本身有洞”。
顺带一句版本选择:截至 2026-09-25,Node 24 是当前的 LTS 线(代号 Krypton)。所以 nvm install --lts 当天就等价于 nvm install 24;真要追新,可以 nvm install node(最新 Current;核对日为 v26.10.0,注意它非 LTS)。
机制四:版本号为什么对不上——可复现性靠 pin,不靠 major
demo 末尾两行写着期望输出 v24.14.1 / 11.11.0。核对一下真实数据:
| 版本 | 发布时间 | 附带 npm | LTS |
|---|---|---|---|
v24.14.1 |
2026-03-24 | 11.11.0 | Krypton |
v24.21.0 |
2026-09-07 | 11.19.0 | Krypton |
也就是说这两个数字是半年前那一版的真实快照,本身不假,但按 24.x 滚动发布,核对日(2026-09-25)nvm install 24 拿到的是 v24.21.0,npm 是 11.19.0。
这两个数字不是查来的,是本文写作机上的真实输出:那台机器至今还装着 v24.14.1,
nvm use 24会打印Now using node v24.14.1 (npm v11.11.0)。也就是说 demo 里”应该打印什么”来自一次真实安装,时间是 2026-03-24。
所以拿 npm -v 一 diff 看到 11.19.0 ≠ 11.11.0,不是装坏了。但代价是:demo 里的注释是死的,且 24 天然不可复现。要可复现就 pin 到完整三段版本:
nvm install 24.21.0 # 精确安装
node -v # 打印 v24.21.0(前提是没有 PATH 遮挡,见排查表)
判断标准:只要跨机器或跨时间要一致,就 pin 三段版本;只需要”同一大版本线”(跟 latest LTS 走),用
24。
机制五:.nvmrc 写 24 和写 24.21.0 语义完全不同
这是最容易踩、也最少被讲清楚的一处。.nvmrc 里的内容走的是和命令行不同的解析路径:
nvm install(无参)→nvm_version "$(cat .nvmrc)":对远端解析;nvm use(无参)→nvm_match_version:对本地已装解析。
而 nvm_find_up 会从当前目录逐级向上找 .nvmrc,所以你在 packages/web/ 里执行 nvm use,读到的是仓库根目录那个文件。
由此得到两条很实际的结论:
echo "24" > .nvmrc # ⚠️ 不固定到某个 minor/patch = "我解析时最新的 24.x"
echo "24.21.0" > .nvmrc # ✅ 锁死三段
git add .nvmrc # 连同 package.json 一起提交
第一种写法能正常工作(这也是官方片段风格),但它把”哪一版”这件事推迟到了每个人各自的时间点:A 同学 3 月装的、B 同学 9 月装的,npm ci 出的依赖树可能不同。团队仓库里这就是”我们昨天还好好的”类玄学问题的常见来源。
不过别把它当错——这是个取舍,不是对错:
| 目标 | 写法 | 代价 |
|---|---|---|
| 完全可复现(审计、回放、复现 bug) | 24.21.0 |
每次发版要记得升 .nvmrc |
| 跟 LTS 线自动升级、省维护 | 24 |
谁在什么时候装的哪一版不可知,跨人跨时不一致 |
| 折中 | 24 + 提交 package-lock.json |
依赖树可复现,但运行时仍可能差一个 minor |
选 major 的合理前提是:团队接受”运行时小版本漂移”,并靠 lockfile 把依赖钉死;一旦有人需要”我出问题那天到底跑的是哪版 Node”,major 就变成了不可回答的问题。
.nvmrc 天然往 CI 延伸:
# .github/workflows/ci.yml
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
本机和 CI 读同一个文件——这才是版本管理该有的闭环。
机制六:安装器往你的 shell 里到底写了什么
curl … install.sh | bash 并不是只 clone 一个目录。nvm_detect_profile 的探测顺序是(v0.40.8 install.sh 第 310 行起,master 与之一致):
nvm_detect_profile() {
if [ "${PROFILE-}" = '/dev/null' ]; then # 311: 最高优先级开关
return
fi
if [ -n "${PROFILE}" ] && [ -f "${PROFILE}" ]; then # 316: 显式指定
nvm_echo "${PROFILE}"; return
fi
# 324: 先 bash … elif 330: 再 zsh(同一个 if/elif 链上的先后,不是并列)
PROFILE=/dev/null→ 明确表示不要碰你的 profile(第 311 行,第一个分支);$PROFILE已设置且文件存在 → 用它(第 316 行);- shell 是 bash →
~/.bashrc,否则~/.bash_profile(第 324 行起的if); - shell 是 zsh →
${ZDOTDIR:-$HOME}/.zshrc,否则~/.zprofile(第 330 行起的elif); - 都没命中 → 兜底依次试
.profile、.bashrc、.bash_profile、.zprofile、.zshrc。
想自己核对:https://github.com/nvm-sh/nvm/blob/v0.40.8/install.sh#L310 。第 311 行和第 316 行是两次独立的提前返回,bash 的
if在 324、zsh 的elif在 330——所以”/dev/null最优先”和”bash 先于 zsh”都是源码里写死的,不是我整理出来的顺序。
命中的文件末尾会被追加一段,大意是:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
($NVM_DIR 也可以在安装前用环境变量指到别处,默认就是 ~/.nvm。)
几个由此产生的真实坑:
- zsh 不读
~/.profile。 它只读~/.zshrc(登录 shell 另读~/.zprofile)。所以如果 nvm 被写进了~/.profile,zsh 用户永远看不到它。 ZDOTDIR会让写入位置偏移。 配了ZDOTDIR=~/.config/zsh的用户,nvm 写的是~/.config/zsh/.zshrc。- 非交互 shell 默认不读这些文件。 脚本里要用 nvm,得自己
\. "$NVM_DIR/nvm.sh"(或用 demo 里\.那种写法)。 - 想完全不让它改配置——但别把
VAR=x写在管道左边:
# ❌ 无效:VAR=x 只作用于紧跟其后的那个命令(这里是 curl),管道右侧的 bash 拿不到
PROFILE=/dev/null curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash
# ✅ 把赋值放到管道右侧,交给真正执行安装器的那个进程
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | PROFILE=/dev/null bash
# ✅ 或者落地再跑(顺带解决了"想先读一眼再执行")
curl -o install-nvm.sh https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh
PROFILE=/dev/null bash install-nvm.sh
这个坑值得单独记一笔:VAR=x cmd1 | cmd2 中,赋值前缀属于 cmd1 这个简单命令,只进入 cmd1 的环境;cmd2 是从原 shell fork 出来的,拿不到。同理 LC_ALL=C sort | uniq 里的 C 也只作用于 sort。要让两边都拿到,用 export,或把赋值写到管道右侧。
安装器还会顺手做一件贴心的事:检测你当前 PATH 上已有的全局 npm 包并告警——因为它们会成为「出问题怎么查」里那条 PATH 遮挡问题的元凶。
必须修的一处:nvm 版本与 CVE-2026-10796
demo 里 curl 那行 pin 的 v0.40.4,正好卡在修复点的前一位:
| nvm 版本 | 日期 | 关键内容 |
|---|---|---|
| v0.40.4 | 2026-01-29 | ← demo 用的这版 |
| v0.40.5 | 2026-06-04 | 修复 CVE-2026-10796;新增 nvm install --offline |
| v0.40.6 | 2026-07-15 | loongarch64 支持;Alpine arm64-musl 修复;绕过 curl/wget 的 alias |
| v0.40.7 | 2026-08-18 | NVM_NO_SOURCE_FALLBACK;并发安装同版本串行化 |
| v0.40.8 | 2026-09-21 | 当前最新;alias/version path 拒绝 ..;不再污染 zsh 的 nomatch/markdirs |
CVE-2026-10796(CVSS 4.0 = 7.5 HIGH):nvm <= 0.40.4 会把镜像返回的 version 字符串不做校验地拼进 eval 拼出的 curl/wget 命令行,以及 awk 程序文本,形成命令执行。触发前提是攻击者能控制你配置的 mirror(或对非 TLS 的 mirror 做中间人);默认的 https://nodejs.org 不受影响。修复方式是全部改成传字面 argv、awk 用 -v 传数据、并拒绝不合版本语法的字符串。
来源(以下均为本文核对日 2026-09-25 的公开记录,不是安全公告;发布或转载前请逐条点开复核,尤其是”影响 <= 0.40.4“和”修复于 0.40.5”这两个边界值——它们撑着全文的核心建议):
- GitHub Security Advisory
GHSA-3c52-35h2-gfmm(severity: high,vulnerable<= 0.40.4,patched0.40.5;2026-06-04 发布后未被修订):https://github.com/nvm-sh/nvm/security/advisories/GHSA-3c52-35h2-gfmm - CVE 记录(CVE List v5,CVSS 4.0 = 7.5 HIGH):https://www.cve.org/CVERecord?id=CVE-2026-10796
- 三个修复提交(均在 2026-06-03 合入):
6d870d1nvm_download去掉eval、防止 mirror 版本串注入命令;90bb887nvm_get_checksum把 tarball 名作为数据传给 awk 而非程序文本;70fb4ednvm_download_artifact拒绝含非法字符的版本串 - nvm v0.40.5 Release notes(2026-06-04,Security fix 段):https://github.com/nvm-sh/nvm/releases/tag/v0.40.5
安全类结论会随新披露变化。如果哪天 GHSA 被撤回或范围被修订,以链接里的最新内容为准,本文只对核对日负责。
所以这不是”你现在就中招了”,而是:tag 停在修复点前一位,在一篇讲安装的文章里没有任何理由。(顺带一提:本文写作机上的 nvm 就恰好是 0.40.4——写这篇的过程本身撞上了这个坑。)把 demo 升级成:
# Download and install nvm (>=0.40.5; 0.40.4 及更早受 CVE-2026-10796 影响):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash
# in lieu of restarting the shell
\. "$HOME/.nvm/nvm.sh"
# Download and install Node.js 24 (当前 LTS 线; 需要可复现就写全 24.21.0):
nvm install 24
# Verify the Node.js version:
node -v # 以安装当天的最新 24.x 为准(核对日 2026-09-25: v24.21.0)
# Verify npm version:
npm -v # 同上(核对日 2026-09-25: 11.19.0)
关于 curl | bash 本身:URL 固定到具体 tag 是对的(脚本内容可复现),但真正被 pin 的是”脚本版本”,”装出来的 nvm 版本”由脚本内部决定——v0.40.4 的 install.sh 里写死了 nvm_latest_version() { nvm_echo "v0.40.4"; }。两个可以用但 demo 没提的开关:
# 装比脚本更新的 nvm(NVM_INSTALL_VERSION 覆盖脚本内写死的版本)
curl -o install-nvm.sh https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh
NVM_INSTALL_VERSION=v0.40.8 bash install-nvm.sh
# 完全不让它改 shell 配置(赋值必须放管道右侧,见机制六)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | PROFILE=/dev/null bash
真要审一眼再跑,就先落地再读(唯一值得保留的 curl | bash 顾虑):
curl -o install-nvm.sh https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh
less install-nvm.sh && bash install-nvm.sh
切完版本别忘了原生模块
这一节独立于版本管理本身,但踩中的人很多,所以放在这里。
Node 的原生模块(C/C++ 编译产物)跟着 ABI 版本(NODE_MODULE_VERSION)走。这个编号通常随 major 变化,但具体以 process.versions.modules 的实测值为准——它不是数学上保证的规律:
| Node 线 | NODE_MODULE_VERSION(ABI) |
相对上一线 |
|---|---|---|
| v18.x | 108 | — |
| v20.x | 115 | +7 |
| v22.x | 127 | +12 |
| v24.x | 137 | +10 |
| v26.x | 147 | +10 |
增量并不均匀(+7、+12、+10、+10),所以别把”每个 major 固定 +10”当规律。数据来自 nodejs.org/dist/index.json 的 modules 字段,核对日 2026-09-25;其中 v24=137 在本文写作机上用 node -p process.versions.modules 实测确认(v24.14.1 → 137),其余各值为该 JSON 核对。
于是 nvm use 24 之后,在 Node 22 下装好的 node_modules 里的原生模块是不匹配的——典型症状是加载即报 was compiled against a different Node.js version,或者更隐蔽的行为异常。涉及 better-sqlite3、sharp、canvas、node-gyp 系、以及任何带 .node 文件的依赖。
不确定自己有没有踩这个坑,先扫一眼:
find node_modules -name '*.node' | head # 有输出 = 你确实有原生模块
处理方式按代价从低到高:
npm rebuild # 先试:只重编原生模块
rm -rf node_modules && npm ci # 稳妥:依赖树本来就要重装时用这个
npm rebuild也不是万能:它一样要源码或适配当前平台的预编译包,缺了就会退回本地编译(于是你会看到编译工具链报错)。而且换架构(arm64 ↔ x64)或换 libc(Alpine 的 musl ↔ Debian 的 glibc)时,即使同一个 major 也必须重编——预编译产物是按平台分发的。Docker 里 Alpine 换 Debian(或反过来)就属于这种情况。
同 major 内只切 minor/patch 不需要重编(ABI 没变),只有跨 major、跨架构或跨 libc 才需要。所以上一节说的”pin 三段版本”还有个附带好处:它顺便让 ABI 在团队里保持一致。
验证与常用姿势
command -v nvm # 期望: nvm (不要用 which nvm)
type nvm # 期望: nvm is a function (zsh 会多写 "shell ")
nvm ls # 已装列表,* 标记当前生效
node -v && npm -v # 版本
which -a node # 所有 node 来源,用来抓 PATH 遮挡
nvm alias default 24 # 新终端默认版本
nvm uninstall 18 # 清理某个版本
一条命令看清全局状态:
nvm current && nvm ls && echo "NVM_DIR=$NVM_DIR" && type -a node
卸载 nvm 本身(三步;顺序无所谓,但建议先删 profile 那几行,免得中途处于”配置还在、目录没了”的报错状态):
# 1. 删掉 profile 末尾那段(export NVM_DIR / 加载 nvm.sh / bash_completion)
# macOS 默认 ~/.zshrc(登录 shell 另读 ~/.zprofile),Linux 一般 ~/.bashrc
# 2. 删目录(默认 ~/.nvm),所有已装 Node 一起没
rm -rf "$NVM_DIR"
# 3. 重开终端,或当前 shell 里 unset NVM_DIR
出问题怎么查
| 现象 | 真实原因 | 处理 |
|---|---|---|
nvm: command not found |
profile 没被当前 shell 读(zsh 不读 ~/.profile;配了 ZDOTDIR;非交互 shell) |
找到安装器报告改动的那个文件,\. 一次;非交互场景自己 source |
node -v 是系统版本 |
PATH 前面有 Homebrew / 官方安装版残留 |
which -a node 逐个看,卸掉多余的全局装 |
| 安装器卡住 / 下载失败 | 代理没配到当前 shell | export HTTPS_PROXY=http://host:port 再跑 |
| 装完没生效 | 没重启终端,且没 source | \. "$HOME/.nvm/nvm.sh" |
| Apple Silicon 装成 x86 | 终端跑在 Rosetta 下 | uname -m 应为 arm64;换原生终端 |
| 切版本后原生模块报 ABI 不匹配 | 跨 major 换了 NODE_MODULE_VERSION |
npm rebuild,不行就 rm -rf node_modules && npm ci(见「切完版本别忘了原生模块」) |
nvm use 24 报 N/A: version "v24" is not yet installed. |
use 只切换、不安装 |
先 nvm install 24 |
nvm install 编译源码很慢 |
该版本没有对应架构的预编译包 | 正常回退;需 nvm >= 0.40.7(台账 N1 那一版才有这个开关)才可用 NVM_NO_SOURCE_FALLBACK=1 强制失败而不是编译 |
什么时候别用 nvm
nvm 是开发者本机的方案,作用域到”可交互 shell”为止。三种场景应该换思路:
平台不匹配——Windows。 nvm-sh 的 nvm 是 POSIX shell 工具,不支持 Windows 原生 shell 环境。两条路:在 WSL 里按本文装;或用另一个项目 nvm-windows(它是独立实现,命令语法和本文不同,别混用)。
生产 / 容器——用锁死的镜像:
FROM node:24.21.0-bookworm-slim
更进一步可以在 CI 里锁 digest(@sha256:…),彻底排除”同 tag 重新构建”的漂移。默认就该用官方镜像,而不是在 image build 阶段装 nvm——后者多一次 clone、多一层 shell 依赖,还把”构建时可变”引入”运行时本该固定”的东西。nvm 和官方镜像不是二选一:镜像是运行时交付物,nvm 是本机开发期的工具。
CI——读 .nvmrc,而不是在 runner 上装 nvm: 前面 actions/setup-node + node-version-file 那段就是答案。省掉一次 clone 和一次 shell 注入。
也在选型阶段:nvm 不是唯一解。 fnm 是单二进制(不是 shell 函数,跨平台、切换更快),mise 除了 Node 还能统一管 Python/Go/工具链。它们都吃 .nvmrc/.node-version,迁移成本主要是改 shell 配置那几行。选哪个取决于你要不要”只管 Node”以及是否在意 shell 启动开销——但”the right way”这种说法,在 2026 年至少应该先看一眼替代品再下结论。
版本维护:台账与替换方法
这一节是给未来的维护者(可能就是你自己)看的,跳过不影响阅读。 它解决的是”这篇文章的版本号半年后会过期”这一个问题。
本文会腐烂的只有版本号。它们全部收敛到下面这张台账里,维护时只改表里标”需更新”的行,正文引用编号即可,不必全文搜索。
版本台账(核对日 2026-09-25)
| 编号 | 值 | 发布日期 | 含义 / 约束 | 需更新 |
|---|---|---|---|---|
| N1 | v0.40.8 |
2026-09-21 | nvm 当前最新 tag。必须 >= 0.40.5 |
是 |
| N2 | 24.21.0 |
2026-09-07 | Node 24 最新三段版本(LTS 线 Krypton) | 是 |
| N3 | 11.19.0 |
2026-09-07 | N2 那一版附带的 npm | 是 |
| N4 | 0.40.5 |
2026-06-04 | 修复 CVE-2026-10796 的最低版本(CVSS 4.0 = 7.5 HIGH,影响 <= 0.40.4) |
否,除非出新技术结论 |
| N5 | v0.40.4 |
2026-01-29 | demo 原始片段 pin 的版本,文章批它的对象 | 否(历史事实) |
| N6 | v24.14.1 / 11.11.0 |
2026-03-24 | demo 注释里的期望输出,用来说明”注释会过期” | 否(历史事实) |
| N7 | v26.10.0 |
— | 写文时的最新 Current(非 LTS) | 是 |
| N8 | 137 |
随 major 变 | Node 24 的 NODE_MODULE_VERSION(ABI),原生模块重编的依据;v24=137 为本机 node -p 实测,其余取自 index.json 的 modules 字段 |
是(major 换代时) |
正文里 N1–N3、N7 的落点:N1 在”必须修的一处”的升级版 demo、机制六的 4 条 curl、NVM_INSTALL_VERSION;N2 在机制三对照表、机制四的 pin 示例、机制五的 .nvmrc、FROM node:…-bookworm-slim;N3 只出现在升级版 demo 的期望输出注释里;N7 在机制三的”追新”那句;N8 在「切完版本别忘了原生模块」的表格。
怎么查新值(命令已实测)
# N1 / N4:nvm 最新 tag(三选一)
gh api repos/nvm-sh/nvm/releases/latest --jq .tag_name
git ls-remote --tags --refs https://github.com/nvm-sh/nvm.git | sed 's#.*refs/tags/##' | sort -V | tail -1
curl -s -H 'User-Agent: blog' https://api.github.com/repos/nvm-sh/nvm/releases/latest | grep -o '"tag_name": *"[^"]*"' | head -1
# 注意:GitHub API 缺 User-Agent 会返回 403,curl 那条必须带 -H
# N2 / N3 / N7:Node 24 最新版本 + 附带 npm(需要 jq)
# 这里用 index.json 而不是 nvm 自己默认的 index.tab:同一份数据,JSON 更好喂给 jq
curl -s https://nodejs.org/dist/index.json \
| jq -r '[.[]|select(.version|startswith("v24."))][0]|"\(.version) npm=\(.npm) \(.date) lts=\(.lts)"'
# → v24.21.0 npm=11.19.0 2026-09-07 lts=Krypton
# 取 [0] 依赖 index.json 按发布时间倒序(官方一直是这个顺序);若上游改序,改用
# sort_by(.date) | .[-1]
# 下面三条兜底不依赖 jq,但依赖 index.json 是无空格的紧凑序列化格式;上游一旦改成
# 带空格美化输出,grep -o 会失配,那时退回上面那条 jq 版本。
# 另外这几条里的 "| head -1" 会让上游先吃到 SIGPIPE——grep 没事,但换成 jq 输出量大时
# 可能整段输出被吞,所以下面 ABI 那条改用 first()。
# 同上,不装 jq 的兜底(拆 JSON 后按行取,三条要分别跑)
curl -s https://nodejs.org/dist/index.json | grep -o '"version":"v24\.[0-9.]*"' | head -1
curl -s https://nodejs.org/dist/index.json | tr '{' '\n' | grep '"version":"v24\.' | head -1 | grep -o '"npm":"[^"]*"'
curl -s https://nodejs.org/dist/index.json | tr '{' '\n' | grep '"version":"v24\.' | head -1 | grep -o '"\(date\|lts\)":"[^"]*"'
# 顺手确认 LTS 线有没有换代(换了的话 N2 的 major 也要换)
curl -s https://nodejs.org/dist/index.json | jq -r '[.[]|select(.lts!=false)][0]|"\(.version) \(.lts)"'
# 原生模块 ABI 也会随 major 变(本文表格里的 108/115/127/137/147 就是这个字段)
curl -s https://nodejs.org/dist/index.json | jq -r 'first(.[]|select(.version|startswith("v24.")))|"\(.version) modules=\(.modules)"'
替换步骤
- 跑上面那几组查询,拿到新的 nvm tag、Node 24 版本/npm、以及(major 换代时)新的 ABI。
- 全局替换 N1(新 tag)、N2(新三段版本)、N3(新 npm)、N7(新 Current)。
- N1 顺手过一道安全闸:必须
>= 0.40.5;否则先看 Releases notes 里有没有新的 Security fix 段——有就先别只改数字,把结论补进”必须修的一处”。 - 把台账里的日期、
frontmatter的date与version_verified一起更新成本次核对日。 - 通读一遍受影响的句子:N2 一变,
.nvmrc示例、FROM node:…和”可复现性”那段的表述要一起对齐。
不要改的地方
- 顶部 demo 代码块保持原样。 它的
v0.40.4、v24.14.1、11.11.0是史料,文章要论证的正是”照抄会拿到什么、会过期在哪”。要修的版本出现在下面的”升级版 demo”里,两块并存才构成对比。 - N4 / N5 / N6 属于历史事实,改了反而破坏论证。
什么时候不只是改数字
install.sh的 profile 探测顺序变了 → 重读nvm_detect_profile,机制六整节要改(本文依据 v0.40.8 源码)。- nvm 出新 CVE → “必须修的一处”整节重写,并把新的最低安全版本补进 N4。
- Node 26 转正 LTS → N2 的 major 从
24换到26,标题、demo 里的nvm install 24、.nvmrc示例、FROM行、以及全文”当前 LTS 线”的说法一起换;N8 的 ABI 也跟着换。
这篇文章里唯一会骗人的东西就是版本号。上面这套台账 + 查询命令就是防腐剂:任何人都能在 30 秒内把全文重新校准到今天。
总结
demo 只有 4 个步骤、5 条命令,但它能跑通依赖 6 个机制:
- nvm 是 shell 函数,不是可执行文件——没有守护进程,所以”当前 shell 有没有加载过它”决定一切(
command -v而非which); - 必须
.进来,不能执行——.是 POSIX 内建(所以不用source),反斜杠只为防 alias; 24是版本规格不是版本号——nvm install对远端解析、nvm use对本地解析,所以两者结果不一定相同;- 可复现性来自三段 pin——
24.21.0才是锁,24是”此刻最新”; .nvmrc里写24等于把”哪一版”推迟到每个人自己的时间点——团队仓库要写全,并把同一文件接到 CI;- 安装器会改你的 shell 配置——探测顺序、
ZDOTDIR偏移、zsh 不读~/.profile是三个高频坑;想彻底关掉用PROFILE=/dev/null,但赋值要写在管道右侧。
再加两条不属于机制、但属于常识的:把 nvm 装到 >= 0.40.5,别停在 demo 里的 0.40.4;以及跨 major 切 Node 后重编原生模块(ABI 127 → 137,npm rebuild 或 rm -rf node_modules && npm ci)。
一句话:nodejs24 这段片段的正确用法是”照抄跑一遍,然后立刻把两处版本号改成你自己要的”——nvm 的版本、Node 的版本。前者防 CVE,后者防”我这台机器上是好的呀”。