从 nvm 到 fnm:一个前端老兵的版本管理工具迁移实录

从 nvm 到 fnm:一个前端老兵的版本管理工具迁移实录

引子:那个让我崩溃的早晨

某个周一早晨,我像往常一样打开终端准备开始工作。

bash 复制代码
# 打开 iTerm2... 等待... 等待...
# 终于出现命令提示符,耗时 2.3 秒

接着切换到一个需要 Node 16 的老项目:

bash 复制代码
$ nvm use 16
Now using node v16.20.2 (npm v8.19.4)
# 又是 0.5 秒过去了

然后跳到另一个 Node 20 的新项目:

bash 复制代码
$ cd ../new-project
$ nvm use 20
Now using node v20.19.6 (npm v10.8.2)
# 再等 0.5 秒

一天下来,在 5-6 个项目间来回切换,光是等 nvm 响应就浪费了不知道多少时间。更别提有时候忘记 nvm use,直接 npm install 导致 node_modules 版本混乱的惨剧了。

是时候做出改变了。

为什么要离开 nvm?

痛点一:Shell 启动慢得令人发指

nvm 是纯 Bash 脚本实现的。每次打开新终端,它都要:

  1. 加载 nvm.sh 脚本(几千行)
  2. 解析已安装的 Node 版本
  3. 设置环境变量
  4. 初始化自动补全

我用 time 测了一下我的 .zshrc 加载时间:

bash 复制代码
# 有 nvm
$ time zsh -i -c exit
zsh -i -c exit  0.42s user 0.23s system 95% cpu 0.678 total

# 注释掉 nvm 后
$ time zsh -i -c exit
zsh -i -c exit  0.08s user 0.05s system 92% cpu 0.142 total

nvm 让我的终端启动慢了近 5 倍!

痛点二:版本切换不够智能

每次进入不同项目都要手动 nvm use,太原始了。虽然有 avnnvm-auto 等插件可以实现自动切换,但:

  • 又增加了一层依赖
  • 又拖慢了 Shell 速度
  • 配置起来也挺麻烦

痛点三:Windows 支持是个笑话

nvm 官方根本不支持 Windows。nvm-windows 是另一个独立项目,命令和行为都有差异。团队里用 Windows 的同事经常遇到各种奇怪问题。

痛点四:偶发的诡异 Bug

用了几年 nvm,遇到过各种奇怪问题:

  • 全局安装的包莫名消失
  • npm prefix 路径错乱
  • 多个终端窗口版本不同步
  • .nvmrc 有时候不生效

遇见 fnm:Rust 带来的性能革命

fnm(Fast Node Manager)是用 Rust 写的 Node.js 版本管理器。第一次用的时候,我的反应是:

"就这?结束了?怎么这么快?"

性能实测对比

我在自己的 M1 MacBook Pro 上做了详细测试:

终端启动时间
bash 复制代码
# nvm
$ time zsh -i -c exit
0.678 total

# fnm
$ time zsh -i -c exit
0.089 total

# 提升:7.6 倍
版本切换时间
bash 复制代码
# nvm
$ time nvm use 20
Now using node v20.19.6
real    0m0.347s

# fnm
$ time fnm use 20
Using Node v20.19.6
real    0m0.012s

# 提升:29 倍
安装新版本
bash 复制代码
# nvm install 22
# 总耗时约 45 秒(包含下载)

# fnm install 22
# 总耗时约 38 秒(包含下载)

# 下载速度差不多,但 fnm 的解压和配置更快

为什么 fnm 这么快?

  1. 原生二进制:Rust 编译成机器码,不需要解释器
  2. 并行处理:充分利用多核 CPU
  3. 惰性加载:只在需要时才读取版本信息
  4. 高效的文件操作:Rust 的 I/O 性能本就出色

真实场景:fnm 如何改变我的工作流

场景一:多项目并行开发

我同时维护着这些项目:

项目 Node 版本 原因
老后台系统 14 历史包袱,依赖不支持高版本
主站前端 18 稳定的 LTS
新管理后台 20 需要新特性
实验性项目 22 尝鲜最新 API
Electron 应用 18 Electron 版本限制

