乾坤挪移:为什么推荐你使用pnpm?

大家好,我是老纪。

pnpm是一种Node.js的包管理工具,用于帮助开发人员管理和安装项目所需的依赖项。它与其它包管理工具(如npmyarn)类似,但有一些不同之处。pnpm使用基于内容寻址的文件系统,来管理磁盘上依赖,使用硬链接和符号链接来共享依赖项,从而减少磁盘空间的使用量。它还支持并发安装,可以加快依赖项的安装速度。

总的来说,pnpm旨在提供更快速、更高效的包管理体验。

为什么不用npm和cnpm

Node.js的包管理工具,比较知名的有npm、yarn、pnpm、cnpm,像Deno、Bun这种底层另起炉灶的不计算在内。

这几个里,npm首先被排除出局。它唯一的优势是Node.js官方出品,但缺点非常明显:

  1. 安装速度慢。
  2. 安装过程不可靠。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,有完整的实战实践。

全面对比

  1. 磁盘空间占用:相对于npm和yarn,pnpm在磁盘空间占用方面更加高效。pnpm使用了符号链接(symlink)来共享依赖项,因此每个依赖项只需要在磁盘上存储一次,可以节省大量的磁盘空间。
  2. 性能:pnpm和yarn都旨在提高包安装的性能。它们通过并行下载和使用本地缓存来加快安装速度。根据不同的项目和网络条件,它们的性能可能有所不同。由于pnpm使用硬链接,在已有缓存的情况下,它的安装速度会更快。
  3. 版本控制:yarn和pnpm都支持锁定文件来确保在不同环境中安装相同的依赖项版本。它们都会生成一个 yarn.lockpnpm-lock.yaml 文件,以记录确切的依赖项版本。这有助于确保项目的构建和部署的一致性。
  4. 幽灵依赖和npm分身:yarn采取平铺的node_modules结构,避免不了幽灵依赖和npm分身问题。而pnpm的链接机制,巧妙地解决了这两个顽疾。本文不详细阐述其中区别,有兴趣的看看《平铺的结构不是 node_modules 的唯一实现方式》和《npm分身》这两篇文章。
  5. 社区支持和生态系统: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_modulesyarn.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将会变得越来越受欢迎。

相关推荐
weixin_382395239 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__9 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.10 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~11 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华11 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子14 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅14 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni14 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学15 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端