大家好,我是老纪。
pnpm
是一种Node.js
的包管理工具,用于帮助开发人员管理和安装项目所需的依赖项。它与其它包管理工具(如npm
和yarn
)类似,但有一些不同之处。pnpm
使用基于内容寻址的文件系统,来管理磁盘上依赖,使用硬链接和符号链接来共享依赖项,从而减少磁盘空间的使用量。它还支持并发安装,可以加快依赖项的安装速度。
总的来说,pnpm
旨在提供更快速、更高效的包管理体验。
为什么不用npm和cnpm
Node.js的包管理工具,比较知名的有npm、yarn、pnpm、cnpm,像Deno、Bun这种底层另起炉灶的不计算在内。
这几个里,npm首先被排除出局。它唯一的优势是Node.js官方出品,但缺点非常明显:
- 安装速度慢。
- 安装过程不可靠。npm初始设计没有锁定依赖版本,这才有了yarn的出现。而同一项目中,同一个包不同版本的管理是个复杂的话题,npm也很努力地在解决各种问题,安装策略在不断调整,导致换个npm版本,可能项目就跑不起来了。朝令夕改,不向下兼容,这对于团队开发无疑是致命的。
cnpm是淘宝团队为了解决npm在国内下载速度慢的问题而开发的,它通过镜像加速了包的下载和安装过程,提供了更快的速度和稳定性。
由于我们团队有自己的npm私服,不能使用cnpm,这是根本原因。而cnpm有没有其它方面比如幽灵依赖、npm分身等优化,这块没有调研,不予置喙。
pnpm vs. yarn
pnpm和yarn几乎一同出道,都是2016年的作品,当然,它正式发布要稍晚些。
star
我们看下pnpm的star,已经有24.7K:
与yarn的41.2K还有段距离,但已经完全是同一数量级了。
更重要的是,Vue3已经迁移到了pnpm: Vite也一样:
所以,从成熟角度讲,pnpm已经完全可以依赖,有什么坑,尤雨溪已经帮你踩过了。还有微软多年来已成功使用 pnpm 来管理大型 monorepo,有完整的实战实践。
全面对比
- 磁盘空间占用:相对于npm和yarn,pnpm在磁盘空间占用方面更加高效。pnpm使用了符号链接(symlink)来共享依赖项,因此每个依赖项只需要在磁盘上存储一次,可以节省大量的磁盘空间。
- 性能:pnpm和yarn都旨在提高包安装的性能。它们通过并行下载和使用本地缓存来加快安装速度。根据不同的项目和网络条件,它们的性能可能有所不同。由于pnpm使用硬链接,在已有缓存的情况下,它的安装速度会更快。
- 版本控制:yarn和pnpm都支持锁定文件来确保在不同环境中安装相同的依赖项版本。它们都会生成一个
yarn.lock
或pnpm-lock.yaml
文件,以记录确切的依赖项版本。这有助于确保项目的构建和部署的一致性。 - 幽灵依赖和npm分身:yarn采取平铺的
node_modules
结构,避免不了幽灵依赖和npm分身问题。而pnpm的链接机制,巧妙地解决了这两个顽疾。本文不详细阐述其中区别,有兴趣的看看《平铺的结构不是 node_modules 的唯一实现方式》和《npm分身》这两篇文章。 - 社区支持和生态系统:yarn是由Facebook开发并得到广泛使用的,因此拥有一个庞大的社区支持和生态系统。与此相比,pnpm的社区和生态系统相对较小。但随着Vue与Vite的入坑,双方差距已经可以忽略不计。
再来一个pnpm官方的对比表格:
功能 | pnpm | Yarn | npm |
---|---|---|---|
工作空间支持(monorepo) | ✔️ | ✔️ | ✔️ |
隔离的 node_modules | ✔️ - 默认 | ✔️ | ✔️ |
提升的 node_modules | ✔️ | ✔️ | ✔️ - 默认 |
自动安装 peers | ✔️ | ❌ | ✔️ |
Plug'n'Play | ✔️ | ✔️ - 默认 | ❌ |
零安装 | ❌ | ✔️ | ❌ |
修补依赖项 | ✔️ | ✔️ | ❌ |
管理 Node.js 版本 | ✔️ | ❌ | ❌ |
有锁文件 | ✔️ - pnpm-lock.yaml | ✔️ - yarn.lock | ✔️ - package-lock.json |
支持覆盖 | ✔️ | ✔️ - 通过 resolutions | ✔️ |
内容可寻址存储 | ✔️ | ❌ | ❌ |
动态包执行 | ✔️ - 通过 pnpm dlx | ✔️ - 通过 yarn dlx | ✔️ - 通过 npx |
Side-effects cache | ✔️ | ❌ | ❌ |
Listing licenses | ✔️ - Via pnpm licenses list | ✔️ - Via a plugin | ❌ |
以下是pnpm使用硬连接存储的示例图:
迁移
如果你已经有一个项目在使用npm或yarn,那么你想迁移到pnpm上,应该怎么做?
你可能会三下五除二,直接pnpm install
开干,那样就丧失了原来lock
文件记录的版本优势。最佳姿势是使用pnpm import:
bash
pnpm import yarn.lock
pnpm import package-lock.json
以我某个工程为例:

生成一个pnpm-lock.yaml文件:
bash
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
arg:
specifier: 5.0.0
version: 5.0.0
chalk:
specifier: 4.1.1
version: 4.1.1
pnpm可谓是非常贴心了。
之后删除node_modules
与yarn.lock
,再用pnpm i
安装依赖即可。
旧的项目这样安装完依赖,运行时仍有可能报错,缺失某个包,这是因为pnpm的包安装策略与yarn、npm都有所差别。这时缺哪个包,就安装哪个包,大部分情况就OK了。
特性功能
pnpm完全兼容npm的功能,常规命令如安装依赖、钩子等就不提了,pnpm官网上都有。这里说几个比较有意思的。
打补丁
打补丁是个非常实用的功能。我在《奇技淫巧:如何修改第三方npm包?》一文中已经说的很详细了,这里就不重复了。
管理Node.js版本
神奇的是,pnpm居然还能管理Node.js
版本,这有点儿抢nvm、n饭碗的意思。
不过它们之间是有冲突的,原理很简单,使用Node.js哪个版本是看哪个脚本的优先级高,比如我机器的nvm
在.zshrc
中是这样的:
bash
export NVM_DIR="$HOME/.nvm"
[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh" # This loads nvm
[ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" # This loads nvm bash_completion
这时Node.js的版本就由nvm控制了:
bash
jw@MacBook ~ % node -v
v16.16.0
jw@MacBook ~ % which node
/Users/jw/.nvm/versions/node/v16.16.0/bin/node
但当使用了pnpm管理后,.zshrc
中多了一段:
bash
# pnpm
export PNPM_HOME="/Users/jw/Library/pnpm"
case ":$PATH:" in
*":$PNPM_HOME:"*) ;;
*) export PATH="$PNPM_HOME:$PATH" ;;
esac
# pnpm end
再看Node.js版本又由pnpm接管了:
bash
jw@MacBook ~ % node -v
v18.17.0
jw@MacBook ~ % which node
/Users/jw/Library/pnpm/node
pnpm安装Node.js很简单:
bash
pnpm env use --global lts
pnpm env use --global 16
移除:
bash
pnpm env remove --global 14.0.0
列表:
bash
pnpm env list
pnpm env list --remote
pnpm env list --remote 16
可以在.npmrc
文件中强制指定Node.js版本:
bash
use-node-version=18.14.2
node-mirror:release=https://npmmirror.com/mirrors/node/
node-mirror:rc=https://npmmirror.com/mirrors/node-rc/
node-mirror:nightly=https://npmmirror.com/mirrors/node-nightly/
安全检查
检查已安装包的已知安全问题,可以执行pnpm audit
进行安全检查。其实这个命令是npm就有,算不上特色。因为用淘宝镜像不行,需要指定官方源:
bash
pnpm audit --registry=https://registry.npmjs.org
这是一个输出样例: 如果发现安全问题,请尝试通过
pnpm update
更新依赖项。
一定要指定具体包,千万别全量更新,相信我,如果不是新的工程,你十有八九没有这个好运气。
如果简单的更新不能解决所有问题,请使用 overrides来强制使用不易受攻击的版本。
例如,如果 lodash@<2.1.0 易受攻击,可用这个overrides来强制使用 lodash@^2.1.0:
json
{
"pnpm": {
"overrides": {
"lodash@<2.1.0": "^2.1.0"
}
}
}
或者,运行 pnpm audit --fix
:
bash
pnpm audit --fix --registry=https://registry.npmjs.org
如果你想容忍一些不影响项目的漏洞,可以使用 pnpm.auditConfig.ignoreCves 设置。
工作空间
pnpm 内置了对单一存储库(也称为多包存储库、多项目存储库或单体存储库,也就是monorepo项目)的支持, 你可以创建一个workspace,将多个项目合并到一个仓库中。
本文开篇Vue3、Vite的仓库截图都能看到一个文件 pnpm-workspace.yaml。这是Vite工程的文件内容:
yaml
packages:
- 'packages/*'
- 'playground/**'
package.json中:
json
"scripts": {
"build": "pnpm -r --filter='./packages/*' run build",
"dev": "pnpm -r --parallel --filter='./packages/*' run dev",
"release": "tsx scripts/release.ts",
"ci-publish": "tsx scripts/publishCI.ts"
},
"devDependencies": {
"vite": "workspace:*"
}
这是packages下的子工程: 打开packages/plugin-legacy/package.json,可以看到scripts的内容:
json
"scripts": {
"dev": "unbuild --stub",
"build": "unbuild && pnpm run patch-cjs",
"patch-cjs": "tsx ../../scripts/patchCJS.ts",
"prepublishOnly": "npm run build"
}
这几个命令都是父工程可以调用的,比如父工程的release,是用来发布版本用的,脚本里会触发子工程的publish,也就是上面的prepublishOnly
钩子:
javascript
async function publishPackage(pkdDir, tag, provenance) {
const publicArgs = ["publish", "--access", "public"];
if (tag) {
publicArgs.push(`--tag`, tag);
}
if (provenance) {
publicArgs.push(`--provenance`);
}
await runIfNotDry("npm", publicArgs, {
cwd: pkdDir
});
}
这段代码出自@vitejs/release-scripts。有意思的是,按理说这个包应该属于Vite这个工程的一部分,现在却是独立的,在包的package.json里也找不到代码仓库,不知道出于什么考虑。看历史提交记录是github.com/vitejs/vite...,或许是觉得放当前仓库里有鸡生蛋蛋生鸡的哲学拷问?
pnpm的存储特性,使得它构建monorepo项目更有优势。还一个有趣的发现是Vue与Vite都没有采用市面上流行的monorepo工具,如Rush、Nx或者Lerna,而是自己直接基于pnpm写了套脚本。这也侧面说明pnpm工作空间的价值,只需要少量的脚本,就能搭建一个不错的monorepo项目。
总结
本文简单介绍了pnpm的优点,介绍了如何将项目从npm、yarn迁移,以及一些特性功能,比如补丁、管理Node.js版本、安全检查和工作空间。随着Vue社区开始拥抱pnpm,相信pnpm将会变得越来越受欢迎。