以前用 nvm:

bash 复制代码
$ cd legacy-admin
$ nvm use  # 等待...
Found '/Users/me/legacy-admin/.nvmrc' with version <14>
Now using node v14.21.3

$ cd ../main-site
$ nvm use  # 又等待...
Found '/Users/me/main-site/.nvmrc' with version <18>
Now using node v18.20.8

# 经常忘记 nvm use,然后...
$ npm install
# 装了一堆错误版本的依赖 💥

现在用 fnm:

bash 复制代码
$ cd legacy-admin
Using Node v14.21.3  # 自动切换,瞬间完成

$ cd ../main-site
Using Node v20.19.6  # 无感切换

# 永远不会忘记切换版本,因为是自动的

场景二:CI/CD 环境统一

我们团队的 CI 配置以前是这样的:

yaml 复制代码
# .gitlab-ci.yml (使用 nvm)
before_script:
  - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
  - export NVM_DIR="$HOME/.nvm"
  - . "$NVM_DIR/nvm.sh"
  - nvm install
  - nvm use

换成 fnm 后:

yaml 复制代码
# .gitlab-ci.yml (使用 fnm)
before_script:
  - curl -fsSL https://fnm.vercel.app/install | bash
  - eval "$(fnm env)"
  - fnm install
  - fnm use

CI 构建时间减少了约 15 秒(主要是 nvm 初始化太慢)。

场景三:团队协作与跨平台

我们团队成员使用的系统:

  • 60% macOS
  • 30% Windows
  • 10% Linux

nvm 时代的痛苦:

bash 复制代码
# macOS/Linux 同事
$ nvm use 20

# Windows 同事(nvm-windows)
$ nvm use 20.19.6  # 必须写完整版本号!
# 而且 .nvmrc 经常不生效

fnm 时代的统一:

bash 复制代码
# 所有平台,相同命令,相同行为
$ fnm use 20

Windows 同事终于不用单独维护一套文档了。

场景四:Docker 开发环境

在 Dockerfile 中安装 Node:

dockerfile 复制代码
# 以前用 nvm(不推荐,但有人这么干)
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash \
    && . ~/.nvm/nvm.sh \
    && nvm install 20 \
    && nvm alias default 20
# 镜像体积大,层数多

# 现在用 fnm
RUN curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir /usr/local/bin \
    && fnm install 20 \
    && fnm default 20
# 更简洁,体积更小

场景五:Monorepo 中的多版本需求

我们有一个 Monorepo,不同 package 需要不同 Node 版本:

bash 复制代码
monorepo/
├── packages/
│   ├── legacy-sdk/        # 需要 Node 14(兼容老用户)
│   │   └── .node-version  # 14
│   ├── web-app/           # 需要 Node 20
│   │   └── .node-version  # 20
│   └── cli-tool/          # 需要 Node 18
│       └── .node-version  # 18
└── .node-version          # 20(默认)

fnm 的 --use-on-cd 让我在不同 package 间跳转时完全无感:

bash 复制代码
$ cd packages/legacy-sdk
Using Node v14.21.3

$ cd ../web-app
Using Node v20.19.6

$ cd ../cli-tool
Using Node v18.20.8

完整迁移指南

第一步:安装 fnm

macOS (Homebrew):

bash 复制代码
brew install fnm

Windows (Scoop):

powershell 复制代码
scoop install fnm

Windows (Chocolatey):

powershell 复制代码
choco install fnm

Linux/macOS (curl):

bash 复制代码
curl -fsSL https://fnm.vercel.app/install | bash

Cargo (Rust 用户):

bash 复制代码
cargo install fnm

第二步:配置 Shell

这是最关键的一步,配置正确才能享受自动切换的便利。

Zsh (~/.zshrc):

bash 复制代码
# fnm - Fast Node Manager
eval "$(fnm env --use-on-cd --shell zsh)"

Bash (~/.bashrc):

bash 复制代码
# fnm - Fast Node Manager
eval "$(fnm env --use-on-cd --shell bash)"

