乾坤挪移:为什么推荐你使用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将会变得越来越受欢迎。

相关推荐
Fan_web10 分钟前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常11 分钟前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇1 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr1 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho2 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常3 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记4 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java4 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele4 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范