百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

新一代包管理工具 pnpm(npm包管理工具使用场景)

liuian 2025-05-03 15:15 5 浏览


背景

如果你从事过前端方面的工作和开发,相信你对 npmyarn 这样的工具已经再熟悉不过了。作为包管理工具,npm 已经有了长足的历史[1]。一个项目初始化过程都需要通过 npm install 命令安装相关的依赖到 node_modules 目录下,对于较大型的前端项目,node_modules 的大小很多时候是超乎我们想象的。而 npm 在版本更新迭代的过程中,也一直在优化这个问题,下面我们就简要聊聊 npm 的发展历程。

npm 发展

npm2

npm2 发展阶段,安装依赖是相对比较直接的,它会直接按照配置文件 package.json 中的依赖项去下载相关依赖包,而依赖包的组织形式则是按照树形结构去排列的。由于不同的包的依赖关系在版本上差异较大,依赖关系相对复杂,所以 npm2 直接按照配置去下载并组织依赖的方式,是简单明晰的做法,保证了各个依赖的独立性,在依赖变更时,相互并不影响,其关系可以通过下图来描述:

从上图中,我们可以看到

  1. ABC 包相互独立
  2. ABC 包可能会依赖相同的包,比如 D@1.0
  3. ABC 包可能会存在较深的依赖层级,比如 C package

其中 23 这两点的负面影响会随着项目复杂度而上升,可能会导致的几个问题

  • 较大的冗余。多次下载的相同的依赖包 D@1.0,无法实现共享
  • 较深层级的依赖树。
    • Too many dependencies break the Windows file system[2]
    • Maximum Path Length Limitation[3]
    • Why does the 260 character path length limit exist in Windows?[4]
    • node_modules 依赖包路径过长,超出操作系统最长路径限制( windows260 字符,macos1024 字符),参见:
    • 太深的层级导致文件查找复杂度上升,严重影响性能,增加耗时

Note: 通过 npm ls --depth=n 查看项目相关依赖层级深度

npm3

为解决 npm2 中存在的冗余和依赖树问题,npm3 对依赖项进行了依赖扁平化讨论和处理[5]

扁平化具体来讲就是依赖不在按照树型进行安装,而是安装将依赖安装在同级目录下,npm install安装依赖时,会按照配置文件 package.json 里的依赖顺序进行解析,遇到新包就把它放在第一层级的目录(如 D@1.0、E@1.0、F@1.0),后面如果遇到第一级目录已有的包,会先进行依赖版本判断,如果版本一样则忽略,否则会按照 npm2 的方式依次挂在依赖包目录下,这样处理的原理遵循了`Nodejs`的依赖解析规则[6]当前目录下没有找到node_modules,它将递归解析父目录下的node_modules

使用 npm3 安装依赖后如下图:

这种扁平化处理方式一定程度上缓解了冗余和依赖树问题,同时 npm3 还支持动态安装更新包,如果依赖有更新,可以通过 npm dedupe 命令对依赖树进行优化。

但是 npm3 也存在部分问题,比如:

  • phantom_deps(幻影依赖)[7]npm3不会以确定的方式安装依赖项。举例来说:我们在 NodeJSrequire() 的函数,不需要考虑配置文件 package.json 中是否有该依赖项。这可能会导致依赖版本不兼容,并且开发者不容易发现;另外,由于`Nodejs`的依赖解析规则[8],这还会导致幻影 node_modules ,即依赖向上查找,可能会越过代码目录自身的 node_modules 。如下:
- my-monorepo/
  - package.json
  - node_modules/
    - semver/
    - ...
  - my-monorepo/my-library/
    - package.json
    - lib/
      - index.js
    - node_modules/
      - brace-expansion
      - minimatch
      - ...

my-monorepo/my-library/lib/index.js 可能使用的是my-monorepo/node_modules 中的依赖,而非自身目录 my-monorepo/my-library/node_modules

  • npm doppelgangers(npm 分身)[9]。简单来讲,npm 分身是指同一个依赖的不同版本会出现在 node_modules 中,比如项目中同时依赖了 A@1.0.0A@2.0.0,无论是扁平化处理A@1.0.0A@2.0.0,另一个依赖还是会被重复,如果这样的分身较多,就会导致一些潜在问题,比如扩展包大小变大、相关类型校验交叉等