Fish (~/.config/fish/config.fish):

fish 复制代码
# fnm - Fast Node Manager
fnm env --use-on-cd --shell fish | source

PowerShell ($PROFILE):

powershell 复制代码
# fnm - Fast Node Manager
fnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression

参数说明:

参数 作用
--use-on-cd 进入目录时自动读取 .node-version.nvmrc 并切换
--shell <shell> 指定 Shell 类型,生成对应的环境变量设置命令
--version-file-strategy recursive 向上递归查找版本文件(可选)
--corepack-enabled 自动启用 Corepack(可选)

第三步:迁移已安装的 Node 版本

查看 nvm 安装了哪些版本:

bash 复制代码
ls ~/.nvm/versions/node/
# v10.24.1 v14.21.3 v16.20.2 v18.20.8 v20.19.6 v22.21.0

在 fnm 中安装对应版本:

bash 复制代码
# 方式一:通过 LTS 代号安装(推荐,自动获取最新补丁版本)
fnm install lts/fermium   # v14.x
fnm install lts/gallium   # v16.x
fnm install lts/hydrogen  # v18.x
fnm install lts/iron      # v20.x
fnm install lts/jod       # v22.x

# 方式二:通过大版本号安装
fnm install 14
fnm install 16
fnm install 18
fnm install 20
fnm install 22

# 方式三:安装精确版本
fnm install 18.20.8
fnm install 20.19.6

设置默认版本:

bash 复制代码
fnm default 22

第四步:处理全局 npm 包

这是很多人忽略的一步!nvm 下安装的全局包不会自动迁移。

查看 nvm 中的全局包:

bash 复制代码
# 切换到 nvm 的某个版本
export PATH="$HOME/.nvm/versions/node/v20.19.6/bin:$PATH"
npm list -g --depth=0

在 fnm 的对应版本中重新安装:

bash 复制代码
fnm use 20
npm install -g typescript ts-node nodemon pm2 # 你需要的包

Pro Tip: 可以写个脚本批量处理:

bash 复制代码
#!/bin/bash
# migrate-global-packages.sh

# 你常用的全局包
PACKAGES="typescript ts-node nodemon pm2 pnpm yarn"

for version in 18 20 22; do
  echo "Installing global packages for Node $version..."
  fnm use $version
  npm install -g $PACKAGES
done

第五步:注释掉 nvm 配置

编辑你的 Shell 配置文件:

bash 复制代码
# ~/.zshrc 或 ~/.bashrc

# nvm - 已迁移到 fnm,注释掉避免冲突
# export NVM_DIR="$HOME/.nvm"
# [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
# [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"

第六步:验证迁移结果

bash 复制代码
# 重新加载配置
source ~/.zshrc  # 或 source ~/.bashrc

# 验证 fnm 工作正常
fnm list
fnm current
node -v
npm -v
which node

# 测试自动切换
cd /path/to/project-with-nvmrc
# 应该看到 "Using Node vX.X.X"
node -v

第七步:删除 nvm(可选但推荐)

确认一切正常后,可以删除 nvm 释放磁盘空间:

bash 复制代码
rm -rf ~/.nvm

# 如果遇到权限问题
sudo rm -rf ~/.nvm

命令速查表

功能 nvm 命令 fnm 命令
安装指定版本 nvm install 20 fnm install 20
安装最新 LTS nvm install --lts fnm install --lts
安装指定 LTS nvm install --lts=iron fnm install lts/iron
切换版本 nvm use 20 fnm use 20
设置默认版本 nvm alias default 20 fnm default 20
查看已安装版本 nvm ls fnm list
查看远程可用版本 nvm ls-remote fnm list-remote
查看当前版本 nvm current fnm current
卸载版本 nvm uninstall 18 fnm uninstall 18
在指定版本执行命令 nvm exec 18 node -v fnm exec --using=18 node -v

LTS 版本代号参考

