1. 版本管理这件事,先想清楚再动手
手里同时压着三四个项目,每个项目的package.json里 engine 字段写的 node.js 版本都不一样,这时候如果机器上只有一个全局的 node,那就是灾难现场。nvm 这类版本管理器的价值就在这个场景里体现出来——它让你在同一个 shell 里自由切换 node.js 的版本,而不是每次靠卸载重装来"更新"。很多人第一次接触 nvm,动机很朴素:想把本地的 node 从 16 升到 18 或者 20,但装完之后发现原来能跑的项目跑不起来了,或者全局装的pm2、nodemon、typescript全都不见了。这些问题背后不是 nvm 有 bug,而是版本管理的模型没有被理解透。
我这些年带过的新人里,几乎每一个都在"更新 node.js"这一步踩过至少一次坑。有的人直接去官网下了个 msi 或者 pkg 覆盖安装,结果旧版本残留在 PATH 里;有的人 nvm 装完了,nvm use也执行了,但node -v还是老版本;还有人在 Windows 上折腾半天,最后发现是权限问题。这些坑的共同点是:它们的报错信息都不会直接告诉你真正的原因。
这篇内容我会按一个完整的工作流来写——从理解 nvm 的机制,到不同系统上的安装细节,再到更新版本的具体命令、全局包怎么迁移、项目里怎么锁版本,最后是各种报错的排查思路。适合刚接手前端或者 Node 后端项目、需要频繁切版本的人,也适合那些已经装了 nvm 但一直没搞明白它工作原理的人。我不打算只给你一堆命令,而是把每个动作背后的原因讲清楚,这样下次遇到没见过的报错,你自己就能推。
1.1 nvm 到底替你干了什么
nvm 的核心动作其实只有一件事:改 PATH。你在 macOS 或者 Linux 上敲nvm use 20.11.1,它做的事情是把~/.nvm/versions/node/v20.11.1/bin这个目录塞到 PATH 的最前面,同时把其他版本的 node 路径从 PATH 里摘掉。所以node、npm、npx这些命令指向哪个二进制,完全取决于当前 PATH 的顺序。
这里有个特别容易误解的点:nvm 本身不是二进制程序,它是一个 Shell 函数。你打开~/.nvm/nvm.sh看,里面全是 bash 函数定义。这也解释了几个常见现象:
which nvm查不到东西,因为 nvm 不是可执行文件,type nvm才能看到它是函数;- 写脚本的时候,在
#!/bin/bash的脚本里直接调nvm use会报command not found,因为非交互式 shell 不会加载你的.bashrc或者.zshrc; - 换了个终端(比如从 zsh 切到 fish),nvm 就"消失"了,因为它需要针对不同 shell 重新 source。
Windows 上的 nvm-windows 完全是另一回事。它不是 Shell 脚本,而是用 Go 写的独立可执行程序,工作方式是在C:\nodejs这个位置创建一个指向实际版本目录的链接,然后把C:\nodejs放进系统 PATH。所以 Windows 上nvm use需要管理员权限——创建符号链接这个操作普通用户没权限做。理解这个差异很重要,因为你在网上搜到的一半教程可能是 macOS 的,另一半是 Windows 的,混着抄必然出问题。
1.2 为什么不能直接覆盖安装新版 node
官网下载安装包直接覆盖,理论上也能把 node 升级上去,但它会带来三个后果,而且都是那种过两天才发作的。
第一是版本残留。安装包不会清理旧版本的文件,Windows 上经常出现C:\Program Files\nodejs和用户目录下的 npm 缓存对不上号的情况,node -v显示 20,但npm -v报错,因为 npm 的全局模块路径还指着旧版本。
第二是没法回退。项目跑在 node 16 上,你手一抖升到 22,发现某个老依赖的 native 模块编译不过去,这时候想退回去就只能再去官网下个 16 的安装包覆盖一遍。一来一回半小时没了。nvm 的nvm use 16只需要一秒钟。
第三是全局包要重装。node 的全局包是装在版本目录下的,覆盖安装不会自动迁移。而 nvm 提供了nvm reinstall-packages这个命令,能把旧版本的所有全局包一键搬到新版本下,这个后面会详细讲。
注意:nvm-windows 和官方安装包是互斥的。如果你的 Windows 上已经用安装包装过 node,必须先通过"应用和功能"彻底卸载,并手动清掉
C:\Program Files\nodejs残留目录,再去装 nvm-windows。否则它会检测到已有 node 然后安装失败,或者装完了 PATH 里两个 node 打架。
1.3 几个版本管理工具的横向对比
市面上的版本管理工具不止 nvm 一家,选之前先看清楚差异,能省掉后期迁移的麻烦。
| 工具 | 实现语言 | 跨平台情况 | 典型命令 | 适用人群 |
|---|---|---|---|---|
| nvm-sh | Shell 脚本 | macOS / Linux / WSL | nvm install 20 | 前端、Node 后端日常开发 |
| nvm-windows | Go | Windows | nvm install 20.11.1 | Windows 本地开发 |
| fnm | Rust | 全平台 | fnm use | 在意 shell 启动速度的人 |
| Volta | Rust | 全平台 | volta install node@20 | 需要团队统一版本的场景 |
| n | Node 编写 | macOS / Linux | n install lts | 喜欢极简、只用 lts 的人 |
选型上我的建议很直接:macOS 和 Linux 环境优先用 nvm-sh,Windows 用 nvm-windows,如果你追求 shell 启动速度就上 fnm。fnm 是用 Rust 写的,启动时不用加载一大堆 shell 函数,开终端的速度明显快一截,而且它支持.node-version和.nvmrc两种文件,迁移成本很低。
Volta 的定位略有不同,它除了管 node,还管 npm、pnpm、yarn 的版本,而且可以在package.json里声明volta字段来锁定工具链版本。团队协作里如果你希望"克隆下来就一定是同一个版本",Volta 的约束力比 nvm 强。但代价是它对新版本的跟进稍慢,某些刚发布的 node 版本可能还没收录。
nvm-sh 的老毛病是开终端慢,因为它要在每个新 shell 里执行一遍nvm.sh,这个文件有几千行。社区有个优化方案是懒加载,只在真正调用 nvm 的时候才 source,配置文件里大概是这样:
# ~/.zshrc 或 ~/.bashrc export NVM_DIR="$HOME/.nvm" lazy_load_nvm() { unset -f nvm node npm npx [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" } nvm() { lazy_load_nvm; nvm "$@"; } node() { lazy_load_nvm; node "$@"; }这个技巧能把你开终端的时间从几百毫秒压到几十毫秒,代价是第一次执行 node 相关命令时会稍微卡一下。用不用看你对终端响应速度的敏感度。
2. nvm 安装与环境配置的实操细节
安装这一步看着简单,但它决定了后面 90% 的诡异问题。很多人的"nvm 用不了"其实是在安装阶段就埋了雷。
2.1 macOS 与 Linux:脚本安装与 brew 的取舍
macOS 上有两条路。一是官方的 install 脚本,二通过 Homebrew 安装。我个人更推荐官方脚本,原因是 brew 装的 nvm 在升级时容易和~/.nvm目录的权限纠缠不清,而且 brew 的版本更新节奏和你手上的项目需求不一定对得上。
官方脚本安装的命令是这一串:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash执行完之后,脚本会尝试把下面这段写进你的.bashrc、.zshrc或.profile:
export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"注意:如果你的 shell 是 zsh 但配置文件是
.zshrc,脚本有时候会写到.bash_profile里去。装完先type nvm试一下,报 not found 就说明写错文件了,手动把上面三行贴到正确的配置文件里,然后source ~/.zshrc。
如果你用 brew,命令是brew install nvm,但装完必须手动做两件事:创建~/.nvm目录,以及把上面那三行环境变量写进配置文件。brew 只是把 nvm 的文件放到了/opt/homebrew/opt/nvm,它不会自动帮你配置 shell。这是新手最容易漏的一步,装完敲 nvm 一点反应都没有。
Linux 上基本就是脚本那一套,没什么区别。要注意的是有些服务器上的/bin/sh是 dash 而不是 bash,这时候要把脚本下载下来用bash install.sh显式执行,否则会因为语法不兼容出错。
2.2 Windows:nvm-windows 的安装顺序很重要
Windows 上装 nvm 有一份必须遵守的顺序表,顺序错了就要重来:
- 先卸载已有的 node。控制面板里卸载,然后手动检查
C:\Program Files\nodejs和C:\Users\你的用户名\AppData\Roaming\npm是否还有残留,有就删掉。 - 下载 nvm-setup.exe,从项目的 Releases 页面下载,别从各种第三方站点下。
- 安装路径不要有空格和中文。默认的
C:\Users\张三\AppData\Roaming\nvm就是个坑,中文用户名会让某些脚本解析路径失败。改成C:\nvm。 - symlink 路径设成
C:\nodejs,安装向导里会让你选,别用默认的带空格的路径。 - 安装完成后,用管理员身份打开一个新的 PowerShell 或 cmd。
第 5 步不是可选项。nvm-windows 在执行nvm use时会去操作C:\nodejs这个目录链接,普通权限会报"无法创建符号链接"之类的错误。你可以把常用终端设成"始终以管理员身份运行",省得每次右键。
2.3 镜像配置:决定你下载快慢的两个参数
默认情况下 nvm 会去 nodejs.org 拉版本列表和安装包。网络到那边不稳定的时候,nvm install会卡住或者超时。这时候换镜像是最直接的办法。
macOS / Linux 上通过环境变量指定:
export NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node把这行加进你的 shell 配置文件,之后nvm ls-remote和nvm install都会走这个源。
Windows 上要改的是 nvm 安装目录下的settings.txt,内容大概是:
root: C:\nvm path: C:\nodejs node_mirror: https://npmmirror.com/mirrors/node/ npm_mirror: https://npmmirror.com/mirrors/npm/两个参数分工不同:node_mirror管 node 本体的下载,npm_mirror管装完 node 之后自动安装 npm 的那一步。只改 node_mirror 不改 npm_mirror 是个常见疏漏,表现是 node 装好了但 npm 装不上,然后npm -v报找不到模块。改完 settings.txt 要重启终端才生效。
实操心得:镜像不是万能药。如果你配了镜像之后
nvm ls-remote返回的列表明显比官网版本少,说明镜像同步滞后了。这时候要么等,要么临时把镜像参数注释掉走官方源装指定版本。判断方法是拿nvm ls-remote | tail -20的结果和 nodejs 官网的版本页对比一下。
3. 更新 node.js 的完整动作拆解
环境搭好了,更新这件事本身其实只有三步,但每一步都有细节。
3.1 查、装、切:三步走的标准动作
第一步,看当前状态和可用版本。
node -v # 看当前用的版本 nvm current # 同样是看当前版本,nvm 自己的命令 nvm ls # 看本地已经装了哪些版本 nvm ls-remote --lts # 看远程所有 LTS 版本nvm ls的输出值得单独说一下,它会用颜色和箭头标出当前正在使用的版本、默认版本(alias default 指向的)以及 lts 别名指向的版本。看到一长串列表的时候别慌,只要关注箭头指向的那个和你想装的那个就行。
第二步,装新版本。有三种写法:
nvm install 20.11.1 # 装精确版本 nvm install 20 # 装 20 这个大版本下的最新版 nvm install --lts # 装最新的 LTS 版本 nvm install --lts=iron # 装指定代号 LTS,iron 对应 Node 20我个人更推荐nvm install 20这种写法。理由是--lts拿到的是当前最新的 LTS,可能和你项目实际需要的版本差一个大版本;而写死精确版本号过一段时间又显得旧。写大版本号是个折中,既能拿到该系列的维护更新,又不会跨大版本。
这里有个细节:大版本号匹配到的一定是宿主机架构对应的包。Apple Silicon 的 Mac 上,nvm 会装darwin-arm64版本,而 Rosetta 环境下装的可能是darwin-x64。如果你在 M 系列芯片的 Mac 上发现 node 跑起来特别慢,先node -p "process.arch"看一下输出是 arm64 还是 x64,装错了就nvm uninstall重装一次。
第三步,切换并设为默认。
nvm use 20.11.1 # 当前 shell 生效 nvm alias default 20.11.1 # 新开终端默认用这个版本nvm use只影响当前这个终端会话。你关掉窗口再开一个,又会回到 default 指向的版本。所以「更新」这个动作要彻底完成,必须加上nvm alias default。很多人说"我明明切了版本,重开终端又变回去了",就是漏了这一步。
nvm alias default还有个实用写法是设成大版本:
nvm alias default 20这样以后nvm install 20.12.0之后,default 自动指向最新的 20.x,不用每次手动改别名。
3.2 全局包迁移:别让命令行工具凭空消失
这是版本更新里最重要也最容易被忽略的一步。nvm use切到新版本之后,你会发现tsc、nodemon、pm2、serve这些全局命令全都报 command not found。原因前面说过,全局包是按 node 版本目录隔离的,新版本下自然一个都没有。
nvm 提供了一条命令来搬:
nvm reinstall-packages 18.20.4执行它的时候,nvm 会做这几件事:读取旧版本目录下的全局模块列表,然后在当前版本下逐个重新安装。这里的"重新安装"不是复制文件,而是真的走一遍 npm install,所以它会从 registry 重新下载。网络不好的时候这一步会慢,配好 npm 镜像会快很多。
需要注意两点:
- 必须先
nvm use到目标版本,再执行这个命令。顺序反了会把新版包装到旧版本目录下; - 带 native 编译的全局包可能会失败,比如
node-gyp相关的东西。失败是正常的,因为编译产物和 node ABI 版本绑定,重装本来就该重新编译。看到报错就单独重装那个包,别一看到红色文字就以为整个迁移废了。
如果你不想用reinstall-packages,也可以先导出列表再手动装:
npm ls -g --depth=0 # 在旧版本下执行,把输出的包名记下来这个方法的好处是你能顺手筛掉那些早就不用的包。我自己的全局包常年维持在七八个,每次升级都是趁机会清理一遍,只装真正需要的。
3.3 用 .nvmrc 把版本钉在项目里
团队协作里,光靠口头说"用 node 20"是不靠谱的。.nvmrc文件的作用就是把这个约定写进仓库。
在项目根目录建一个.nvmrc,内容就一行:
20.11.1也可以在.nvmrc里写lts/iron或者20,nvm 都能解析。写具体版本号的好处是完全确定,写大版本号的好处是能自动拿到补丁更新,各有取舍。我倾向于在业务项目里写精确版本,在工具库项目里写大版本。
配好之后:
nvm use # 自动读取当前目录的 .nvmrc nvm install # 如果这个版本没装,会自动装要注意.nvmrc的查找是向上递归的。你在子目录里执行nvm use,它会一层层往上找,直到找到.nvmrc或者到根目录为止。这个行为在多包仓库里很有用,但也会造成困惑——你明明在 A 目录,生效的却是 B 目录的配置。搞不清楚的时候用nvm which current看看实际用的是哪个二进制。
.nvmrc之外,我还建议在package.json里加一个 engines 字段:
{ "engines": { "node": ">=20.11.0 <21" } }这个字段默认不会强制生效,需要在项目里加.npmrc并写入engine-strict=true才会让 npm 在版本不符时报错。对团队来说,这个组合能挡住大部分"我这跑得好好的啊"的扯皮。
3.4 收尾配置:default 版本、corepack 与 pnpm
新版本装好之后,还有几个收尾动作值得做。
确认 npm 版本跟着变了。node 和 npm 是绑定的,切换 node 版本之后 npm 也会换成对应的版本。node -v和npm -v一起看一下,如果 npm 还是老版本,多半是 PATH 里有别的 npm 在抢优先级,用which npm确认一下路径是不是在新的版本目录下。
启用 corepack 管理包管理器版本。如果你的项目用 pnpm 或 yarn,手动npm i -g pnpm会导致不同机器上的 pnpm 版本不一致,锁文件格式可能对不上。更规范的做法是用 corepack:
corepack enable corepack prepare pnpm@9.1.0 --activate然后在package.json里加"packageManager": "pnpm@9.1.0",corepack 会严格按照这个版本来运行。Node 16.9 之后自带 corepack,但较新的 Node 版本里 corepack 的打包策略有调整,如果执行corepack enable报找不到命令,就先npm i -g corepack装一下。
检查 npm 的全局前缀。执行npm config get prefix,输出应该指向当前 node 版本目录,类似~/.nvm/versions/node/v20.11.1。如果它指向了/usr/local或者别的系统目录,说明你在某次操作里改过这个配置,会导致全局包装错地方,甚至需要 sudo 才能装包。修正命令是npm config delete prefix,让它回落到默认值。
注意:不要用 sudo 执行 npm install -g。用 nvm 管理的环境下,全局目录本来就在你的用户目录里,不需要提权。一旦用 sudo 装过,文件属主变成 root,后面不带 sudo 就会报 EACCES,清理起来很烦。
4. 踩坑最多的地方:报错排查实录
前面讲的都是顺利路径,实际干活时大部分时间花在排查上。这一节把几个高频报错拆开讲。
4.1 "is not yet released or is not available" 到底在说什么
完整报错通常长这样:
node.js v24.21.0 is not yet released or is not available.第一反应是"我版本号写错了",但很多时候版本号是对的。这个报错的真实含义是:nvm 在它查询的版本索引里没找到你给的这个版本号。可能的原因有三个层次。
第一层,版本号确实不存在。比如你写nvm install 24.21.0,但实际上 24.x 系列根本还没出到 .21 这个补丁号。解决办法是nvm ls-remote看一眼真实存在的版本列表。注意ls-remote拉的是一个索引文件,输出很长,用nvm ls-remote | grep v24过滤一下更清爽。
第二层,本地 nvm 太老,索引里没有新版本。nvm-windows 尤其容易遇到,某些旧版本内置的版本解析逻辑不认识新的版本命名。这时候要升级 nvm 本身。nvm-windows 升级的方式是下载新版 exe 覆盖安装,安装路径选和原来一样的目录,它会提示"检测到已有安装,是否保留设置",选是,配置就都留着了。nvm-sh 的升级则要看当初怎么装的:脚本装的就重跑一遍 install 脚本,git clone 装的就在$NVM_DIR里git pull然后source一下。
第三层,镜像源没同步。如果你配了镜像,而镜像的索引文件还没更新到最新,就会出现官网有但本地查不到的情况。临时办法是注释掉镜像配置,走官方源装指定版本。
还有一个容易被忽略的:在 Windows 上不要用nvm install lts这种写法,nvm-windows 对 lts 别名的支持不如 nvm-sh 完整,直接用nvm list available查到的具体版本号更稳。
4.2 "node:util does not provide an export named" 的几种成因
这个报错的完整形态是:
The requested module 'node:util' does not provide an export named 'xxx'它属于 ESM 和 CommonJS 混用引发的经典问题,跟 node 版本更新有很强的关联性。要理解它,得先知道一个背景:node 在较新版本里对 ESM 的解析规则更严格了,一些在老版本下"碰巧能跑"的写法,升级之后就会被拦下来。
成因主要有这么几种:
一是把 CommonJS 模块当 ESM 导入。某个依赖包是用module.exports导出的,但你的代码或者另一个依赖用import { something } from 'xxx'去拿具名导出。老版本 node 会尝试做静态分析然后把属性名猜出来,新版本在某些情况下不再做这个猜测,直接报错。解决办法是改成默认导入:
// 报错的写法 import { readFile } from 'some-cjs-package'; // 可行的写法 import pkg from 'some-cjs-package'; const { readFile } = pkg;二是package.json里的type字段和文件后缀不匹配。项目type设成module之后,所有.js文件都会按 ESM 解析。这时候如果某个文件里用了require(),或者引入了只提供 CJS 入口的包,就会出问题。临时办法是把那类文件改成.cjs后缀,或者给那个包单独处理。
三是exports字段配置不完整。有些包在package.json里写了exports字段但只声明了require条件,没声明import条件。在 ESM 环境下引入时,node 找不到对应的入口,抛出各种奇怪的导出错误。这种情况只能等包作者修,或者用createRequire绕过:
import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const legacyPkg = require('legacy-package');四是 node 版本和包的 engine 要求不匹配。有些包明确要求 node 20 以上,你还在 18 上跑,它内部的 ESM 代码用了新版本才有的导出,自然报错。这种情况看包的 README 或者package.json里的 engines 字段就能确认。
排查这类问题的通用思路是:先定位是哪个import语句触发的(报错堆栈里会给出文件行号),然后npm ls 那个包名看版本,再去 node 的版本发布说明里查这个版本有没有调整 ESM 解析行为。升级大版本之前先在测试环境跑一遍构建和启动,比在生产上炸了再回滚要划算得多。
4.3 切换之后版本没变:PATH 与 shell 缓存的锅
症状很典型:nvm use 20.11.1输出显示成功,nvm current也显示 20.11.1,但node -v还是 16.20.0。
按顺序排查这几项:
第一,检查 PATH 里的优先级。执行which node(Windows 用where node),看输出的路径是不是指向~/.nvm/versions/node/v20.11.1/bin/node。如果指向/usr/local/bin/node或者/usr/bin/node,说明系统里还有一个独立安装的 node,它在 PATH 里的位置比 nvm 的路径靠前。macOS 上常见于之前用 pkg 安装包装过 node,Linux 上常见于用 apt 或 yum 装过。
处理办法是在 shell 配置文件里把 nvm 的初始化放到最后,或者手动把系统 node 卸掉。Linux 上可以考虑把系统 node 的软链接删掉,但要注意有些系统工具依赖它,删之前先确认。
第二,检查 shell 有没有缓存命令路径。bash 和 zsh 都会缓存命令的位置,切版本之后缓存没更新,就会一直执行旧的二进制。执行hash -r清空缓存,再试一次。
第三,检查是否在 tmux 或 IDE 的内置终端里。这些环境可能不加载你的 shell 配置文件。IDE 内置终端尤其常见,需要在 IDE 设置里指定"以登录 shell 运行",或者手动 source 一下配置文件。
第四,Windows 上确认是否用了管理员权限。前面提过,nvm-windows 需要管理员权限来创建目录链接,权限不够时nvm use会静默失败或者只改一半。
4.4 报错速查表
把常见问题和对应的处理动作整理成一张表,遇到问题可以先扫一眼。
| 报错或现象 | 大概率原因 | 处理动作 |
|---|---|---|
nvm: command not found | shell 配置没加载 | 检查.zshrc/.bashrc里的 NVM_DIR 配置,source一下 |
nvm use无输出且版本没变 | 权限不足(Windows) | 用管理员身份重开终端 |
node -v与nvm current不一致 | PATH 里有其他 node | which node确认路径,清理系统 node |
is not yet released or is not available | 版本号不存在或索引过期 | nvm ls-remote查真实版本,升级 nvm 本体 |
| 全局命令全部失效 | 全局包未迁移 | nvm reinstall-packages <旧版本> |
npm install -g报 EACCES | 全局目录属主被改 | npm config delete prefix,检查目录属主 |
does not provide an export named | ESM/CJS 混用 | 改默认导入、调 type 字段、换包版本 |
nvm install卡住不动 | 下载源不通 | 配 node_mirror 和 npm_mirror |
| 切换版本后 npm 版本异常 | npm 未随之切换或 PATH 冲突 | 检查which npm,必要时重装 node 版本 |
实操心得:排查这类问题时,养成先收集环境信息的习惯——
nvm -v、node -v、npm -v、which node、echo $PATH,这五条命令的输出基本能覆盖八成问题的定位。把它们存成一个 shell 别名,出问题一键打印,比一条条敲快得多。
5. 更新之后怎么验收,以及团队里怎么统一
装好了、切过去了,不代表事情结束。node 大版本更新带来的行为变化,很多要到运行时才暴露。
5.1 一份可以直接抄的升级自检清单
每次升级 node 大版本之后,我会按这个顺序过一遍。整套下来十分钟,能挡掉大部分上线后的意外。
第一步,基础信息核对。node -v、npm -v、nvm current三个输出要对得上,which node的路径要指向 nvm 的版本目录。这一步确认环境本身没问题。
第二步,全局工具逐个验证。把常用的全局命令挨个敲一遍,tsc -v、pm2 -v、nodemon -v。有报错的当场重装,别拖。
第三步,项目依赖重装。这一步争议比较大,但因为 native 模块和 node ABI 版本强绑定,跨大版本升级时删掉node_modules重装是最省心的做法:
rm -rf node_modules package-lock.json npm install不删也不是不行,但如果你后面遇到莫名其妙的NODE_MODULE_VERSION报错,那就是这个原因。
第四步,跑一遍完整构建。npm run build能暴露出语法层面的兼容问题,比如某些转换工具对新版 node 的支持。
第五步,启动服务并检查启动日志。重点看有没有 deprecation warning,比如某些 API 在新版本被标记废弃。警告当下不影响运行,但下一个大版本可能就删了,现在记下来比以后临时改好。
第六步,跑一遍测试。有自动化测试的项目直接跑,没有的话至少手点一遍核心链路。这一步能发现那些只在运行时才触发的行为差异。
第七步,观察资源占用。新版 node 在内存管理和 V8 引擎上都有调整,启动内存和 CPU 占用可能和你之前的经验值不一样。如果你有基于内存阈值做告警的监控,升级后要重新校准一下阈值。
5.2 CI 与多人协作里的版本策略
本地跑通了,CI 上翻车是另一个高频场景。因为 CI 环境的 node 版本通常是写死的,你本地升级了,流水线里可能还是旧的。
GitHub Actions 里的写法有两种。一种是显式指定:
- uses: actions/setup-node@v4 with: node-version: 20.11.1 cache: npm另一种是读.nvmrc,让本地和 CI 保持同一个来源:
- uses: actions/setup-node@v4 with: node-version-file: .nvmrc cache: npm我更推荐第二种。单一数据源的好处是不会有"本地 .nvmrc 写 20,CI 里写 18"这种不一致。改了.nvmrc就是全链路更新,改漏了 CI 会直接报错,比静默跑在错误版本上强得多。
团队层面还有一件事要做:在 README 或者 CONTRIBUTING 里把版本要求写清楚,包括最低版本、推荐版本、以及修复某个问题时依赖的最低补丁版本。新人入职时跟着文档走一遍,比在群里问十次有效。
如果你的团队规模再大一点,可以考虑引入 Volta 或者在 CI 里加一个版本校验的步骤,在构建开始前就跑node -v并和.nvmrc比对,不一致直接 fail。这个检查几行脚本就能写完,能省掉大量"我这边能跑啊"的沟通成本。
5.3 几个我一直用的实操习惯
最后分享一些具体到操作的细节,都是踩坑之后形成的肌肉记忆。
升级前先记录当前版本。在项目目录下node -v > .node-version-backup,或者干脆在笔记里记一下。真出问题要回退的时候,你至少知道自己是从哪个版本升上来的。这个习惯帮我省过至少两次时间。
不要一次跨太多大版本。从 16 直接跳到 22,中间跨了 18、20 两个 LTS,遇到的兼容问题会成倍增加。如果需要跨越多个大版本,建议中间用 18 或者 20 过渡验证一次,把问题分批解决。虽然听起来麻烦,但定位问题的效率高得多。
保留最近两个版本不删。nvm uninstall能清理磁盘,但我不建议升完就删旧的。留一到两个版本,遇到紧急问题能一秒切回去,等新版本稳定跑上一两周再清理。
把nvm use挂到 cd 的钩子上。zsh 用户可以在配置文件里加一段,让每次切换目录时自动读取.nvmrc并切换版本:
autoload -U add-zsh-hook load-nvmrc() { local node_version="$(nvm version)" local nvmrc_path="$(nvm_find_nvmrc)" if [ -n "$nvmrc_path" ]; then local nvmrc_node_version=$(nvm version "$(cat "${nvmrc_path}")") if [ "$nvmrc_node_version" = "N/A" ]; then nvm install elif [ "$nvmrc_node_version" != "$node_version" ]; then nvm use fi elif [ "$node_version" != "$(nvm version default)" ]; then nvm use default fi } add-zsh-hook chpwd load-nvmrc load-nvmrc配好之后,进项目目录自动切到项目要求的版本,出目录自动回到默认版本。在多项目并行的时候这个体验提升非常明显,不用再记"这个项目现在是 18 还是 20"。
定期看一眼 nvm 的版本。nvm -v的输出如果是半年前的老版本,很可能已经不认识新发布的 node 版本了。我一般两三个月检查一次,有新版本就顺手升一下。
用nvm exec临时跑其他版本的代码。有时候你需要用 node 18 跑一个脚本,但不想切换整个环境,可以这样:
nvm exec 18 node some-script.js它会在 18 的环境下执行这个命令,执行完当前 shell 的版本不受影响。做版本对比测试的时候特别顺手。
善用nvm which。nvm which 20.11.1会打印出这个版本 node 二进制的绝对路径。排查 PATH 冲突的时候,这个命令能直接告诉你 nvm 认为的路径和实际执行的是不是同一个。我排查问题基本都会先跑这一条。
注意:
.nvmrc里的版本号如果写得很精确,团队里每个人都要装同一个版本,磁盘占用会上去。如果团队人多、机器磁盘紧张,可以考虑在.nvmrc里写大版本号,配合package.json的 engines 字段做范围约束,兼顾一致性和灵活性。
最后再提一个容易被忽略的细节:node 版本升级之后,npm 的缓存目录不会自动跟着变。npm cache默认在用户目录下的.npm文件夹里,跨版本共用是没问题的,但如果你在升级过程中遇到过奇怪的包损坏问题,npm cache verify或者npm cache clean --force是值得试的一步。我自己遇到过一次升级后某个包一直装不上的情况,折腾半天换镜像换 registry 都没用,最后清了一下缓存就好了。