npm5

npm5 通过添加 lock 文件来记录依赖树信息,进行依赖锁定,从而唯一确定 node_modules 的结构,这样处理可以保证团队成员使用同一份node_modules依赖结构。但是,我们前文提到的平铺式的算法的复杂性、幻影依赖和分身问题仍然没有解决。

pnpm 简介

前文我们大致梳理了 npm 的发展和遗留问题。而 pnpm 比较巧妙地解决了它们,并且极大地提升了依赖包管理的效率。

pnpmperformant npm(高性能的 npm),如 pnpm 官网[10]所言,它是快速的,节省磁盘空间的包管理工具,同时,它也较好地支持了 workspacemonorepos

举例来说,如果项目中,你使用了某个依赖项的多个版本,那么 pnpm 只会将有差异的文件添加到仓库。如果某个依赖包有 100 个文件,而它的新版本只改变了其中 1 个文件。那么 pnpm update 时只会添加 1 个新文件,而不会复制整个新版本的所有包。此外。所有文件都会存储在硬盘上的某一位置。当依赖包被被安装时,其中的文件会硬链接到这一位置,而不会占用额外的磁盘空间。同时,项目中允许共享同一版本的依赖。接下来我们先了解下 pnpm 的使用效果

pnpm 效果

与 npm、yarn、yarn pnp 工具链效果对比,来自 pnpm benchmarks[11]

action

cache

lockfile

node_modules

npm

pnpm

Yarn

Yarn PnP

install




1m 9.5s

15.3s

16.6s

23.6s

install

2.4s

1.3s

2.3s

n/a

install


14.8s

4s

6.8s

1.5s

install



21.8s

8.9s

11.2s

6.2s

install



35.4s

13.4s

12s

17.9s

install


3.1s

1.9s

7s

n/a

install


2.4s

1.3s

7.6s

n/a

install



3s

6.1s

11.8s

n/a

update

n/a

n/a

n/a

2.3s

11.8s

15.5s

28.3s

从上表数据我们可以看出,pnpm 的各项性能均比其它包管理工具有优势,那你可能会想,为什么 pnpm 有如此优越的表现,接下来我们聊聊 pnpm 的主要原理

pnpm 的原理

pnpm 主要有两个不同与其包管理工具的特性:

基于硬链接的 node_modules

pnpm 创建从全局存储到项目中 node_modules 文件夹的硬链接[12],而硬链接指向磁盘上原始文件所在的同一位置,具体来说就是 node_modules 中每个包的每个文件都是来自内容可寻址存储[13]的硬链接,简言之,就是特定版本和名称的包全局只有一份。举例来看:

node_modules
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       └── bar -> <store>/bar
    │           ├── index.js
    │           └── package.json
    └── foo@1.0.0
        └── node_modules
            └── foo -> <store>/foo
                ├── index.js
                └── package.json

node_modules 下面的唯一文件夹叫做 .pnpm, .pnpm 下面是一个 <PACKAGE_NAME@VERSION>文件夹,而在其下面 <PACKAGE_NAME> 的文件夹是一个基于内容可寻址存储的硬链接。同时,我们也可以通过 pnpm root 命令来打印当前项目中存放模块(modules)的有效目录

基于依赖解析的软链接 symlinks

观察以下依赖包结构

node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       └── bar -> <store>/bar
    └── foo@1.0.0
        └── node_modules
            ├── foo -> <store>/foo
            └── bar -> ../../bar@1.0.0/node_modules/bar

我们可以看到在 foo@1.0.0/node_modules/bar 内引用了 bar 的软链接 ../../bar@1.0.0/node_modules/bar,而在项目里引用 foo 的软链接 ./.pnpm/foo@1.0.0/node_modules/foo,如果项目内新增一个依赖包 qar@2.0.0,则其引用结构如下:

