先说个真实经历。有段时间我的 Mac 上node -v和npm -v永远对不上,brew 每次升级都能带出新的报错,Angular 9 的项目要求 node 12,另一个仓库又非要 18 起步。忍了好几周之后我做了个决定:把 node、npm、homebrew 全部卸掉,从零建一套干净环境。事后回头看,那次卸载比我预期的麻烦得多——原因很简单,这三个工具在系统里留下的痕迹远不止一个命令文件。
这篇文章就是那套完整卸载流程的记录。macOS 和 Windows 都会讲到,包含 Homebrew 的官方卸载脚本、残留目录清理、node/npm 的多种安装路径对应清理方式,以及很多人在 Windows 上遇到的npm.ps1无法加载、被禁止运行脚本的问题。适合三类人:想彻底清掉旧环境重来的、被 PATH 和残留配置折磨的、以及刚接触这些工具想先搞清楚卸载逻辑的新手。
1. 为什么要把 node、npm 和 homebrew 一并清掉
很多人对“卸载”的理解是:删掉可执行文件、删掉主目录,完事。实际这套逻辑用在 node、npm、homebrew 上根本不成立,因为这三个工具的安装过程会在系统里埋下好几层痕迹,缺了哪一层都不算干净。
1.1 只卸一半的尴尬:全局包和残留配置不会自动消失
先说一个最常见的错误操作:brew uninstall node,然后以为 node 没了。但如果你的 node 根本不是用 brew 装的,而是从官网下载 pkg 装的,这个命令卸的只是 brew 自己的 formula 记录,/usr/local/bin/node这个软链反而指向另一个真实的 node 文件,卸载完成后node -v照样有输出。
npm 的全局包也是一样的道理。很多人全局装过@angular/cli、commitlint、corepack之类,这些包默认落在/usr/local/lib/node_modules(Intel 或官方 pkg 场景)。你卸载了 node 本体,这个目录里几十个全局包可能还在,重装完 node 之后它们依然能被 require,但版本和依赖却是旧的,甚至和新的 node 不兼容。
还有隐藏的配置文件。~/.npmrc里可能存着你的 registry 镜像地址、公司私有源、甚至某些登录 token。不删掉的话,重装完 npm 第一次安装包时,它会静默地用旧配置去请求一个已经失效的地址,然后给你报一个莫名其妙的 404。
1.2 真正值得全套清掉的三种场景
不是所有情况都需要走到“卸载三件套”这一步,我总结下来,值得全套清掉的场景基本是这三种:
第一种,版本管理器和包管理器混用。前脚用 nvm 装了 node 16,后脚又用 brew 装了 node 18,两个二进制文件都在 PATH 里,谁的优先级高取决于 shell 配置文件的加载顺序。今天打开终端是 16,明天重启变成了 18,全局包装到哪套环境全看运气。这种混乱靠“卸掉其中一个”很难解决,因为两个安装路径都写进了 PATH,不如全清干净再统一用一套方案。
第二种,homebrew 自己已经坏了。brew update 一直报错、自检不通过、权限错乱,或者网上搜到一堆“homebrew 卸载残留”的帖子,说明它的状态已经不可维护。这时候单独修 brew 的成本往往比重装还高。
第三种,环境要整体交付或长期复用。比如要在 CI 机器、共用开发机上重建一套标准环境,最稳妥的做法不是在新机器上叠加各种补丁,而是先明确“当前机器上没有旧工具干扰”。
1.3 卸载的本质:撤销安装时的所有写入点
我后来给自己总结了一个很直白的定义:卸载 node、npm、homebrew,不是在删软件,而是在撤销当初安装时对系统的所有写入点。
一个工具装进系统,至少会写入这几个地方:可执行文件或软链、包目录、配置目录、缓存目录、shell 启动文件(PATH 配置)。卸载时就要反过来检查这些位置。所以后面两章的清理顺序,也是按照这个思路设计的:先跑官方卸载脚本干掉主要文件,再按路径排查残留目录,最后清理 shell 配置。
2. macOS 端 homebrew 的完整卸载链路:官方脚本加残留战场
macOS 下卸载 homebrew,第一步永远是跑官方卸载脚本,而不是手动rm -rf。但很多人不知道的是,官方脚本跑完之后,系统里其实还留着不少东西。
2.1 先跑官方卸载脚本,看它到底做了什么
Homebrew 官方提供了一站式卸载脚本,官方文档里推荐的方式是运行这条命令:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)"执行之后,脚本会先扫描当前已安装的 formula 和 cask,然后问你几个问题,常见的是这两个:是否确认卸载 Homebrew,以及是否连同它安装过的软件包一起删除。如果你希望环境“彻底干净”,这两处都选 yes;如果你只是想卸掉 brew 本体、但想保留某些用 brew 装过的命令行工具,就选 no,之后自己手动处理。
跑这个脚本之前,强烈建议先做一件事:brew list --formula和brew list --cask,把当前装过的所有包列出来存个档。我有一次卸载前没存档,脚本清理完才发现有个依赖是 brew 装的,后来翻了一下午才找到替代安装方式。
脚本执行时间取决于装机量,一般几分钟到十几分钟不等。期间它会打印删除路径的日志,建议截图或保留输出,后面排查残留时能省不少事。
2.2 脚本不会帮你清的目录:按芯片平台分情况处理
官方脚本卸载的是“Homebrew 自己认知范围内的文件”,但它对系统目录里的一些散落文件夹并不总是完全清理。这也是“homebrew 卸载残留”问题的主要来源。
Apple Silicon(M 系列芯片)的 Mac,brew 主目录是/opt/homebrew,卸载脚本正常情况下会把整个目录干掉。但如果你之前往里面手动放过东西,或者某些进程还在占用,脚本可能只删了它管理的一部分,剩下一个空壳目录。Intel 芯片的 Mac 更麻烦,brew 的文件分散在/usr/local下的很多子目录里。
下面这张表是按常见残留位置整理的,建议逐个检查:
| 路径 | 作用 | 是否常见残留 |
|---|---|---|
/opt/homebrew | Apple Silicon 下 brew 主目录 | 是 |
/usr/local/Homebrew | Intel 下 brew 程序目录 | 是 |
/usr/local/Cellar | Intel 下已安装 formula 的实体目录 | 是 |
/usr/local/Caskroom | cask 应用的安装目录 | 常见 |
/usr/local/Frameworks | 部分依赖的 framework | 偶尔 |
/Library/Homebrew | 系统级共享目录 | 较少 |
~/Library/Caches/Homebrew | 下载缓存 | 非常常见 |
~/Library/Logs/Homebrew | 日志 | 常见 |
确认脚本已经把该删的删了之后,再手动补刀:
# Apple Silicon 用户 sudo rm -rf /opt/homebrew # Intel 用户 sudo rm -rf /usr/local/Homebrew /usr/local/Cellar /usr/local/Caskroom /usr/local/Frameworks # 通用:清理用户级缓存和日志 rm -rf ~/Library/Caches/Homebrew rm -rf ~/Library/Logs/Homebrew这里必须强调一个很多人踩过的坑:Intel 机器上不要直接sudo rm -rf /usr/local。/usr/local下面可能有你自己编译装的其他工具、手动放的脚本、甚至其他包管理器写的文件,整个目录删掉会误伤一片。
2.3 PATH 里的 brew 残留怎么彻底抹掉
brew 安装时会在 shell 启动文件里写入配置,最常见的两行是:
eval "$(/opt/homebrew/bin/brew shellenv)"正式环境变量或者在/etc/paths.d目录下放一个专门的文件,总之你的PATH里只要还在找 brew,which brew就可能指向一个不存在的路径,进而让终端每次启动都慢半拍、甚至报错。
排查方法很简单:
which -a brew grep -n brew ~/.zshrc ~/.zprofile ~/.bash_profile /etc/paths /etc/paths.d 2>/dev/null有输出就说明还有残留。个人经验是,最常出问题的是~/.zshrc里的eval那行,删掉即可;/etc/paths.d下如果有 Homebrew 开头的文件,用sudo rm -f删掉。如果你用的是 fish shell,还要检查~/.config/fish/config.fish。
如果你打算之后用其他包管理器(比如后面要提到的 nvm、fnm),这一步反而是检查 PATH 写入逻辑的最好时机——每加一个工具,你就能明确知道它往哪里写了配置。
3. macOS 上 node 和 npm 的清理:先分清是“谁装的”再动手
装过 type-c 充电器的人都知道,同一个接口,不同充电头的协议完全不同。node 和 npm 的安装也是这样——nvm 装的、brew 装的、官方 pkg 装的,路径和卸载方式完全不同。你没法用同一个命令覆盖三种情况,所以第一步永远是确认来源。
3.1 三步确认 node 的来源
开个终端,依次执行:
which node ls -l $(which node) brew list --formula | grep node结果大概有四种情况:
- 路径是
~/.nvm/versions/node/...,说明 node 是 nvm 管的; - 路径是
/opt/homebrew/bin/node或/usr/local/bin/node,且brew list里有 node,说明是 brew 装的; - 路径是
/usr/local/bin/node,但brew list里没有 node,那大概率是官方 pkg 装的; - 多个路径同时存在,说明环境已经混了,按下面三种情况分别清。
这里有个容易看走眼的细节:官方 pkg 安装时创建的/usr/local/bin/node是一个软链,指向真实的安装目录(比如/usr/local/nodejs/bin/node)。只看which node可能觉得路径很干净,一定要再看一眼ls -l结果里的->指向。
3.2 分情况清理:nvm、brew、官方 pkg 三种路径
情况 A:nvm 管理。先在 nvm 里把装的版本一个个卸掉:
nvm ls nvm uninstall 16 nvm uninstall 18然后再把 nvm 本身从系统里移除:
rm -rf ~/.nvm同时清理 shell 配置里和 nvm 相关的三行样板代码(通常在~/.zshrc或~/.bash_profile里)。这里容易漏的是:nvm 会在~/.npmrc里留下一个prefix配置,指向 nvm 的目录,重装完如果有配置冲突,先回来查这里。
情况 B:brew 装。直接:
brew uninstall --ignore-dependencies node加--ignore-dependencies是因为有些用户装 node 是作为其他包(比如某些 ruby 工具链)的依赖出现的,默认卸载会连带追问依赖处理,加这个参数可以只卸 node 本体。如果你已经执行过上一章的 brew 卸载脚本,这一步通常可以跳过,brew 脚本会把 node 的软链一起清掉。
情况 C:官方 pkg 装的。先找到包 ID 并注销系统记录:
pkgutil --pkgs | grep node sudo pkgutil --forget org.nodejs.node.pkgpkgutil --forget只是删除“包安装记录”,不会删文件。所以接下来还要手动删安装时写入的文件:
sudo rm -rf /usr/local/bin/node \ /usr/local/bin/npm \ /usr/local/bin/npx \ /usr/local/bin/corepack \ /usr/local/lib/node_modules \ /usr/local/include/node \ /usr/local/share/doc/node \ /usr/local/share/man/man1/node.1如果你用ls -l /usr/local/bin/npm看到指向别处,手动顺着软链目标把真实目录也删掉。这一步别怕误删,/usr/local/bin下的 node 系目录基本都是 node 相关,不会碰到其他工具。
3.3 缓存与全局配置:~/.npm、~/.npmrc、~/.node-gyp 别漏
不管 node 是从哪个渠道装的,这最后一项清理必须是同一套:
rm -rf ~/.npm rm -rf ~/.npmrc rm -rf ~/.node-gyp~/.npm是 npm 的本地缓存目录,长期安装各种包后会积累几个 GB 的压缩文件,卸载后不删,等于白卸;~/.npmrc前面提过,里面可能存了默认 registry、镜像源、登录凭据;~/.node-gyp是编译原生模块时的缓存,里面往往带着旧的 node 头文件,新版本装某些包含 C++ 代码的包时会因为这个目录里的旧头文件编译失败。
如果你在别的目录里还配置过.yarnrc或者 pnpm 的store路径,想彻底的话也顺手处理,但 node 系列的根,还是上面三个。
4. Windows 端 node/npm 卸载与 PowerShell 脚本权限坑
Windows 的卸载流程和 macOS 不同,主要靠 MSI 卸载器外加手动清理。这里最常见的问题是:卸载完 node,却发现 npm 命令还在,或者干脆在卸载前就遇到了npm.ps1被禁止运行的尴尬。
4.1 常规卸载流程:程序和功能、目录、环境变量三件套
如果你是在“设置 → 应用”或“控制面板 → 程序和功能”里看到 Node.js,直接走系统卸载,MSI 卸载器会把C:\Program Files\nodejs(或你当初自定义的路径,比如热搜里常见的D:\Program Files (x86)\nodejs)里的主文件清掉。
但光这一步不够,MSI 卸载器并不会清理全局包和缓存,因为那些目录是在用户主目录下,不在 MSI 的监控范围里。卸载完后需要手动处理这几处:
C:\Program Files\nodejs或D:\Program Files (x86)\nodejs——如果卸载器没删干净,手动删;%APPDATA%\npm——全局包目录,里面有node_modules和一堆.cmd脚本;%APPDATA%\npm-cache——npm 缓存目录;%LOCALAPPDATA%\Programs\Nodejs——部分安装包版本的实际安装目录。
然后是环境变量。右键“此电脑 → 属性 → 高级系统设置 → 环境变量”,在用户变量和系统变量的 Path 里,删掉所有包含nodejs、%APPDATA%\npm的条目。
这里有个血泪教训:动手改 Path 之前,先把当前值复制到文本编辑器里存一份。Windows 的编辑器没有 Ctrl+Z,删错了想恢复,只能靠这份备份。另外要注意,有些版本的 npm 会在用户变量 Path 里写入C:\Users\用户名\AppData\Roaming\npm,而不是%APPDATA%\npm,搜路径时两个关键词都要搜。
4.2 全局包与缓存目录:一定要手动补刀
如果你的 npm 还能运行,卸载前先列一下全局包,方便之后重装:
npm ls -g --depth=0不需要逐条卸载这些包,因为它们会随%APPDATA%\npm整个目录被删掉。真正要确认的是:这个目录本身还残留了什么。我见过的情况是,Node 从 14 升级到 18 之后,%APPDATA%\npm里的旧ng.cmd、nest.cmd还留着,新版的命令行工具装不上,就是因为旧.cmd文件占着路径却指向不存在的旧包。
清理命令就是删除目录,PowerShell 下可以这样:
Remove-Item -Recurse -Force "$env:APPDATA\npm" Remove-Item -Recurse -Force "$env:APPDATA\npm-cache"顺手检查一下%LOCALAPPDATA%下有没有.npmrc之类的隐藏文件,有就一起删。
4.3 npm.ps1 被禁止运行的根源:PowerShell 执行策略
很多人没卸载前就被这个问题卡住:在 PowerShell 里敲npm,报错内容大致是:
npm : 无法加载文件 D:\Program Files (x86)\nodejs\npm.ps1,因为在此系统上禁止运行脚本。这个报错的根源和 node 本身没关系,是 PowerShell 的执行策略(ExecutionPolicy)在管。Windows 上的 npm 提供了两个入口:npm.cmd给 cmd 用,npm.ps1给 PowerShell 用。PowerShell 的默认执行策略是 Restricted,意思是任何.ps1脚本都不允许运行。所以,不是 npm 坏了,是 PowerShell 拒了它。
不想动执行策略的话,有两个绕过方式。第一,在 cmd 里运行npm,cmd 会走npm.cmd,完全不受影响;第二,在 PowerShell 里显式调用npm.cmd,比如npm.cmd config list。
如果你希望 PowerShell 里能正常用 npm,最正规的做法是只修改当前用户的执行策略:
Set-ExecutionPolicy -Scope CurrentUser RemoteSignedRemoteSigned的意思是:本地创建的脚本可以运行,从互联网下载的脚本必须带可信签名。这样既解决了 npm.ps1 的问题,又不会把系统执行策略放开到不安全级别。执行完之后可以用Get-ExecutionPolicy -List确认一下各作用域的设置层级。
这里多说一句:网上很多教程让你Set-ExecutionPolicy Unrestricted,其实没必要。为了一个 npm 命令把整个执行策略放开,等于把所有本地脚本都赋予了运行权限,风险不划算。RemoteSigned足够满足日常使用。
如果你是因为打算卸载 node 才遇到这个报错,其实可以跳过它直接走 4.1 的 MSI 卸载流程,并不需要先解决 npm 能不能用。只有当你希望在卸载前备份全局包列表时,才需要先靠上面任一方式把 npm 跑起来。
5. 清完之后怎么装一个“不太容易再坏”的环境
卸载不是最终目的,重装一套清爽的环境才是。这一章分享的安装思路,核心是:别再用“官网下载 pkg + 一路下一步”的方式装 node,也别再裸奔一个 homebrew 不管镜像。这两件事,各自都有可以提前避坑的成熟做法。
5.1 node 环境:用版本管理器,别用官方 pkg 直接上
重装 node 时,强烈建议用版本管理器,macOS 上最常用的是 nvm,Windows 上是 nvm-windows。用版本管理器有几个好处:版本随意切换、按项目独立指定 node 版本、卸载时只要删目录和配置即可,不会再留下 pkg 安装那种遍布系统的软链。
macOS 的 nvm 安装脚本是这样的:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash如果raw.githubusercontent.com访问不稳定导致脚本拉不下来,可以多试几次,或者先把 install.sh 内容保存到本地再bash install.sh,核心是让脚本在你的 shell 配置里写入 nvm 的初始化代码。装完重开终端,然后:
nvm install 18 nvm use 18 nvm lsWindows 用户则去 nvm-windows 的 release 页面下载安装包,安装前确保旧 node 已经按上一章流程清理干净,否则 nvm 会检测到已有 node 环境并提示你先卸载。
如果对 nvm 的加载速度不满意,mac 上还可以考虑 fnm 或 volta,它们都是 Rust 写的版本管理器,切换速度更快,但基本思路一样。我个人的建议是:在“能切换 node 版本”这个层面,nvm 的资料最多,踩坑方案最好找,新手先选 nvm 最稳。
5.2 npm 镜像源一次配好,不再被网络卡脖子
npm 默认 registry 是官方源,国内访问速度不稳定,装个大型依赖动辄卡十几分钟。镜像源是标准做法,目前最常用的是 npmmirror(原淘宝镜像):
npm config set registry https://registry.npmmirror.com npm config get registry这里插一句关于.npmrc的建议:镜像源配置会写进~/.npmrc(Windows 是%USERPROFILE%\.npmrc),重装完第一次执行npm config get registry确认输出是你配置的地址,再装包,能省掉很多“下载超时”的排查时间。
如果你在公司环境里有私有源,配置思路也是一样的,只是 registry 地址换成公司源,必要时再配合.npmrc里的@scope:registry做包级路由。
5.3 重装 homebrew 的国内网络姿势
热词里经常看到“mac 安装 homebrew 失败”“国内 mac 安装 homebrew 失败”,核心原因大多是安装脚本和后续下载都依赖访问 GitHub 和 ghcr.io 的存储服务,网络稍不稳定就断在半路。
在确认网络正常情况下,推荐在安装前先设置几个国内镜像环境变量。以清华 TUNA 的 Homebrew 镜像为例,在终端里先执行:
export HOMEBREW_API_DOMAIN="https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/api" export HOMEBREW_BOTTLE_DOMAIN="https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles" 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"然后再执行官方安装脚本:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"建议把这些 export 写入~/.zshrc持久化,否则新终端窗口又丢了。装完后运行brew --version、brew install任一包试试,确认走了镜像源。需要注意:镜像站的路径可能随版本调整,以对应镜像站首页说明为准;另外不同镜像(阿里、中科大、腾讯)给出的变量名和 URL 可能略有差异,选一家即可,别叠加混用。
我还想提一个更省事的思路:如果只是开发 node 项目,不装全局编译型工具,homebrew 其实不是必需品。node 版本管理 + npm 全局包基本覆盖了九成场景。先想清楚是否真的需要一个系统级包管理器,再决定要不要折腾 brew,能省掉很多后续维护成本。
卸载这件事,我最深的体会是:清理的不只是“软件本身”,而是它留在系统里的所有痕迹。每次图省事只跑一条官方卸载命令,过两周大概率会因为某个 PATH 残留回来补课。按本文链路走一遍,大概十来分钟,换来的是一个 node 相关命令完全可控的干净环境。至少对我和我身边的同事来说,这笔时间花得相当值。