打开终端,cd 进一个几个月没碰的老项目,跑 npm install 结果报了一堆错,仔细一看,Node 版本不对。这种场景我遇到过太多次了。以前用 nvm 的时候,切版本要 nvm use 16.20.2,然后看着终端卡顿一下,半天才切过去。项目多、标签页开得勤的时候,这种延迟真的会被反复放大。后来换了 fnm,第一感觉就是“快”,切版本几乎是无感的,而且 Windows、macOS、Linux 三端行为一致,团队协作再也不用为 shell 差异吵来吵去。这篇文章就围绕 fnm 展开,从安装、初始化、核心命令到各种坑的排查方式都过一遍,适合所有被 nvm 启动速度折磨过的 Node.js 开发者,也适合刚接触 Node.js、正在纠结版本管理器怎么选的新手。
1. 被 nvm 的慢和乱逼疯后,我为什么转向 fnm
1.1 nvm 的三大痛点
先说说 nvm 的问题,不然很多人不理解“换工具”的意义在哪。第一个痛点是启动慢。nvm 本质是一堆 shell 脚本,每次打开新终端,它都要执行一遍完整初始化逻辑,加载函数、扫描已安装版本、设置 PATH。在插件很多、机器配置一般的情况下,这个初始化轻松突破一秒,甚至更久。你可能觉得一秒没什么,但当你在多个项目之间切来切去、频繁开新终端时,这种延迟会变成每天都得忍受的慢性折磨。
第二个痛点是跨平台体验割裂。nvm 最初是给类 Unix 系统设计的,Windows 上用的是社区维护的 nvm-windows,虽然名字像,但命令细节、配置文件路径、版本解析逻辑都和原版不完全一致。团队里有人用 macOS、有人用 Windows,光是文档就得写两份,而且老版本的 nvm-windows 对新生 Node 版本的支持经常滞后,遇到解析不了的版本号只能干瞪眼。更尴尬的是,如果你在 Windows 的 Git Bash 里用 nvm-windows,很多行为会和 PowerShell 里不一致,这种不确定性很劝退。
第三个痛点是自动化难。nvm 切换版本依赖 shell source 机制,必须在你当前终端进程里生效,想在脚本里静默切换、想在 CI 里做版本管理,都得绕很多弯。相比之下,fnm 是一个独立二进制,天然适合被脚本调用,这是它被很多工程化团队接受的重要原因。
1.2 fnm 为什么能“极速”
fnm 全称 Fast Node Manager,用 Rust 写的,编译出来就是单个原生二进制,没有任何运行时依赖。Rust 的性能优势在启动速度上体现得非常直接,实测下来开终端几乎不感知初始化过程。更重要的是版本切换机制的差异,fnm 维护一个 Node 版本安装目录,然后用符号链接指向当前应激活的版本,切换版本本质上只是改一次链接指向,所以我切版本时基本是毫秒级反馈,完全没有 nvm 那种“卡一下”的感觉。
除了切换快,fnm 还支持并行下载和本地缓存,安装新版本 Node 时速度也有提升。它内部默认从 Node.js 官方源拉取版本列表和压缩包,但你完全可以用镜像环境变量覆盖下载地址,这个后面我会说具体怎么配。另外,fnm 的命令行设计跟 nvm 高度相似,老用户迁移成本很低,这也是它能在开发者社区快速扩散的原因之一。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 安装 fnm:Windows、macOS、Linux 三种姿势
2.1 Windows 上安装 fnm
Windows 用户最省事的方案是用 winget,这是 Windows 10/11 自带的包管理器,直接执行:
powershell复制winget install Schniz.fnm
如果平时用 Scoop,也可以:
powershell复制scoop install fnm
用 Chocolatey 的话:
powershell复制choco install fnm
我个人的建议是优先用 winget 或 Scoop,别问,问就是升级省心。很多人喜欢去 GitHub Releases 手动下载 Windows 的 exe 压缩包,解压后丢到某个目录自己配 PATH,这种做法不是不行,但后续每次升级你都得手动来一遍,麻烦不说还容易漏。包管理器一把梭,升级不过是 winget upgrade Schniz.fnm 或 scoop update fnm 的事。
安装完成后先别急着用,因为 fnm 这时候还没有把环境变量注入到你的 PowerShell 会话里。这一步不是让你手动改系统环境变量,而是用 fnm 自带的 env 生成器统一处理,具体见 2.3。
注意:Windows 下安装完 fnm 后,如果后续执行脚本时报“因为在此系统上禁止运行脚本”,别慌,这不是 fnm 的问题,是 PowerShell 执行策略的限制,后面避坑章节会专门讲处理方式。
2.2 macOS 和 Linux 的安装方式
macOS 上用 Homebrew,一行命令:
bash复制brew install fnm
Linux 上比较通用的方式是官方安装脚本:
bash复制curl -fsSL https://fnm.vercel.app/install | bash
这个脚本会把 fnm 装到 ~/.fnm 目录,并尝试往你的 shell 配置文件里写入初始化片段。不过我提醒一句,脚本对 bash 和 zsh 的支持比较成熟,如果你用的是 fish,它不一定能自动覆盖,建议手动按 fish 的语法配置。也有一些发行版的包管理器直接收录了 fnm,比如 Debian/Ubuntu 的 apt 仓库里如果已经带了 fnm,你可以直接 apt install fnm,但我实测下来仓库版本往往偏老,不如脚本或 Homebrew/Linuxbrew 版本新。
安装完 fnm 之后有个非常常见误区,很多人以为 fnm 装完就可以 node -v 了,实际上 fnm 只是版本管理器,它不携带任何 Node 运行时。你还需要用 fnm install 安装一个具体版本的 Node,这一点和 nvm 是一样的逻辑。
2.3 初始化 shell 环境:让 fnm 真正生效
这是安装过程中最重要的一步。fnm 是一个独立二进制,它必须把安装目录、当前版本链接路径注入到你的 shell 会话里,node 命令才会被解析。以 PowerShell 为例,编辑你的 $PROFILE 文件:
powershell复制notepad $PROFILE
如果提示文件不存在,先执行:
powershell复制New-Item -Path $PROFILE -Type File -Force
然后在文件末尾加入:
powershell复制fnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression
保存后,在当前会话里重新加载配置:
powershell复制. $PROFILE
或者干脆重开一个终端。--use-on-cd 参数很关键,它让 fnm 在你 cd 进一个包含 .nvmrc 文件的目录时自动切换对应 Node 版本,这是提升日常效率的核心特性之一。
macOS 和 Linux 的 bash/zsh 用户则在 ~/.bashrc 或 ~/.zshrc 里加:
bash复制eval "$(fnm env --use-on-cd)"
fish 用户:
fish复制fnm env --use-on-cd --shell fish | source
为什么要这么绕?因为 fnm use 只影响当前 shell 进程的环境变量,新开的终端是全新进程,你不做初始化它根本不知道当前激活的版本在哪。fnm env 的作用就是把这套环境信息“打印”出来,再由 shell 执行注入。理解了这个原理,很多后续问题你都能自己排查。
3. 上手实操:5 分钟搞定 Node.js 版本安装与切换
3.1 一个完整的实战流程
下面演示从零开始的完整流程,目标是用 fnm 安装 Node 22 LTS,并把它设成默认版本。
第一步,先看远端有哪些 LTS 版本可用:
bash复制fnm ls-remote --lts
输出会列出很多 LTS 版本号,选一个最新的即可,或者直接安装:
bash复制fnm install --lts
如果项目需要指定版本,比如要 22.14.0:
bash复制fnm install 22.14.0
也可以只给大版本号,fnm 会自动匹配该大版本下最新子版本:
bash复制fnm install 22
这个行为比 nvm 友好,省去手动找具体版本号的麻烦。第二步,查看本地已安装列表:
bash复制fnm ls
输出会标注哪些是当前版本、哪些是默认版本,清晰度很高。第三步,切换并使用:
bash复制fnm use 22
node -v
第四步,设置全局默认版本:
bash复制fnm default 22
完成后新开的终端默认就是 Node 22。如果你在 Windows 上设了 default 却始终无效,十有八九是 fnm env 没进 $PROFILE,回 2.3 检查。
提示:fnm 默认从 Node.js 官方源下载,国内环境速度不稳定。可以通过环境变量
FNM_NODE_DIST_MIRROR指定镜像源,比如设置为https://npmmirror.com/mirrors/node/,速度差距非常明显。设置方法是在系统环境变量里新增这一项,然后重开终端。
3.2 从 nvm 迁移到 fnm 的命令对照表
很多读者是 nvm 老用户,我整理了一份高频命令对照表,可以直接照着抄:
| 功能 | nvm 命令 | fnm 命令 |
|---|---|---|
| 安装最新 LTS | nvm install --lts |
fnm install --lts |
| 安装指定版本 | nvm install 18.20.0 |
fnm install 18.20.0 |
| 切换版本 | nvm use 18 |
fnm use 18 |
| 已安装版本列表 | nvm ls |
fnm ls |
| 远程版本列表 | nvm ls-remote |
fnm ls-remote |
| 显示当前版本 | nvm current |
fnm current |
| 设置默认版本 | nvm alias default 18 |
fnm default 18 |
| 卸载版本 | nvm uninstall 18 |
fnm uninstall 18 |
| 项目版本声明文件 | .nvmrc |
.nvmrc,原生支持 |
整体映射关系非常简单,唯一需要适应的是 fnm 的 alias 体系和 nvm 的 alias 体系在细节上有差异,fnm 的 alias 更像是给版本起别名,默认版本单独由 fnm default 管理。
3.3 用 .nvmrc 把版本管理固化到项目里
这才是版本管理器的进阶玩法。在项目根目录创建 .nvmrc 文件,内容只要一行版本号:
code复制22.14.0
也可以只写主版本:
code复制18
配合 --use-on-cd,每次 cd 进这个目录,fnm 会自动读取该文件并切换版本。我维护多个项目时,有一个 Vue 2 老项目必须锁 Node 16,还有一个新的 Vite 项目用 Node 22,以前全靠脑子记“进哪个目录切哪个版本”,切换次数多了总会出错,现在全交给 fnm 自动处理。团队协作时把 .nvmrc 提交进 Git 仓库,所有成员 clone 后 cd 进目录就能自动切到统一版本,彻底消灭“在我本地是好的”这种扯皮现象。注意 .nvmrc 的格式约定,尾随换行没问题,但别写 v22.14.0 这种带 v 前缀的写法,虽然部分版本能识别,但不是标准格式。
4. fnm 进阶:alias、默认版本与自动化配置
4.1 默认版本和别名的管理
fnm 的 alias 命令可以给版本起一个可读性更强的名字:
bash复制fnm alias 22.14.0 stable-project
fnm use stable-project
别名的价值不只是少敲几个字母,而是让自动化脚本和文档更语义化。比如你有两套环境:一套给业务系统用 Node 20,另一套给内部工具用 Node 18,分别建别名,然后用脚本参数控制切换,比每次手动改 fnm default 要清晰得多。日常管理时可以用:
bash复制fnm ls --verbose
查看本地版本的详细信息,包括当前的默认版本标记和别名列表。
有一点需要明确:fnm default 和 fnm alias 的职责不同。default 决定新终端启动时初始用哪个版本;alias 更像一个可复用的标签,你可以让多个 alias 指向同一个版本,也可以在不同项目里引用不同 alias。对个人开发者来说,初期只设 default 足够,但项目一多、环境一复杂,alias 就会变得很香。
4.2 在 CI/CD 和 Docker 场景中使用 fnm
fnm 在工程化场景里同样能打。GitHub Actions 里可以直接用官方 action:
yaml复制- name: Setup fnm
uses: Schniz/fnm-action@v1
with:
version-file: .nvmrc
这个 action 会读取仓库根目录的 .nvmrc,自动安装并激活对应 Node 版本,简洁可靠。如果你不想依赖第三方 action,也可以在自己的 CI 脚本里手动跑:
bash复制eval "$(fnm env)"
fnm install
fnm use
因为 fnm 会自动检测当前目录的 .nvmrc,所以 install 和 use 不带参数也能正确解析版本。Dockerfile 里同样可以先安装 fnm 再安装 Node,但这里我要说句实话:容器环境如果只需要一个固定版本,直接用官方 node 镜像更简单,没必要套 fnm。只有当你需要在同一个容器里动态切换多个 Node 版本时,fnm 才值得引入。工程选型要讲究实际收益,别为了用而用。
5. 避坑指南:我踩过的那些 fnm 的坑
5.1 “not yet released”报错的真正原因
很多人在社区问 error installing 24.20.0: node.js v24.20.0 is not yet released or is not available,我第一次遇到也愣了半天。这个报错的本质原因就两类:版本号敲错,或者版本确实还没出现在 fnm 能获取到的官方列表里。解决思路很简单,先执行:
bash复制fnm ls-remote
看看实际可用的版本号,再安装。还有一种隐蔽情况是 fnm 版本太旧,它内置的版本列表解析逻辑没能同步官方最新数据,这时候升级 fnm 就好。我踩过几次之后养成了一个习惯:安装前总先查一下版本列表,绝不凭记忆填版本号,别看这个习惯简单,真能省不少时间。
5.2 Windows 权限与 PowerShell 执行策略问题
Windows 用户最容易遇到两个问题。第一个是 PowerShell 执行策略报“因为在此系统上禁止运行脚本”,用管理员身份打开 PowerShell,执行:
powershell复制Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
然后重开终端。第二个问题是符号链接权限。fnm 在切换版本时需要创建或修改符号链接,如果当前终端没有管理员权限,可能报错“请求的操作需要提升”。我的经验是:初次安装或设置 default 版本时,用管理员身份终端跑一次相关命令,让 fnm 把版本目录和链接建好,之后日常开发完全不用管理员权限。千万不要长时间用管理员终端写代码,没必要,安全问题反而更大。
5.3 新终端不生效、VSCode 识别不到的排查思路
这类问题我见过太多次,其实排查路径非常固定。
第一步,检查你的 $PROFILE 或 .bashrc/.zshrc 里有没有 fnm env 那一行,没有就补上;第二步,新开终端执行 fnm env,看输出是否正常;第三步,执行 fnm current 确认当前版本;第四步,确认项目目录下是否存在 .nvmrc,并且内容格式正确。
VSCode 识别不到版本,绝大多数情况是 IDE 内置终端没有重新加载 shell 配置。在 VSCode 里点垃圾桶图标关闭终端再重开,或者直接执行 reload window,问题基本能解决。如果还不行,检查是否装了多个 shell 相关插件缓存了环境变量,这时重启 VSCode 是最快的解法。
还有一个容易忽略的坑:fnm 每个 Node 版本有独立的全局目录,你用 npm i -g 安装的全局工具只在当前版本下可见,切到另一个版本后这些命令会“消失”。这不是 bug,是刻意设计,目的是隔离版本之间的全局依赖。解决办法有两个:一是尽量使用项目本地依赖,二是在需要全局工具的版本上单独执行一次 npm i -g。我自己更倾向第一种,毕竟全局包装多了本身就是一种环境债。
6. 从 nvm 迁移到 fnm 之后的一些个人体会
用了一段时间 fnm 之后,我最大的感觉是“工具最好的状态就是让你感受不到它的存在”。它不会在每次打开终端时刷一屏初始化脚本,也不会为切换版本多等那一下。如果你现在还在用 nvm,并且觉得启动慢还在忍受范围内,那我不劝你非得换;但如果你要跨平台协作,或者已经被多项目版本管理搞得焦头烂额,我真的建议你花十分钟按这篇文章把 fnm 配起来。最后说一个我个人非常推荐的日常习惯:所有新建项目,第一件事就是提交一个 .nvmrc,把版本号写清楚。这个文件看着不起眼,却是所有避坑经验里性价比最高的一条,它能让你过三个月、换一台电脑、换一个团队之后,依然用一个 cd 命令就走回正确的技术栈起点。