node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       ├── bar -> <store>/bar
    │       └── qar -> ../../qar@2.0.0/node_modules/qar
    ├── foo@1.0.0
    │   └── node_modules
    │       ├── foo -> <store>/foo
    │       ├── bar -> ../../bar@1.0.0/node_modules/bar
    │       └── qar -> ../../qar@2.0.0/node_modules/qar
    └── qar@2.0.0
        └── node_modules
            └── qar -> <store>/qar

根据前文我们介绍到的`Nodejs`的依赖解析规则[14]foo@1.0.0/node_modules/foo/index.js 中所需的依赖包 bar,实际上使用的是bar@1.0.0/node_modules/bar中的内容,因此,只有真正在依赖项中的包才能被访问到。而对于不同的 peer dependencies 的依赖解析原理,可以参考这里 How peers are resolved[15]

通过基于硬链接的node_modules和基于依赖解析的软链接原理,我们了解到,当我们在相同操作系统下第二次安装同一个依赖包时,我们需要做的仅仅是创建一个该依赖包对应的硬链接,对于同一个依赖包的不同版本,也只有不同的部分会被重新保存起来,而具体有没有 pnpm 是在哪里判断的呢?全局的 pnpm 索引文件在 ~/.pnpm-store/v3/files。基于此,使用硬链接让依赖包的安装速度非常快,同时也去除了冗余,节省较大磁盘空间。

symlinks 符号连接[16]

pnpm 使用

pnpm 的具体使用这里我们不展开介绍了,可以查看官网使用方法[17]CLI 命令[18]即可。这里只提几个有意思的点

CI 集成

GitHub Actions 上,你可以像这样使用 pnpm 安装和缓存依赖项,配置文件目录: .github/workflows/NAME.yml

name: pnpm Example Workflow
on:
  push:
jobs:
  build:
    runs-on: ubuntu-20.04
    strategy:
      matrix:
        node-version: [15]
    steps:
    - uses: actions/checkout@v2
    - uses: pnpm/action-setup@v2.0.1
      with:
        version: 6.20.3
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v2
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'pnpm'
    - name: Install dependencies
      run: pnpm install

pnpm 除了在开发体验方面的优越表现,在项目集成方面也毫不逊色,对于较大型项目从 npm 或 yarnpnpm迁移过程后,也得到了极大的优化,结果如下:


Without cache

With cache

yarn 2 (without dedupe)

6min 31s

1min 11s

yarn 3 (without dedupe)

4min 50s

57s

yarn 3

4min 1s

50s

yarn 3 (optimized)

1min 10

45s

pnpm

58s

24s

通过以上数据,我们可以 pnpm 在 CI 应用中的良好表现。

具体可以参考这篇最佳实践 A story of how we migrated to pnpm[19]

pnpm 前置

项目中使用 pnpm 时,如果你不希望项目内其他人使用 npm iyarn这类包管理器,可以在 package.json 配置文件中添加预安装 preinstall 配置项,从而规范使用统一的包管理器。

{
    "scripts": {
        "preinstall": "npx only-allow pnpm"
    }
}

管理 NodeJS 版本

在以前,如果你同时支撑了多个项目,而且需要在其中切换,你可能需要切换不同的 NodeJS 版本,也许你会用到像 nvmVolta[20] 这样的 NodeJS 版本管理器,而 pnpmv6.12.0 版本后支持了 pnpm env[21] 命令,你可以使用它来安装并指定使用哪个版本的 NodeJS ,是不是方便了很多。

monorepo 支持

因为pnpmmonorepos 的大力支持,像 VueVite 这些开源项目也转而使用了它。使用pnpm run 结合 --filter--recursive--parallel 选项,可以指定特定包,并高速执行相关命令。这样做的好处是之前要另外安装 lerna 这种 monorepo 管理工具的场景,现在 pnpm 可以包揽了。详细文章可以参考这里 pnpm vs Lerna: filtering in a multi-package repository[22]

总结

