从 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 节省的时间累积起来是非常可观的。更重要的是,那种丝滑无感的体验,会让你的开发心情都变好。


参考资料:


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

相关推荐
张拭心25 分钟前
Cursor 又偷偷更新,这个功能太实用:Visual Editor for Cursor Browser
前端·人工智能
I'm Jie30 分钟前
深入了解 Vue 3 组件间通信机制
前端·javascript·vue.js
用户90443816324602 小时前
90%前端都踩过的JS内存黑洞:从《你不知道的JavaScript》解锁底层逻辑与避坑指南
前端·javascript·面试
CodeCraft Studio2 小时前
文档开发组件Aspose 25.12全新发布:多模块更新,继续强化文档、图像与演示处理能力
前端·.net·ppt·aspose·文档转换·word文档开发·文档开发api
PPPPickup3 小时前
easychat项目复盘---获取联系人列表,联系人详细,删除拉黑联系人
java·前端·javascript
老前端的功夫3 小时前
前端高可靠架构:医疗级Web应用的实时通信设计与实践
前端·javascript·vue.js·ubuntu·架构·前端框架
前端大卫3 小时前
【重磅福利】学生认证可免费领取 Gemini 3 Pro 一年
前端·人工智能
孜燃3 小时前
Flutter APP跳转Flutter APP 携带参数
前端·flutter
脾气有点小暴4 小时前
前端页面跳转的核心区别与实战指南
开发语言·前端·javascript
lxh01134 小时前
最长递增子序列
前端·数据结构·算法