从 nvm 到 fnm:一个前端老兵的版本管理工具迁移实录
从 nvm 到 fnm:一个前端老兵的版本管理工具迁移实录
引子:那个让我崩溃的早晨
某个周一早晨,我像往常一样打开终端准备开始工作。
# 打开 iTerm2... 等待... 等待...
# 终于出现命令提示符,耗时 2.3 秒
接着切换到一个需要 Node 16 的老项目:
$ nvm use 16
Now using node v16.20.2 (npm v8.19.4)
# 又是 0.5 秒过去了
然后跳到另一个 Node 20 的新项目:
$ 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 脚本实现的。每次打开新终端,它都要:
- 加载
nvm.sh脚本(几千行) - 解析已安装的 Node 版本
- 设置环境变量
- 初始化自动补全
我用 time 测了一下我的 .zshrc 加载时间:
# 有 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,太原始了。虽然有 avn、nvm-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 上做了详细测试:
终端启动时间
# nvm
$ time zsh -i -c exit
0.678 total
# fnm
$ time zsh -i -c exit
0.089 total
# 提升:7.6 倍
版本切换时间
# 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 倍
安装新版本
# nvm install 22
# 总耗时约 45 秒(包含下载)
# fnm install 22
# 总耗时约 38 秒(包含下载)
# 下载速度差不多,但 fnm 的解压和配置更快
为什么 fnm 这么快?
- 原生二进制:Rust 编译成机器码,不需要解释器
- 并行处理:充分利用多核 CPU
- 惰性加载:只在需要时才读取版本信息
- 高效的文件操作:Rust 的 I/O 性能本就出色
真实场景:fnm 如何改变我的工作流
场景一:多项目并行开发
我同时维护着这些项目:
| 项目 | Node 版本 | 原因 |
|---|---|---|
| 老后台系统 | 14 | 历史包袱,依赖不支持高版本 |
| 主站前端 | 18 | 稳定的 LTS |
| 新管理后台 | 20 | 需要新特性 |
| 实验性项目 | 22 | 尝鲜最新 API |
| Electron 应用 | 18 | Electron 版本限制 |
以前用 nvm:
$ 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:
$ cd legacy-admin
Using Node v14.21.3 # 自动切换,瞬间完成
$ cd ../main-site
Using Node v20.19.6 # 无感切换
# 永远不会忘记切换版本,因为是自动的
场景二:CI/CD 环境统一
我们团队的 CI 配置以前是这样的:
# .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 后:
# .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 时代的痛苦:
# macOS/Linux 同事
$ nvm use 20
# Windows 同事(nvm-windows)
$ nvm use 20.19.6 # 必须写完整版本号!
# 而且 .nvmrc 经常不生效
fnm 时代的统一:
# 所有平台,相同命令,相同行为
$ fnm use 20
Windows 同事终于不用单独维护一套文档了。
场景四:Docker 开发环境
在 Dockerfile 中安装 Node:
# 以前用 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 版本:
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 间跳转时完全无感:
$ 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):
brew install fnm
Windows (Scoop):
scoop install fnm
Windows (Chocolatey):
choco install fnm
Linux/macOS (curl):
curl -fsSL https://fnm.vercel.app/install | bash
Cargo (Rust 用户):
cargo install fnm
第二步:配置 Shell
这是最关键的一步,配置正确才能享受自动切换的便利。
Zsh (~/.zshrc):
# fnm - Fast Node Manager
eval "$(fnm env --use-on-cd --shell zsh)"
Bash (~/.bashrc):
# fnm - Fast Node Manager
eval "$(fnm env --use-on-cd --shell bash)"
Fish (~/.config/fish/config.fish):
# fnm - Fast Node Manager
fnm env --use-on-cd --shell fish | source
PowerShell ($PROFILE):
# 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 安装了哪些版本:
ls ~/.nvm/versions/node/
# v10.24.1 v14.21.3 v16.20.2 v18.20.8 v20.19.6 v22.21.0
在 fnm 中安装对应版本:
# 方式一:通过 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
设置默认版本:
fnm default 22
第四步:处理全局 npm 包
这是很多人忽略的一步!nvm 下安装的全局包不会自动迁移。
查看 nvm 中的全局包:
# 切换到 nvm 的某个版本
export PATH="$HOME/.nvm/versions/node/v20.19.6/bin:$PATH"
npm list -g --depth=0
在 fnm 的对应版本中重新安装:
fnm use 20
npm install -g typescript ts-node nodemon pm2 # 你需要的包
Pro Tip: 可以写个脚本批量处理:
#!/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 配置文件:
# ~/.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"
第六步:验证迁移结果
# 重新加载配置
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 释放磁盘空间:
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。如果你的项目结构比较深,可以启用递归查找:
eval "$(fnm env --use-on-cd --version-file-strategy recursive)"
2. 启用 Corepack
Corepack 是 Node.js 内置的包管理器版本管理工具,可以锁定 pnpm/yarn 版本:
eval "$(fnm env --use-on-cd --corepack-enabled)"
3. 自定义安装目录
默认安装在 ~/.local/share/fnm,可以自定义:
export FNM_DIR="/path/to/custom/fnm"
eval "$(fnm env --use-on-cd)"
4. 使用国内镜像加速
# 临时使用
fnm install 20 --node-dist-mirror=https://npmmirror.com/mirrors/node
# 永久配置
export FNM_NODE_DIST_MIRROR="https://npmmirror.com/mirrors/node"
5. 在脚本中使用 fnm
#!/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 在哪里?
~/.local/share/fnm/node-versions/
每个版本是一个独立目录,结构清晰。
Q: 为什么 which node 显示的路径很奇怪?
fnm 使用"多 Shell"机制,每个 Shell 会话有独立的 PATH:
$ which node
/Users/xxx/.local/state/fnm_multishells/12345_1234567890/bin/node
这是正常的,确保了不同终端窗口可以使用不同版本。
Q: 如何在 VS Code 中使用 fnm 管理的 Node?
VS Code 会自动检测 fnm。如果遇到问题,可以在 settings.json 中配置:
{
"terminal.integrated.env.osx": {
"PATH": "${env:PATH}"
}
}
或者在项目根目录创建 .vscode/settings.json:
{
"eslint.runtime": "node"
}
Q: fnm 支持 .nvmrc 吗?
完全支持!fnm 会按以下顺序查找版本文件:
.node-version.nvmrc-
package.json的engines.node字段
Q: 如何回退到 nvm?
如果你想回退(虽然我不建议),只需:
- 注释掉 fnm 配置
- 取消注释 nvm 配置
- 重新加载 Shell
你的 nvm 数据(如果没删)还在 ~/.nvm。
总结:值得迁移吗?
绝对值得。
迁移成本:
- 时间:约 15-30 分钟
- 学习曲线:几乎为零(命令高度相似)
- 风险:极低(可随时回退)
获得收益:
- 终端启动快 5-10 倍
- 版本切换快 20-30 倍
- 自动切换版本,告别手动
nvm use - 跨平台一致性,团队协作更顺畅
- 更少的 Bug 和怪异行为
如果你每天要打开几十次终端、在多个项目间切换,fnm 节省的时间累积起来是非常可观的。更重要的是,那种丝滑无感的体验,会让你的开发心情都变好。
参考资料:
如果这篇文章对你有帮助,欢迎点赞收藏。有问题可以在评论区交流!