本文从 pnpm 的出现背景开始,简要介绍了 npm 的发展过程及存在的问题,继而对 pnpm 及其效果进行了简介,重点讲述了 pnpm 的实现原理,并从应用侧选择了四个点展开。

pnpm 作为新一代包管理器,自身有不少优越的表现,它通过硬链接和软链接的方式,解决了 npm幻影依赖和分身问题,并且较好地解决了依赖包复用问题,从而实现了依赖包高效快速的安装。需要特别注意的是 pnpm 严格遵循了 Nodejs 依赖解析规则,规避了之前任意依赖包的访问修改问题。

当然,pnpm 使用过程中也存在一些问题,包括 Vue 官方在迁移过程中,也处理过部分问题。另外,一些包也存在兼容性问题,由于包自己实现了模块解析,并没有遵循相关规范。但 pnpm 也提供了相关解决方法。具体参考 pnpm FAQ[23]

综上,pnpm 是一个功能全面,性能优越的包管理器,快来体验 pnpm 吧。更多文章,欢迎扫码关注微信公众号 【青梅主码

参考资料

[1]

历史: https://github.com/npm/cli/blob/latest/changelogs/CHANGELOG-1.md

[2]

Too many dependencies break the Windows file system: https://github.com/npm/npm/issues/3697

[3]

Maximum Path Length Limitation: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd

[4]

Why does the 260 character path length limit exist in Windows?: https://stackoverflow.com/questions/1880321/why-does-the-260-character-path-length-limit-exist-in-windows

[5]

依赖扁平化讨论和处理: https://github.com/npm/cli/blob/latest/changelogs/CHANGELOG-3.md

[6]

Nodejs的依赖解析规则: https://nodejs.org/api/modules.html#all-together

[7]

phantom_deps(幻影依赖): https://rushjs.io/pages/advanced/phantom_deps/

[8]

npm doppelgangers(npm 分身): https://rushjs.io/pages/advanced/npm_doppelgangers/

[9]

pnpm 官网: https://pnpm.io/

[10]

pnpm benchmarks: https://pnpm.io/zh/benchmarks

[11]

硬链接: https://zh.wikipedia.org/wiki/%E7%A1%AC%E9%93%BE%E6%8E%A5

[12]

内容可寻址存储: https://en.wikipedia.org/wiki/Content-addressable_storage

[13]

How peers are resolved: https://pnpm.io/zh/how-peers-are-resolved

[14]

symlinks 符号连接: https://zh.wikipedia.org/wiki/%E7%AC%A6%E5%8F%B7%E9%93%BE%E6%8E%A5

[15]

使用方法: https://pnpm.io/zh/pnpm-cli

[16]

CLI 命令: https://pnpm.io/zh/cli/add

[17]

A story of how we migrated to pnpm: https://divriots.com/blog/switching-to-pnpm

[18]

Volta: https://volta.sh/

[19]

pnpm env: https://pnpm.io/zh/cli/env

[20]

pnpm vs Lerna: filtering in a multi-package repository: https://medium.com/pnpm/pnpm-vs-lerna-filtering-in-a-multi-package-repository-1f68bc644d6a

[23]

pnpm FAQ: https://pnpm.io/faq#pnpm-does-not-work-with-your-project-here

相关推荐

Docker 47 个常见故障的原因和解决方法

【作者】曹如熙,具有超过十年的互联网运维及五年以上团队管理经验,多年容器云的运维,尤其在Docker和kubernetes领域非常精通。Docker是一种相对使用较简单的容器,我们可以通过以下几种方式...

电脑30个快问快答,解决常见电脑问题

1.强行关机/停电对电脑有影响吗?答:可能损坏硬盘(机械硬盘风险高)、未保存数据丢失,偶尔一次影响小,但频繁操作会缩短硬件寿命。2.C盘满影响速度吗?答:会!系统运行需C盘空间缓存临时数据,空间不...

使用Tcpdump包抓取分析数据包的详细用法

