我很早就发现一个规律:凡是直接复制粘贴 npm、pnpm、yarn 命令却没有先看运行环境的人,大概率会在几个报错里反复绕圈——npm.ps1 无法加载、pnpm 不是内部或外部命令、certificate has expired、pnpm approve-builds。表面看这些都是“装不上”的玄学问题,实际上是把三个层面搞混了:依赖在 node_modules 里怎么放、命令本身从哪里来、下载走哪条网络通道。
这篇文章不打算罗列一份“全网最全命令表”就完事,我会把机制、安装环境、日常命令、高频报错放成一条线来讲。你看完以后,遇到包管理器相关的问题,能自己顺着报错往回推,而不是再复制三份不同的命令碰运气。
1. 三个工具对 node_modules 的差异,决定了你该抄谁的命令
1.1 npm 的依赖提升:平坦但留下“幽灵依赖”
npm 随 Node 一起分发,是绝大多数人接触的第一个包管理器。但很多人不知道,npm 的依赖布局并不是“天然合理”的。npm v2 时代是严格的嵌套结构,每个依赖又各自维护自己的node_modules/xxx,在网络差、依赖层级深的时候,不仅安装极慢,还会在 Windows 上触发“路径过长”问题。所以 npm v3 开始转向“依赖提升(hoisting)”:安装时尽量把所有包平铺到根目录node_modules下,只有遇到版本冲突才把冲突的子包嵌套到依赖它的包目录里。
这个改动解决了性能问题,却带来了三个长期被忽视的副作用。
第一,node_modules是“安装时结果”,同一份 package.json 在不同时间、不同网络、不同顺序下安装,最终提升到顶层的是谁可能不一样。第二,代码里能require到某个包,但它并没有出现在 package.json 里,这就是社区常说的“幽灵依赖”。第三,package-lock.json 能锁住版本号,却锁不住依赖树里“顶层提升谁”的过程。
很多从 npm 项目迁到 pnpm 的人会突然遇到Cannot find module,并不是 pnpm 故意找茬,而是旧代码在开发时就已经悄悄依赖了 npm 的宽松机制。读懂这一点,后面很多报错就都有了答案。
1.2 Yarn 的经典方案与 Yarn Berry 的激进转向
Yarn Classic(大家最常见的 yarn 1.x)刚出来时的卖点不是“不同的 node_modules”,而是确定性安装、离线缓存和并行下载。它解决的痛点很明确:npm 当时在团队协作里经常出现“我这能跑你那跑不了”的问题,yarn.lock 让依赖版本第一次真正做到全团队一致。所以你到现在还能看到大量老项目用 yarn 1,锁文件一提交,安装结果就很稳定。
真正称得上“机制级变化”的是 Yarn Berry(yarn 2+),它默认启用 Plug'n'Play(PnP),项目里可以完全不生成 node_modules,改用.pnp.cjs文件记录依赖映射,依赖包以 zip 形式缓存在.yarn/cache。这种设计安装极快、依赖边界严格,但门槛也很高:很多编辑器、ESLint 插件、原生模块编译工具都需要针对 PnP 做适配;过去“手动翻一下 node_modules 找东西”的操作习惯也全部失效。所以 Yarn Berry 推到今天,在社区里仍然口碑两极分化,不是它不先进,而是它对你的工程习惯要求明显上了一个台阶。看到“yarn 比 npm 好”就盲目切换,大概率会卡在配置期。
1.3 pnpm 的存储基因:硬链接、软链和严格依赖边界
pnpm 把“机制”两个字体现得最明显。第一次安装包时,pnpm 会把包解压进一个全局 store:Mac/Linux 一般在~/.local/share/pnpm/store,Windows 上一般在%LOCALAPPDATA%\pnpm\store,用pnpm store path可以查看具体位置。项目的node_modules并不是物理复制,而是这样的结构:
- 项目根目录的
node_modules下只保留直接依赖的符号链接; - 真实文件统一放在
node_modules/.pnpm/<pkg>@<version>/node_modules/<pkg>下,再通过硬链接指向全局 store 里的物理文件; - 每个包在
.pnpm/<pkg>@<version>/node_modules/下只能看到自己声明过的依赖,越权 require 别的包会直接失败。
这套设计带来了三个好处:多项目磁盘占用断崖式下降;安装速度因为大量硬链接避免了解压复制而明显变快;幽灵依赖被从根本上拦住。代价同样存在,硬链接不能跨文件系统,如果 global store 和项目在不同盘符,会退化成复制,速度和磁盘收益都会打折。另外 pnpm 对依赖的 postinstall 脚本默认不自动执行,esbuild、sharp 这类需要编译或下载二进制的包就会被挡住,这也就是后来那句Run "pnpm approve-builds" to pick which dependencies should be allowed to run的由来。
1.4 机制差异汇总表
| 维度 | npm | Yarn Classic | pnpm |
|---|---|---|---|
| 依赖布局 | 扁平提升,层级不固定 | 扁平提升,但确定性更强 | 符号链接 + 硬链接,严格隔离 |
| 幽灵依赖 | 存在 | 存在 | 基本杜绝 |
| 锁文件 | package-lock.json | yarn.lock | pnpm-lock.yaml |
| 默认是否运行依赖 postinstall 脚本 | 允许 | 允许 | 默认阻止,需 approve-builds |
| 多项目磁盘复用 | 无 | 无 | 全局 store 复用 |
| 适合场景 | 默认选择,兼容性最好 | 老项目稳定维护 | 大型仓库、Monorepo、磁盘敏感环境 |
做完这层对比,你再看热词里那些“npm 安装”“pnpm 下载失败”“yarn 管理”的问题,会发现大部分都不是工具本身坏了,而是机制预期不同。先选对工具,再谈命令。
2. 安装、PATH、registry:命令“找不到”比“报错”更常见
2.1 用 npm 还是 corepack 安装 pnpm 和 yarn
最常见的安装方式就两条:
npm install -g pnpm npm install -g yarn但我更推荐在 Node 16.9+ 上使用自带的 corepack:
corepack enable corepack prepare pnpm@latest --activatecorepack 的好处是可以按项目锁定包管理器版本。团队里有人用 pnpm 7、有人用 pnpm 10,这种混乱很容易让同一条pnpm install在制造不同格式的 lockfile。用 corepack 配合packageManager字段,能在项目层面实现统一:
{ "packageManager": "pnpm@10.0.0" }热词里大量出现的“pnpm 下载失败”,其实分很多种:网络层失败、registry 源不通、磁盘缓存损坏、版本不存在。“失败后不要疯狂重试”,先看清报错文本属于哪一层。网络层就换源或检查代理设置,证书层就按第五章的链路排查,下载一半失败就清缓存重试。不看报错反复安装,很容易把临时故障固化成环境问题。
2.2 终端不认识命令时,先查 PATH 而不是重装
这个问题在 Windows 上尤其常见:你明明执行了npm install -g pnpm,安装过程也没报错,但新开一个终端输入pnpm -v,系统却回一句“pnpm 不是内部或外部命令,也不是可运行的程序或批处理文件”。
原因很简单:npm 的全局 bin 目录没在 PATH 环境变量里。排查链路如下:
第一步,确认包确实装上了。
npm ls -g --depth=0能看到pnpm或yarn,说明安装成功,问题不在安装本身。
第二步,拿到 npm 全局目录位置。
npm config get prefixWindows 下常见的结果是C:\Users\<你的用户名>\AppData\Roaming\npm,你到那个目录下能看到pnpm.cmd或pnpm.ps1。
第三步,把这个目录加到用户 PATH 里,然后重开终端。
macOS 和 Linux 上用which pnpm,如果输出空,检查~/.zshrc或~/.bashrc里是否包含 npm 全局 bin 路径。
还有个隐蔽坑:如果用了 nvm 或 fnm 这类 Node 版本管理工具,全局包是装在“当前 Node 版本”对应的路径下的。切换 Node 版本后,全局命令可能瞬间消失。这不是包被卸载了,是路径切换了。要么固定默认 Node 版本,要么每个版本都重新装一次全局工具,别在版本切换后还指望命令能在原地等着你。
2.3 PowerShell 的“禁止运行脚本”到底卡在哪
npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。这可以说是 Windows 本地开发最经典的问题。看到这句话,先要明白它跟 Node 没关系,它是 PowerShell 执行策略拦截了.ps1脚本。npm 在 PowerShell 里会调用npm.ps1,而 Windows 客户端默认的执行策略是 Restricted,本地脚本一律不允许运行。
解决方法是用管理员或当前用户身份放开一个比较安全的策略:
Get-ExecutionPolicy -List Set-ExecutionPolicy -Scope CurrentUser RemoteSignedRemoteSigned的意思是:本地脚本没有签名也可以运行,从网上下载的脚本必须带可信签名。日常开发够用了,不要图省事直接设成Unrestricted,那是把所有下载脚本都放行,等于把供应链风险敞口开大。如果你在公司电脑上遇到 LocalMachine 策略被组织策略锁死,别去硬改,直接用 cmd 或 Windows Terminal 里的 Command Prompt 跑 npm,绕开 PowerShell 执行策略即可。这种场景下,你需要的不是“解锁”,而是“绕行”。
2.4 registry 镜像源的配置边界与旧域名陷阱
热词里那条npm ERR! code cert_has_expired很有代表性,它指向的请求地址是https://registry.npm.taobao.org/...。这个老域名已经停止维护了,TLS 证书过期,但很多人当年的全局.npmrc里还一直留着它。
三个工具其实都会读.npmrc,配置字段也基本一致。查看方式:
npm config get registry pnpm config get registry yarn config get registry如果没有特殊原因,官方源:
npm config set registry https://registry.npmjs.org/需要国内加速,用新的 npmmirror 域名,不是旧 taobao:
npm config set registry https://registry.npmmirror.com/.npmrc的优先级是:项目本地.npmrc> 用户目录.npmrc> 全局.npmrc。所以项目里的.npmrc写错了,用户级配置是覆盖不了它的。我在帮别人排查时,经常发现明明改好了全局配置,跑项目还报旧源错误,最后一看是项目根目录的.npmrc里残留了registry=https://registry.npm.taobao.org。求你检查到这一层再动手重装。
另外提醒一点,strict-ssl=false只能是临时调试手段,别把它写进配置里长期使用。关闭证书校验等于把所有依赖包的完整性都交给网络链路,一旦中间被改动,你根本察觉不到。
3. 命令地图:高频操作对照与迁移注意事项
3.1 日常高频命令对照表
下面这张表覆盖了平时 90% 的操作,建议直接收藏:
| 操作 | npm | pnpm | Yarn Classic |
|---|---|---|---|
| 安装项目全部依赖 | npm install | pnpm install | yarn或yarn install |
| 添加 dependencies | npm install pkg | pnpm add pkg | yarn add pkg |
| 添加 devDependencies | npm install -D pkg | pnpm add -D pkg | yarn add -D pkg |
| 移除依赖 | npm uninstall pkg | pnpm remove pkg | yarn remove pkg |
| 更新依赖版本 | npm update pkg | pnpm update pkg | yarn upgrade pkg |
| 运行脚本 | npm run xxx | pnpm run xxx或pnpm xxx | yarn run xxx或yarn xxx |
| 全局安装 | npm install -g pkg | pnpm add -g pkg | yarn global add pkg |
| 一次性执行工具包 | npx pkg | pnpm dlx pkg | yarn dlx pkg(Yarn 2+) |
| 查看全局 bin 目录 | npm prefix -g | pnpm bin -g | yarn global bin |
| 清理缓存 | npm cache clean --force | pnpm store prune | yarn cache clean |
很多人第一次用 pnpm,容易把pnpm add -g记成pnpm install -g。如果你用pnpm install -g pkg,pnpm 会把它当成“当前项目全局安装目录下的指令”,行为跟 npm 完全不同。这也是为什么我一直强调“抄命令之前先看机制”——npm 的install和 pnpm 的install根本不是同一个语义。
3.2 从 npm/yarn 迁移到 pnpm 时要做的几件事
团队切到 pnpm 不是改一个命令就完事,我建议按这个顺序走:
- 先确保当前 git 工作区干净,提交一次“迁移前备份”。这样中间出问题可以随时回滚。
- 删除旧的 lockfile 和 node_modules:
rm -rf node_modules package-lock.json yarn.lock- 如果希望尽量复用旧 lockfile 里的解析结果,先执行
pnpm import,它会根据旧的 package-lock.json 或 yarn.lock 生成 pnpm-lock.yaml。不执行也可以,直接让 pnpm 重新解析。 - 执行
pnpm install,注意观察输出里有没有Ignored build scripts。 - 如果出现被忽略的构建脚本,运行
pnpm approve-builds,或者把允许名单直接写进 package.json:
{ "pnpm": { "onlyBuiltDependencies": ["esbuild", "sharp"] } }- 检查项目根目录
.npmrc里有没有shamefully-hoist=true。这个配置会让 pnpm 临时复刻 npm 的扁平 node_modules,如果你有某个旧库必须访问幽灵依赖,只能靠它兼容。但这不是长期方案,迁移完成后应该逐步修掉那些越权依赖。
为什么我把“commit 一次备份”放在第一?因为迁移最大的风险不是命令不对,而是你在无回滚点的情况下把项目越改越乱,最后连“改了什么”都说不清。
3.3 同名命令在不同工具里的“语义差”
表格里能看出命令名不一样,但更坑的是那些“命令名一样、语义不一样”的角落。
npm link和pnpm link都用于本地包联调,但默认行为有差异。npm 的npm link会把全局包链到项目里,也可能把当前项目链到全局,具体看你执行位置;pnpm 则更强调显式参数,建议直接写pnpm link --global。我在 monorepo 里做本地联调时,最稳的做法是用 workspace protocol,而不是裸用 link。
yarn裸命令等价于yarn install,但npm裸命令只是打印帮助信息,并不执行安装。很多从 yarn 转 npm 的新人习惯性在终端敲一个npm,然后奇怪为什么没有装依赖——这不是 bug,是设计差异。
npx、pnpm dlx、yarn dlx三个都用于临时执行工具包,但执行完之后的缓存策略不同。npx在旧版本里会留下很多临时包,pnpm dlx用 store 缓存,比较克制。在 CI 里临时执行一个 CLI 工具,我更喜欢pnpm dlx,因为它不往项目目录里塞东西。
4. 高频报错排查链路:从报错文本反推机制
4.1 链路A:npm.ps1 无法运行脚本
报错文本前面已经贴过一遍,这里讲完整的排查思路。
第一,确认执行策略:
Get-ExecutionPolicy -List如果输出里CurrentUser或LocalMachine对应的是Restricted,这就是报错根源。
第二,修正当前用户策略:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned第三,重开终端验证npm -v。
这个链路里最常见的误区是去重装 Node。重装并不会改 PowerShell 执行策略,所以什么用都没有。还有一个误区是把策略设成Unrestricted,解决了一时但给以后埋雷,我前面已经说过原因。如果你处于公司策略管制的机器上,RemoteSigned也可能被组策略覆盖,这时候要么联系管理员开放,要么直接用 cmd 执行 npm。
4.2 链路B:“不是内部或外部命令”的三种藏身处
这个报错在 Windows 上有三个常见藏身处,按出现概率排序。
第一处是 PATH 缺 npm 全局目录。处理方式见 2.2,核心是npm config get prefix拿到目录,把它加进用户 PATH。
第二处是 Node 版本管理器切换后全局路径失效。用where pnpm看一下有没有命中,命中为空就说明当前 Node 版本下没有安装对应全局包。解决方法是给默认版本重装,或者直接用 corepack。
第三处是安装过程被安全软件拦截,导致.cmd文件根本没生成。在npm config get prefix返回的目录下看有没有pnpm.cmd,没看到就检查杀毒软件隔离区。这几年 Windows 上某些安全软件会把 pnpm 的 shim 误判为可疑文件,这个坑比想象中常见。
4.3 链路C:cert_has_expired 与旧淘宝源迁移
报错样本:
npm ERR! code cert_has_expired npm ERR! request to https://registry.npm.taobao.org/vuex-along/download/vuex-along-1.2.11.tgz failed, reason: certificate has expired从报错文本里可以直接看到registry.npm.taobao.org,这就是问题入口。处理分三步:
- 修改 registry:
npm config set registry https://registry.npmmirror.com- 清理可能存在的缓存:
npm cache clean --force- 重装依赖或重试命令。
还有一些边界情况。系统时间不对也会导致证书校验失败,这个在老旧机器或虚拟机里见过很多次。先把系统时间同步“自动设置”,再去追究证书问题。另外,如果项目级.npmrc里写死了旧域名,就算用户级配置改好了也会被覆盖,所以排查一定要先看项目根目录的.npmrc。
4.4 链路D:pnpm 阻断构建脚本,approve-builds 怎么用
pnpm 安装完成时输出:
Ignored build scripts: esbuild, sharp Run "pnpm approve-builds" to pick which dependencies should be allowed to run.这是 pnpm 的安全机制:依赖包声明的 postinstall 脚本默认不会被执行。原因很实际,依赖安装阶段执行任意脚本是供应链攻击的高发场景,npm 时代很多恶意包就是靠 postinstall 在开发者机器上跑代码。pnpm 选择了默认阻断,要求你显式授权。
两种放行方式:
方式一,交互式:
pnpm approve-builds然后按提示勾选需要放行的依赖。
方式二,写配置自动放行:
{ "pnpm": { "onlyBuiltDependencies": ["esbuild", "sharp"] } }如果安装完之后又改了配置,想重新触发构建脚本,可以执行:
pnpm rebuild有个反面操作也要提醒:有人嫌 approve-builds 烦,直接在.npmrc里写ignore-scripts=true。这个配置会把所有依赖脚本全部禁掉,esbuild、sharp 这类包安装后直接缺二进制文件,运行时才报错,排查起来更痛苦。安全机制是用来配合的,不是用来绕的。
5. 锁文件与 CI 场景:团队协作不要“三锁并存”
5.1 lockfile:每个工具都有自己的“走向记录”
package-lock.json、yarn.lock、pnpm-lock.yaml 虽然是不同格式,但核心目标一致:记录依赖解析的最终版本和来源,让所有人安装出同一棵树。lockfile 必须提交到 git,否则团队协作就是碰运气。
比“不提交 lockfile”更糟的是“同时提交多份 lockfile”。如果一个仓库里既能看到 package-lock.json,又能看到 yarn.lock 或 pnpm-lock.yaml,说明有人混用了不同工具。这会让 CI 反复生成不同的依赖树,轻则安装缓慢,重则出现只在某一台机器上能跑的 bug。
我参与过很多次这种清理,处理办法很简单:团队约定只保留一个工具,删除其他 lockfile,在 README 里写明“统一使用 pnpm”。如果历史包袱太重,至少要在 CI 入口加一个检查,发现多余 lockfile 直接报错,让混用问题在早期暴露。
5.2 CI 环境里最稳的三种安装姿势
| 工具 | CI 推荐命令 |
|---|---|
| npm | npm ci |
| pnpm | pnpm install --frozen-lockfile |
| Yarn Classic | yarn install --frozen-lockfile |
为什么 npm 要专门出一个npm ci?因为它会先删除 node_modules,再严格按照 package-lock.json 安装,并且不会修改 lockfile。普通npm install在依赖版本漂移时可能会悄悄更新 lockfile,这在 CI 里是不可接受的,因为你不知道这次构建到底装了什么。
pnpm 的对应参数是--frozen-lockfile,锁文件与 package.json 不一致时直接失败,绝不会自动改锁文件。团队里如果有人在本地用pnpm install跑出了新版本 lockfile,CI 会第一时间报警,这是好行为,不是故障。
还想提一个 CI 细节:缓存 pnpm 的全局 store 能显著提速。不同 CI 平台的缓存目录不一致,先执行pnpm store path拿到路径,再配置到 CI 缓存里。缓存失效时,先清空 store 再跑pnpm install,不要直接怀疑网络或依赖本身有问题。
5.3 用完 pnpm workspace 之后,命令往往会多一层
大型前端仓库普遍会升级到 monorepo 结构,pnpm 在这个场景下优势更明显。workspace 通过根目录pnpm-workspace.yaml声明:
packages: - packages/*子包之间的相互依赖可以直接用 workspace 协议:
{ "dependencies": { "@app/shared": "workspace:*" } }批量操作命令:
pnpm -r install pnpm -F @app/server dev-r表示递归执行,-F表示过滤到某个具体子包。npm 和 yarn 也有类似能力,但 pnpm 对 workspace 的依赖隔离做得更彻底。如果你在 monorepo 里曾经被“子包之间互相乱引用”折磨过,切换到 pnpm 会很有体感。
最后再分享几个我实际踩过的经验
第一个经验:切换包管理器之前,先提交一次干净的 git 状态,再多旧项目都别跳过这一步。中间想回滚随时能回,心里不慌,排查问题也敢放开手脚。
第二个经验:报错文本本身就是最精准的线索。cert_has_expired指向证书和源,not recognized as an internal or external command指向 PATH,Ignored build scripts指向 pnpm 安全策略。不要只把报错复制进搜索引擎,先看它属于哪一层:网络层、环境层、依赖层还是安全策略层。层判断对了,问题基本已经解决一半。
第三个经验:顺手的工程化习惯是“锁定包管理器版本”。无论是 corepack 的packageManager字段,还是 CI 里的--frozen-lockfile,本质都是把不确定性挡在外面。依赖管理和包管理器版本这两件事,都应该追求“确定”。确定才能复现,复现才能排错。