在 Mac ARM (Apple Silicon) 上搭建 Node.js 开发环境,版本管理器和包管理器的选择至关重要。本文将系统地介绍如何使用 fnm 管理 Node.js 版本、通过 Corepack 启用 pnpm,并完成从 Yarn 到 pnpm 的完整迁移流程。
提示:本文由 AI 根据对话历史整理,仅供参考
# fnm 安装方式对比:Homebrew vs 官方脚本
fnm (Fast Node Manager) 可以通过两种方式安装,它们的安装结果相同,但管理方式截然不同。
# Homebrew 安装
brew install fnm
通过 Homebrew 软件包管理器下载并安装编译好的二进制文件。更新只需 brew upgrade fnm,卸载只需 brew uninstall fnm,和系统中其他工具统一由 Homebrew 管理。
# 官方脚本安装
curl -fsSL https://fnm.vercel.app/install | bash
利用 curl 下载远程安装脚本并直接执行。它会自动检测系统架构、下载对应二进制文件,并尝试修改 .zshrc 或 .bashrc 配置。版本通常最新,但更新时需要重新运行脚本,且卸载需要手动清理文件。
# 对比表格
| 特性 | brew install fnm |
curl \| bash (官方脚本) |
|---|---|---|
| 管理方式 | Homebrew 集中管理 | 独立分散安装 |
| 更新命令 | brew upgrade fnm |
通常需要重新执行脚本 |
| 卸载难度 | 极简 (brew uninstall) |
略难(需手动删文件和配置) |
| 配置自动化 | 需手动添加配置到 .zshrc |
脚本通常会尝试自动添加 |
| 安全性 | Homebrew 仓库有社区审核 | 直接运行远程脚本存在安全风险 |
| 适用人群 | 追求系统整洁的 macOS 开发者 | 不想用/没装 Homebrew 的人 |
建议:如果你是 macOS 用户且已安装 Homebrew,优先使用 brew install fnm。关于 Homebrew 的更多管理技巧,可以参考 Homebrew 包管理与依赖清理指南。
无论使用哪种方式安装,都需要在 Shell 配置文件中添加初始化代码(详见下文第三节)。
# fnm 在 M1 Mac 上的文件存储结构
在 ARM 架构的 Mac 上,fnm 的存储结构主要分为 二进制程序本身 和 环境管理目录 两部分。
# 二进制路径
通过 Homebrew 安装时,fnm 可执行文件位于:
/opt/homebrew/bin/fnm
这是 ARM Mac 上 Homebrew 的默认安装路径,与 Intel Mac 的 /usr/local/bin 不同。
# 数据目录
fnm 将所有下载的 Node 运行时、别名和安装包存放在 ~/.local 路径下:
~/.local/share/fnm
# node-versions 目录
这是最核心的目录,存放所有已下载的 Node.js 版本。每个版本都是独立的隔离目录,包含该版本的 bin、lib、include 和 share:
node-versions/<VERSION>/installation/
# 例如:
node-versions/v20.10.0/installation/bin/node
# aliases 目录
存放别名文件。当执行 fnm alias v20.10.0 default 时,fnm 会在此创建指向特定版本的符号链接。常见别名包括 default(默认版本)和 lts-latest 等。
# 总结表格
| 组件 | 路径 (ARM Mac) | 说明 |
|---|---|---|
| 可执行文件 | /opt/homebrew/bin/fnm |
fnm 核心程序 |
| 主数据目录 | ~/.local/share/fnm |
存储所有数据的基础路径 |
| Node 安装包 | .../fnm/node-versions/ |
各个版本的实体文件 |
| 别名设置 | .../fnm/aliases/ |
存储如 default 等指向关系 |
| 环境变量配置 | ~/.zshrc |
负责在启动时注入 fnm 路径 |
如果需要自定义存储位置,可以在 .zshrc 中设置:
export FNM_DIR="/your/custom/path"
# .node-version 自动切换与 .zshrc 配置
fnm 支持通过项目根目录的配置文件自动切换 Node 版本,实现"进入目录即切换"的体验。
# 创建 .node-version 文件
在项目根目录创建 .node-version 文件(也支持 .nvmrc),内容仅需版本号:
v20.10.0
也可以通过命令自动生成:
node -v > .node-version
# 配置 Shell 自动加载
确保 ~/.zshrc 中 fnm 的初始化脚本包含 --use-on-cd 参数:
eval "$(fnm env --use-on-cd --shell zsh)"
配置完成后,当你在终端执行 cd my-project 进入该目录时,fnm 会自动读取 .node-version 文件并切换到对应的 Node 版本。如果该版本未安装,它会提示你运行 fnm install。
关于 .zshrc 的完整配置与环境变量排查,可以参考 Zsh 终端配置与 Homebrew 环境修复。
# Corepack 的概念与启用
# 什么是 Corepack
Corepack 是 Node.js 16.13+ 官方内置的 包管理器管理工具。它的作用是充当一个"代理层":当你输入 pnpm install 时,Corepack 会检查当前项目 package.json 中的 packageManager 字段,自动下载并运行正确版本的 pnpm,无需手动全局安装。
简而言之,fnm 管理 Node.js 本身 的版本,Corepack 管理 包管理器(pnpm/yarn) 的版本。
# 启用 Corepack
由于 Corepack 目前仍处于实验性阶段(虽然已经非常稳定),默认是关闭的,需要手动开启:
corepack enable
# 查看 Corepack 是否已启用
Corepack 不是常驻服务进程,而是一组"二进制代理(Shim)",没有 status 命令。可以通过以下方式确认:
方法一:检查二进制路径(最可靠)
which pnpm
- 已启用:路径指向 Node 的 bin 目录,例如
~/.local/share/fnm/node-versions/v20.10.0/installation/bin/pnpm - 未启用:终端提示
command not found,或指向手动通过npm install -g pnpm安装的全局路径
方法二:尝试运行
corepack --version
如果返回版本号,说明 Corepack 程序本身可用。
# 切换 Node 版本时是否需要重新启用
是的,通常需要。 corepack enable 的本质是在当前 Node 版本的 bin 目录下创建符号链接。当通过 fnm 切换到一个从未启用过 Corepack 的 Node 版本时,那个版本的 bin 目录里没有 pnpm 或 yarn 的代理文件。
可以在 ~/.zshrc 中添加自动化脚本来解决:
# fnm 初始化
eval "$(fnm env --use-on-cd --shell zsh)"
# 自动为新安装的 Node 版本启用 corepack
# 只有当 pnpm 不存在且 corepack 存在时才执行
if ! command -v pnpm &> /dev/null && command -v corepack &> /dev/null; then
corepack enable
fi
# Corepack vs npm install -g 的区别
Corepack 和 npm install -g 的设计目标完全不同,不能混为一谈。
# 管理对象与适用范围
| 特性 | Corepack | npm install -g |
|---|---|---|
| 管理对象 | 仅限包管理器 (pnpm, yarn) | 所有 Node.js 命令行工具 |
| 工作原理 | Node.js 内置的"代理层",拦截并切换 pnpm/yarn 版本 | 将工具的二进制文件直接放入系统路径 |
| 典型例子 | pnpm, yarn | nodemon, serve, typescript, claude-code |
| 版本一致性 | 严格遵循 package.json 中 packageManager 字段 |
手动管理,容易出现版本不一致 |
Corepack 的代码逻辑中硬编码了对 pnpm 和 yarn 的特殊处理,不能用来管理其他 npm 全局包。
# 混合方案(推荐)
在 Mac ARM 架构上,建议采取以下分工:
- 包管理器(pnpm, yarn):使用
corepack enable,确保不同项目可以使用不同版本 - 常用开发工具(nodemon, serve, typescript 等):使用
npm install -g,但注意在 fnm 环境下全局包跟随 Node 版本 - 偶尔使用的工具:使用
npx临时下载运行,不污染全局环境
# 包管理器 -> Corepack
corepack enable
# 常用工具 -> npm -g
npm install -g nodemon serve
# 偶尔使用 -> npx
npx create-react-app my-app
# 注意事项
在启用 Corepack 之前,建议先卸载手动安装的全局 pnpm/yarn,避免版本冲突:
npm uninstall -g pnpm yarn
在 fnm 环境下,全局包是跟着 Node 版本走的。如果在 v18 下安装了某个工具,切换到 v20 后该工具就找不到了,因为它们存储在不同的目录中。
# 从 Yarn 迁移到 pnpm 的完整步骤
以一个使用 Yarn + Monorepo 的 Hexo 项目为例,演示完整的迁移流程。
# 通过 Corepack 安装 pnpm
corepack enable
# 清理 Yarn 旧文件
rm -rf .yarn .yarnrc.yml yarn.lock node_modules
# 创建 pnpm-workspace.yaml
Yarn 使用 package.json 中的 workspaces 字段管理工作区,pnpm 则需要一个独立的配置文件。在项目根目录创建 pnpm-workspace.yaml:
packages:
- 'packages/*'
# 修改 package.json 中的脚本
将 scripts 中的 yarn 命令替换为 pnpm 的等效命令。核心区别在于工作区命令的写法:
- Yarn:
yarn workspace <name> <command> - pnpm:
pnpm --filter <name> <command>
修改后的示例:
{
"scripts": {
"build": "pnpm build:uikit && pnpm build:hexo",
"build:uikit": "pnpm --filter shokax-uikit build",
"build:hexo": "hexo generate",
"deploy": "hexo deploy",
"dev": "hexo server",
"clean": "pnpm clean:uikit && pnpm clean:hexo",
"clean:uikit": "pnpm --filter shokax-uikit clean",
"clean:hexo": "hexo clean",
"clear": "pnpm clear:uikit && pnpm clear:hexo",
"clear:uikit": "pnpm --filter shokax-uikit clear",
"clear:hexo": "rm -rf node_modules pnpm-lock.yaml"
},
"packageManager": "pnpm@9.0.0"
}
同时,记得更新 packageManager 字段为实际安装的 pnpm 版本。
# 处理子包
子包的调整要点:
- 移除
packageManager字段:交给根目录统一管理 - 保留脚本和依赖:
scripts、dependencies、devDependencies不需要大的改动 - 将 pnpm 配置上提到根目录:子包中的
pnpm.overrides和onlyBuiltDependencies需移动到根目录(详见第七节)
# 安装依赖
在项目根目录执行:
pnpm install
这将生成新的 pnpm-lock.yaml 并安装所有依赖。
# 构建验证
如果项目存在依赖链(例如 uikit -> player -> theme),可以利用 pnpm 的拓扑排序自动按顺序构建:
pnpm -r build
# Monorepo 项目的 pnpm 配置
在 Monorepo 结构中,pnpm 提供了一些高级配置项来优化依赖管理和安全性。这些配置需要放在 根目录 的 package.json 中。
# overrides:依赖替换
overrides 可以强制替换依赖树中的特定包。一个常见的用法是通过 nolyfill 替换掉那些为兼容旧环境而存在的 polyfill 包:
{
"pnpm": {
"overrides": {
"array-includes": "npm:@nolyfill/array-includes@latest",
"array.prototype.flat": "npm:@nolyfill/array.prototype.flat@latest",
"hasown": "npm:@nolyfill/hasown@latest",
"object.fromentries": "npm:@nolyfill/object.fromentries@latest",
"object.groupby": "npm:@nolyfill/object.groupby@latest",
"object.values": "npm:@nolyfill/object.values@latest"
}
}
}
这样做的好处:
- 减少
node_modules冗余:避免安装大量微小的兼容包 - 加快安装速度:减少需要下载和解析的包数量
- 修复安全隐患:一些老包可能存在依赖漏洞,替换后可直接规避
# onlyBuiltDependencies / allowedBuiltDependencies
这是 pnpm 的安全特性,用于限制哪些包可以运行安装脚本(preinstall/postinstall)。很多 npm 包在安装时会自动运行脚本,这可能成为恶意软件的攻击面。
{
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"hexo-util"
]
}
}
以上配置表示只有 esbuild 和 hexo-util 被允许运行安装脚本,其他依赖的脚本会被 pnpm 拦截。
- esbuild:需要在安装时根据操作系统下载对应的二进制程序
- hexo-util:底层工具库涉及原生编译
提示:pnpm 9.x 以后,官方建议使用
allowedBuiltDependencies替代onlyBuiltDependencies,虽然目前两者都兼容。
# pnpm 与 Yarn 命令对照表
从 Yarn 迁移到 pnpm 后,以下是常用命令的对照:
| 操作 | Yarn 命令 | pnpm 命令 |
|---|---|---|
| 安装所有依赖 | yarn |
pnpm install 或 pnpm i |
| 添加依赖 | yarn add <pkg> |
pnpm add <pkg> |
| 添加开发依赖 | yarn add -D <pkg> |
pnpm add -D <pkg> |
| 运行脚本 | yarn <script> |
pnpm <script> 或 pnpm run <script> |
| 指定工作区运行 | yarn workspace <name> <cmd> |
pnpm --filter <name> <cmd> |
| 全局安装 | yarn global add <pkg> |
pnpm add -g <pkg> |
| 移除依赖 | yarn remove <pkg> |
pnpm remove <pkg> |
| 递归构建 | - | pnpm -r build |
| 执行本地二进制 | yarn exec <cmd> |
pnpm exec <cmd> |
# 常见问题排查
# pnpm server vs pnpm run server:内置命令冲突
如果定义了 "server": "hexo server" 脚本,你可能会发现 pnpm server 没有输出,但 pnpm run server 正常运行。
原因:pnpm server 是 pnpm 的 内置命令(用于管理 pnpm 的后台共享存储服务器进程),而不是脚本快捷方式。pnpm 只有在遇到非内置关键字时才会回退到 package.json 的脚本中。
解决方案:
- 养成习惯使用
pnpm run server,显式告诉 pnpm 执行脚本 - 或者将脚本改名为
dev或start,避免与内置命令冲突:
{
"scripts": {
"dev": "hexo server"
}
}
# fnm_multishells 是什么
当你运行 eval "$(fnm env --use-on-cd)" 时,fnm 会在系统临时文件夹(通常是 /tmp)里创建一个 fnm_multishells 目录。
它的作用是实现 多终端窗口独立 Node 版本。为了让你在窗口 A 用 Node 20、窗口 B 用 Node 18 且互不干扰,fnm 需要为每个 Shell 会话创建一个独立的"虚拟路径",里面包含指向特定 Node 版本的符号链接。
你会看到大量以数字命名的子目录,这是因为每次打开新终端窗口都会生成一组文件。这些文件可以安全删除(重启终端即可恢复),正常情况下重启电脑后系统会自动清理。
# 为什么不建议通过 Homebrew 安装 Node
既然已经有了 fnm,不建议再通过 brew install node 安装 Node,原因如下:
- 编译风险:Homebrew 安装 Node 时可能触发源码编译,特别是在较旧的 macOS 版本上容易遇到 C++ 编译器不匹配的链接错误(如
ld: symbol(s) not found for architecture arm64) - fnm 更轻量:fnm 直接从 Node.js 官网下载预编译的 ARM64 二进制包,解压即用,完全不经过编译过程
- 冲突风险:同时通过 Homebrew 和 fnm 管理 Node 会导致全局路径混乱
正确做法是只通过 fnm 安装和管理 Node:
fnm install 20
fnm use 20
# brew install --cask 的含义
在 Homebrew 中,--cask 用于安装 带有图形界面(GUI)的 macOS 应用程序。
brew install(不带--cask):安装命令行工具,如node、git、wgetbrew install --cask:安装桌面软件,如 Google Chrome、VS Code 等。背后的操作是自动下载.dmg或.zip,将.app文件移动到/Applications,然后清理临时文件
更多 Homebrew 相关知识可以参考 Homebrew 包管理与依赖清理指南。