1. 先别急着官网下载:Mac装Node的三种方式对比
提到在Mac上安装或升级Node/npm,最常看到的教程是"去nodejs.org下载pkg安装包,一路点下一步"。这个做法虽然省事,但从我处理过的无数环境问题来看,这也是后续坑最多的选择。你迟早会遇到这些场景:项目A要用Node 18,项目B要Node 22,但本地只有一个pkg安装的新版;npm全局包因为权限问题装不进去;想升级node结果发现/usr/local/bin下面残留了一堆旧软链。
1.1 官网pkg安装包的问题在哪
pkg安装包本质上是以管理员身份把Node写入/usr/local,然后在/usr/local/bin下创建软链,并且会写入系统级目录。这意味着:
- 每次安装或升级都要输管理员密码,自动化脚本里很难处理。
- pkg自带的升级链路并不完整,卸载时经常残留/usr/local/lib/node_modules、/usr/local/bin/node这些文件。
- 多版本切换基本靠手动改软链,风险极高,改错了node直接崩。
最麻烦的是,当Node官方发布新版本,你想从18升到20时,pkg不会帮你保留之前的全局npm包,也不会干净清理旧的二进制。很多同学升级后报错定位不到根因,查到最后往往就是旧软链指向不存在的目录。
1.2 brew install node也不行吗
Homebrew的node公式用起来确实简单,一条brew install node就完事。但它是跟着brew的整体升级节奏走的。今天装的是当前最新版,明天执行brew upgrade可能就把node换了一版,这对需要锁定Node版本的项目来说非常不友好。
还有一点容易被忽略:如果你用brew安装node,后来又用nvm安装node,两者同时存在会在PATH里打架。我在第5章会专门说这个坑。所以除非你就想随便装个Node跑个小脚本,否则brew不是首选项。
1.3 为什么最终推荐nvm
nvm(Node Version Manager)是Node社区事实上的版本管理工具。它把每个Node版本装在~/.nvm/versions/node/对应目录下,切换版本只改PATH,不碰系统目录,也不需要sudo。多版本共存、随时切换、卸载干净,这三点已经覆盖了绝大部分日常需求。另外还有一层好处:当你切换到某个Node版本时,npm会被自动绑定为那个版本对应的npm,这比手动维护npm和Node的匹配关系省心得多。
先放一张对比表,帮你快速做决策:
| 安装方式 | 多版本切换 | 是否需要sudo | 全局包隔离 | 适合场景 |
|---|---|---|---|---|
| 官网pkg | 基本不支持 | 需要 | 否 | 临时快速装一个环境 |
| brew install node | 不支持 | 不一定 | 否 | 想用brew统一管理软件的人 |
| nvm | 支持 | 不需要 | 是 | 日常开发、多个项目并存 |
所以,如果你打算长期做前端或者Node服务端开发,建议直接上nvm。下面的内容全部围绕nvm这条主线展开。
2. 前置准备:Homebrew装不上时先解决这一个问题
先用一个结论开头:如果你只是要Node和npm,其实可以完全跳过Homebrew,用nvm就够了。但很多人装Node的时候顺带也想要jdk8、maven、git、python这些开发环境,Homebrew往往是绕不开的。这一章主要解决的是一个问题:Homebrew安装脚本一跑就失败的现象。
2.1 装之前先确认Command Line Tools
Xcode Command Line Tools(CLT)是Mac上编译各种源码的基础。很多Homebrew安装失败、后续npm install时node-gyp编译报错,根源都是没装CLT。装法很简单,打开终端执行:
xcode-select --install如果之前装过,会提示"command line tools are already installed"。可以先用xcode-select -p检查当前路径,能输出路径就说明CLT已经就位。这个步骤不是可选项,而是硬条件。
2.2 官方脚本为什么容易卡在中间
Homebrew官方安装脚本开头会从raw.githubusercontent.com下载安装内容,然后从GitHub拉取brew和homebrew-core仓库,还要访问ghcr.io拉取预编译的bottle。在这些环节中,任何一个网络访问不稳定,脚本就会长时间卡住,最后超时退出。
报错信息最常见的是这类:
curl: (7) Failed to connect to raw.githubusercontent.com port 443: Connection refused但也有一种更隐蔽的情况:install.sh能下载,brew仓库clone也过了,结果在下载某个bottle时进度条卡到一动不动。如果你是用的官方源,在部分网络环境下确实容易撞上这种问题。
2.3 用镜像源走完安装
解决方案是给Homebrew指定镜像仓库和镜像bottle域名。安装前在终端里先设置三个环境变量,再执行官方安装脚本,脚本检测到这些变量后,就会从镜像站拉内容。
我实测可用的配置(以清华镜像为例):
export HOMEBREW_BREW_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git" export HOMEBREW_CORE_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git" export HOMEBREW_BOTTLE_DOMAIN="https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles"然后是官方安装命令:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"这里还有一个细节:上面的install.sh本身也可能下载失败,遇到这种情况就多试几次,或者先下载文件到本地再用bash执行。有些同学会拿别人整理的一键安装脚本跑,对于这类来源不明的脚本,我不建议直接执行,因为你不知道它在后台做了什么。自己设置镜像源变量再用官方脚本,已经足够安全可靠。
2.4 Apple Silicon芯片的路径差异
Intel芯片的Mac,Homebrew装在/usr/local;M1/M2/M3系列的Apple Silicon芯片,Homebrew装在/opt/homebrew,比如brew install go后,命令会出现在/opt/homebrew/bin/go。
这带来两个影响:一个是PATH里需要加/opt/homebrew/bin,配置不对时你会发现brew装完了但命令找不到;另一个是安装脚本跑完后,仔细看终端输出的"Next steps",它会明确提示下一步要执行什么命令。很多人跳过这段,后面才踩坑。
2.5 安装失败后的残留处理
如果之前安装到一半失败,再次执行安装脚本容易因为残留目录冲突报错。常见的残留目录包括/usr/local/Homebrew(Intel)、/opt/homebrew(Apple Silicon),以及/usr/local/Cellar、/usr/local/Caskroom等。需要删掉再重装:
sudo rm -rf /usr/local/Homebrew /usr/local/Cellar /usr/local/Caskroom注意:这种强制删除只针对Homebrew专门目录,没确认之前不要乱删其他东西。装完后验证一下:
brew -v brew doctorbrew doctor没有输出严重警告,基本就是好了。
3. 从nvm开始:Node安装与npm激活的完整流程
如果说前两章是搭台子,这一章才是真正的主角:用nvm装Node,顺便激活自带npm。
3.1 安装nvm:脚本还是Homebrew
nvm官方推荐的是脚本安装,命令一般长这个样子:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash这个脚本会把nvm仓库clone到~/.nvm,并把加载配置追加到~/.zshrc或~/.bash_profile中,可以说是开箱即用。问题还是在raw.githubusercontent.com访问不稳定的情况下,curl下载可能失败。如果遇到,我一般直接去GitHub仓库页面复制install.sh内容,存到本地文件再执行:
bash install.sh或者退一步用Homebrew来装nvm:
brew install nvmbrew方式装nvm有个隐藏细节:它不会自动帮你把nvm的加载配置写进shell,装完需要自己在~/.zshrc里加几行。很多人在这一步漏掉,导致重启终端后nvm命令不存在。
3.2 配置shell:让nvm在新终端里生效
如果用的是脚本安装,通常~/.zshrc里已经自动追加了配置。打开文件确认一下:
cat ~/.zshrc正常应该有这几行:
export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion解释一下这三行的作用:第一行定义nvm的安装目录,第二行是加载nvm主脚本,第三行是补全功能。如果是通过brew装的nvm,路径通常不是~/.nvm,而是$(brew --prefix nvm),所以要先确认NVM_DIR的值,再加载。
改完配置别忘记执行:
source ~/.zshrc然后验证:
nvm -v看到版本号就说明nvm已经就位。
3.3 安装Node LTS版本并设为默认
查看远端可用的LTS版本:
nvm ls-remote --lts网络正常的话会列出一大串版本号。安装指定版本,这里以目前仍在维护的LTS版本22为例:
nvm install 22也可以直接用:
nvm install --lts我的建议是默认装LTS而不是最新版。很多刚入门的朋友看到版本号比较大就去装,结果跑老项目时一堆API不兼容,回头又来回折腾。开发环境不是越新越好,而是和项目依赖越匹配越好。
安装完成后,让某个版本成为新终端的默认版本:
nvm alias default 22验证一下:
node -v npm -v到这里,npm其实已经随着Node一起装好了。这也是很多人容易忽略的一点:npm不是独立安装的东西,它是Node官方发布的包管理器,Node包里自带npm。所以"先装Node再装npm"这个顺序天然成立,单独去下载npm安装包其实没有意义。
3.4 确认安装路径,别被旧版本干扰
用which确认当前node到底在哪:
which node如果返回的是/Users/你的用户名/.nvm/versions/node/v22.x.x/bin/node,说明完全正确。如果返回的是/usr/local/bin/node,那说明系统里还有旧版残留,优先排查第5章的路径冲突问题。
再看全局npm包:
npm ls -g --depth=0正常情况下要么是空列表,要么只有几个内置包。确认无误后,后面所有npm i -g xxx安装的全局工具都会装到nvm对应node版本的目录下,不会污染系统目录。
4. 升级不是重装:Node、npm的版本升级策略与镜像源配置
很多人的"升级"思路是:把旧的删掉再装新的。麻烦不说,还容易留一屁股残留。其实有了nvm,升级Node这件事可以被拆成三个不相关的问题:升级Node本身、升级npm本身、处理全局包和数据依赖。
4.1 谨慎升级Node:保留旧版本,给项目留退路
用nvm升级Node不需要卸载旧版。比如你从Node 18升到22:
nvm install 22 nvm use 22 nvm alias default 22先让项目在22上跑一段时间,确认依赖都正常,再决定是否删除旧版本:
nvm uninstall 18这种"先装新版本,旧版本留观"的做法,是我给所有朋友的第一建议。Node大版本间的API差异比很多人想象的要大,V8引擎、libuv、内置npm版本都会变。一次跨越两个大版本的升级,风险最高的不是Node本身,而是项目里用了很多原生模块(node-gyp编译的native addon),这些模块很可能没有跟上新版本的ABI。
4.2 升级npm:不必重新装Node
npm升级和Node升级是两回事。如果你只想把当前Node环境里的npm升到最新:
npm install -g npm@latest查看版本:
npm -v如果想听项目建议,也可以指定具体版本,比如:
npm install -g npm@10这里需要解释一个容易混淆的点:nvm切换Node版本后,npm会变成对应Node自带的npm版本。你单独升级的npm只作用于当前这个Node版本。所以如果你在Node 18环境里升了npm为10.x,切到Node 22后,npm又变成Node 22自带的版本。这不算bug,就是设计如此。
4.3 npm镜像源:国内开发绕不开的配置
npm官方仓库默认地址是https://registry.npmjs.org/,在部分网络环境下,包一多、体积一大,下载速度差距非常明显。早期我用npm install装个大依赖,经常要等几分钟甚至直接超时。后来全面换成国内镜像源,体验提升巨大。
查看当前源:
npm config get registry改成阿里维护的npmmirror镜像:
npm config set registry https://registry.npmmirror.com/改完再npm config get registry确认一下。这样只是改用户级配置,全局生效,不影响项目内的配置。
稍复杂一点的是项目级.npmrc。如果你某个项目只能访问内网私有仓库,可以在项目根目录写一个.npmrc,它的优先级高于用户级配置。也就是说,用户级配了npmmirror,但项目里.npmrc配了公司私有源,那执行npm install时用的就是私有源。
只有一种情况需要切回官方源:发布npm包。发布动作应该面向官方registry,否则包会发到镜像站去(一般也会被拒绝)。发布时用:
npm publish --registry https://registry.npmjs.org/4.4 全局包的更新与缓存清理
查看全局装了哪些包:
npm ls -g --depth=0检查全局包有哪些可更新:
npm outdated -g --depth=0批量更新:
npm update -g这里注意,npm update -g不会更新npm自身,npm本身还是要用npm install -g npm@latest。
缓存的坑也经常被问到。当你遇到"上次装了一半被打断,这次怎么都装不完整"的情况,先清缓存再重装:
npm cache clean --force然后删掉项目里的node_modules和package-lock.json重来。这一步是很多人宁可直接重装系统的解法,其实只是依赖树状态乱了而已。
4.5 让项目锁定Node版本
升级Node的核心其实不是升级本身,而是让不同项目各用各的Node。当一个仓库需要Node 18,另一个需要Node 22,最优雅的做法是给项目根目录放一个.nvmrc文件:
echo "22" > .nvmrc配合nvm使用:
nvm usenvm会自动找到当前目录下的.nvmrc,加载对应版本,不用手动输版本号。如果你用的是zsh,还可以在切换目录时自动加载对应Node版本,比如在~/.zshrc里加一个简单的hook:
chpwd() { nvm use >/dev/null 2>&1; }项目里再配合package.json的engines字段:
"engines": { "node": ">=18.0.0" }虽然engines默认只是提示,但对协作团队仍然有很强的约束作用。
5. 安装后的高频报错:现象、根因、排查链路
这章写的都是我在实际环境里反复见过的报错。每个都按"现象-根因-排查"的顺序来讲,你可以把它当成一份排查手册,遇到类似问题直接对号入座。
5.1 command not found: node / npm
这是最经典的报错。发生原因通常有三个方向:
- nvm没加载成功。输入
nvm -v如果也提示command not found,说明是nvm本身没生效,去看3.2节的配置。 - 有node版本存在,但当前终端的环境变量里没指向。你先执行
source ~/.zshrc,再node -v,看能不能恢复。 - 打开的是一个新的终端窗口,但某些自动化脚本不加载交互式shell配置。
完整的排查顺序是:echo $PATH看有没有~/.nvm/versions/node/v22.x.x/bin,ls ~/.nvm/versions/node/看目录是否存在,which node看解析到哪个路径。这三条命令足以定位80%以上的问题。
5.2 EACCES: permission denied,以及sudo npm install的致命误区
场景:npm install -g某工具,报EACCES permission denied,日志里提到/usr/local/lib/node_modules没有写权限。新手最常见的处理方式就是加sudo:
sudo npm install -g 工具名这看着是解决问题了,实际上是埋雷。全局包被装到了root用户目录和/usr/local/lib/node_modules下面,下一次用户级npm更新它就缺权限,于是权限问题越来越严重,最后只能手动chown或者删目录。
正确做法:保证当前Node环境是nvm管理下的用户级环境。用which node确认路径在~/.nvm/versions/node/xxx/bin下,再重新npm install -g,全程不该出现任何权限报错。
如果你已经用sudo装了一些包,想修复,先确认全局目录归属:
ls -la /usr/local/lib/node_modules如果归属是root,就把不需要的删掉,或者干脆把Node切到nvm环境后重新装全局包,不要在旧的/usr/local下继续抢救。
5.3 Homebrew的node和nvm的node打架
现象:nvm use切换了版本,但node -v还是旧版,which node指向/usr/local/bin/node。这就是之前用brew或pkg装过Node,且PATH里/usr/local/bin排在了~/.nvm之前。
解决方案分两步。第一步,检查~/.zshrc里PATH顺序,让nvm的bin目录优于/usr/local/bin。不过更干净的思路是彻底移除brew安装的node:
brew uninstall --ignore-dependencies node第二步,删除可能残留的软链:
rm -f /usr/local/bin/node /usr/local/bin/npm然后重新开终端,node -v应该就听nvm的了。
5.4 切换Node版本后,全局npm包"消失"
这不是真的消失。nvm为每个Node版本分别维护独立的全局npm包目录,你在Node 18里用npm i -g typescript装的ts,切到Node 22后默认是看不到的。
解决办法有两个:切到新版本后重新安装需要的全局包;或者用nvm自带的迁移命令:
nvm reinstall-packages 18这条命令会把Node 18下的全局包列表取出来,在切换到当前版本后逐个重新安装。对日常工作来说,全局包通常不多,重新装也不费事。我更推荐在需要长期停留的版本里主动装好自己那套typescript、eslint、pnpm、yarn之类,不要依赖迁移命令,迁移中途网络波动反而更容易出问题。
5.5 原生模块编译失败:node-gyp、python、make相关
npm install某个包含C++模块的包时,报错里出现node-gyp、python、make、gcc等词,比如canvas、bcrypt、sharp这类,它们需要本地编译环境。Mac上缺的通常是Command Line Tools,而不是你自己想象的python版本。
先检查:
xcode-select -p如果提示找不到路径,执行xcode-select --install安装CLT。装完CLT后再重新npm install,绝大多数编译问题都会消失。如果你用的是Apple Silicon,有些包会走arm64预编译产物,不需要本地编译,这部分依赖平台支持情况。
5.6 npm install时反复EEXIST / ENOTEMPTY
这种报错一般发生在依赖树有残留、或者上次安装被中断的情况下。处理思路很简单:清缓存,删node_modules和lock文件,重新安装。但注意,package-lock.json是锁定依赖版本的关键文件,删除后重新生成时,会把依赖整体升级到当前范围内最新,可能导致间接依赖变化引发新的兼容问题。所以只在项目确实需要重新解析依赖树时才删lock,否则保留lock只删node_modules重装:
rm -rf node_modules npm install如果还不行,再考虑npm cache clean --force和删除lock文件。
最后说点我自己的实际操作习惯。每次配新Mac,我的固定流程是:先装Command Line Tools,再设Homebrew镜像变量装Homebrew,然后curl装nvm,nvm install --lts并设置默认版本,最后把npm registry切到国内镜像。这套流程全跑一遍大概不到十分钟,之后再也不会被"装Node、升Node、npm缺权限"这一类问题反复折腾。如果后面你又遇到奇怪的报错,建议先查一遍哪个node被实际调用(which node),再决定重装哪个部分——大多数所谓"环境坏了"其实只是路径和版本错位。