TcpDump可以将网络中传送的数据包的“头”完全截获下来提供分析。它支持针对网络层、协议、主机、网络或端口的过滤,并提供and、or、not等逻辑语句来帮助你去掉无用的信息。tcpdump就是一种...

电脑启动不了(BootDevice Not Found Hard Disk-3F0)解决方案

HP品牌机,开机启动不了,黑屏,开机取下主板电池恢复BIOS后,开机显示找不到启动盘。一、按F2键进入BIOS,出现硬盘内存检测界面的话,直接退出。就会出现这个界面,光标键向下,选择BIOSSetu...

电脑开机黑屏别慌!快码住!起底维修老师傅不能说的秘密

按下开机键却只收获黑屏大礼包?那些神秘的英文提示、刺耳的蜂鸣声,其实是电脑在给你发送求救信号!从按下电源到进入桌面的12秒里,你的电脑经历了史诗级的硬件自检与系统加载,今天我们就破译这段“摩斯电码”。...

电脑启动故障为何总要先看BIOS?新手必读的关键知识解析

最近在帮朋友们解答电脑无法正常开机的问题时,发现大家经常收到一句高频建议:“先检查BIOS”。对不少普通用户而言,BIOS依然是个神秘的存在。那么,BIOS到底是什么?电脑出现哪些故障会与它相关呢?本...

Windows 11 KB5053598更新:安全补丁还是系统噩梦?

2025年3月11日,微软发布了Windows1124H2的强制性更新KB5053598,作为“周二补丁日”(PatchTuesday)的一部分。然而,这款本应提升系统安全性的更新却引发了广泛的...

飞牛OS入门安装遇到问题,如何解决?

之前小编尝试了用旧电脑装飞牛OS安装之前特意查了一些硬件要求飞牛OS目前支持主流的x86架构硬件主机需能连网线飞牛OS暂时不支持只有无线网卡的安装貌似很多小伙伴在一开始安装就卡住了那今天咱们汇总分...

几种常见的电脑开机黑屏显示白色英文字母解决方法

当电脑开机出现黑屏并显示白色英文字母时,通常表示系统启动过程中遇到了错误。以下是几种常见原因及对应的解决方法,按照排查顺序整理:一、检查外接设备与硬件连接可能原因:外接U盘、移动硬盘等未拔出,或内部硬...

电脑启动出现问题,为什么都要先检查BIOS?

【ZOL中关村在线原创技巧应用】最近在回答问题的时候,总会发现很多朋友都在问“电脑无法正常开机怎么办?”这样类似的问题,而许多DIY大佬的回复总会出现一条高频建议“先检查BIOS”。但对于许多普通用户...

教你怎么用JavaScript检测当前浏览器是无头浏览器

什么是无头浏览器(headlessbrowser)?无头浏览器是指可以在图形界面情况下运行的浏览器。我可以通过编程来控制无头浏览器自动执行各种任务,比如做测试,给网页截屏等。为什么叫“无头”浏览器?...

12个高效的Python爬虫框架,你用过几个?

实现爬虫技术的编程环境有很多种,Java、Python、C++等都可以用来爬虫。但很多人选择Python来写爬虫,为什么呢?因为Python确实很适合做爬虫,丰富的第三方库十分强大,简单几行代码便可实...

运维的报表之路,用 node.js 轻松发送 grafana 报表

在运维过程中,无论是监控还是报表,都会有一些通过邮件发送图表的需求,由于开源的zabbix,grafana和kibana等并不完全具有“想发送哪儿就发送哪儿”的图片生成功能,在grafana...

C#基于浏览器内核的高级爬虫(c#爬取网页内容)

基于C#.NET+PhantomJS+Sellenium的高级网络爬虫程序。可执行Javascript代码、触发各类事件、操纵页面Dom结构、甚至可以移除不喜欢的CSS样式。很多网站都用Ajax动态加...

如何优化一个秒杀项目?(秒杀实现思路)

问题1:使用jmeter性能压测,定位瓶颈代码步骤流程:线程组--->Http请求--->查看结果树--->聚合报告tips:host的文件--->优先调用映射,减少DNS的时...