代号 版本 发布日期 LTS 开始 维护结束 状态
Jod v22.x 2024-04 2024-10 2027-04 ✅ Active LTS
Iron v20.x 2023-04 2023-10 2026-04 ✅ Active LTS
Hydrogen v18.x 2022-04 2022-10 2025-04 ⚠️ Maintenance
Gallium v16.x 2021-04 2021-10 2024-09 ❌ End-of-Life
Fermium v14.x 2020-04 2020-10 2023-04 ❌ End-of-Life

建议:新项目使用 v20 或 v22,老项目尽快升级到至少 v18。

进阶技巧

1. 配置版本文件查找策略

默认情况下,fnm 只在当前目录查找 .node-version.nvmrc。如果你的项目结构比较深,可以启用递归查找:

bash 复制代码
eval "$(fnm env --use-on-cd --version-file-strategy recursive)"

2. 启用 Corepack

Corepack 是 Node.js 内置的包管理器版本管理工具,可以锁定 pnpm/yarn 版本:

bash 复制代码
eval "$(fnm env --use-on-cd --corepack-enabled)"

3. 自定义安装目录

默认安装在 ~/.local/share/fnm,可以自定义:

bash 复制代码
export FNM_DIR="/path/to/custom/fnm"
eval "$(fnm env --use-on-cd)"

4. 使用国内镜像加速

bash 复制代码
# 临时使用
fnm install 20 --node-dist-mirror=https://npmmirror.com/mirrors/node

# 永久配置
export FNM_NODE_DIST_MIRROR="https://npmmirror.com/mirrors/node"

5. 在脚本中使用 fnm

bash 复制代码
#!/bin/bash
# 确保 fnm 环境已加载
eval "$(fnm env)"

# 使用指定版本执行
fnm use 20
node your-script.js

# 或者用 exec
fnm exec --using=20 node your-script.js

常见问题 FAQ

Q: fnm 安装的 Node 在哪里?

bash 复制代码
~/.local/share/fnm/node-versions/

每个版本是一个独立目录,结构清晰。

Q: 为什么 which node 显示的路径很奇怪?

fnm 使用"多 Shell"机制,每个 Shell 会话有独立的 PATH:

bash 复制代码
$ which node
/Users/xxx/.local/state/fnm_multishells/12345_1234567890/bin/node

这是正常的,确保了不同终端窗口可以使用不同版本。

Q: 如何在 VS Code 中使用 fnm 管理的 Node?

VS Code 会自动检测 fnm。如果遇到问题,可以在 settings.json 中配置:

json 复制代码
{
  "terminal.integrated.env.osx": {
    "PATH": "${env:PATH}"
  }
}

或者在项目根目录创建 .vscode/settings.json

json 复制代码
{
  "eslint.runtime": "node"
}

Q: fnm 支持 .nvmrc 吗?

完全支持!fnm 会按以下顺序查找版本文件:

  1. .node-version
  2. .nvmrc
  3. package.jsonengines.node 字段

Q: 如何回退到 nvm?

如果你想回退(虽然我不建议),只需:

  1. 注释掉 fnm 配置
  2. 取消注释 nvm 配置
  3. 重新加载 Shell

你的 nvm 数据(如果没删)还在 ~/.nvm

总结:值得迁移吗?

绝对值得。

迁移成本:

  • 时间:约 15-30 分钟
  • 学习曲线:几乎为零(命令高度相似)
  • 风险:极低(可随时回退)

获得收益:

  • 终端启动快 5-10 倍
  • 版本切换快 20-30 倍
  • 自动切换版本,告别手动 nvm use
  • 跨平台一致性,团队协作更顺畅
  • 更少的 Bug 和怪异行为

如果你每天要打开几十次终端、在多个项目间切换,fnm 节省的时间累积起来是非常可观的。更重要的是,那种丝滑无感的体验,会让你的开发心情都变好。


参考资料:


如果这篇文章对你有帮助,欢迎点赞收藏。有问题可以在评论区交流!

相关推荐
To_OC2 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic3 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy3 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志3 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble4 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯5 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜6 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司6 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
江华森7 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http
IT_陈寒7 小时前
Redis的KEYS命令把我搞崩溃了,改用SCAN才活过来
前端·人工智能·后端