2022-01-10 不那么流行的包管理器 - pnpm

在现代前端开发过程中,包管理器是必不可少的一环,它能帮助我们管理node_modules,安装对应依赖,提高了前端开发的效率,同时也让大型的前端工程成为可能。说到包管理工具,我相信大家应该都不陌生,除了与 node.js 自带的 npm,还有 yarn也是大家平时开发时经常用到的工具,相对来说,本文今天要介绍的主角pnpm就显得较为小众。那么,在npmyarn已经如此成熟的情况下,pnpm 这种较新并且用户量较小的包管理器的意义何在呢?顺着本文让我们来一起来寻找问题的答案吧

Why Pnpm ?

我相信这是大家看到 pnpm 后的第一反应,在 npmyarn如此成熟的今天,切换工具链势必会带来迁移成本,pnpm 作为一个后起之秀,必然需要有相较于其它同类工具非常显著的优势,才能让大家有更换它的动力,而当我们打开pnpm的官网,我们会发现作者非常直接的把pnpm的优势在官网上列了出来

image

可以看到 pnpm 相对于其它包管理工具有以下几个显著的优势

  • 超快的安装速度
  • 更高效的利用磁盘空间
  • 内置的 monorepo
  • 避免 phantom dependencies (在代码中可以引用间接安装的包,eg. 项目里安装了A依赖,A依赖了B,项目里可以直接引用B)

如果说更高效的磁盘空间利用率和内置的monorepo不是大多数人在意的点,那么超快的安装速度以及对phantom dependencies的天然隔离则是我们开发过程中每天都会碰到的问题,接下来让我们探索一下pnpm的这些特性是怎么实现的以及他们对前端开发来说存在的意义

超快的安装速度

我相信几乎每个人都碰到过在大型前端项目中(尤其是monorepo)yarn 或者 npm 安装依赖太过耗时的问题,而根据 pnpm 官方给出的数据图表,在大多数场景下,pnpm的安装速度都领先于 yarn 和 npm

image
  • 怎么做到的 ?
    • 要回答这个问题,让我们先看看官方doc中提到的pnpm的开发动机

When using npm or Yarn, if you have 100 projects using a dependency, you will have 100 copies of that dependency saved on disk. With pnpm, the dependency will be stored in a content-addressable store, so:

If you depend on different versions of the dependency, only the files that differ are added to the store. For instance, if it has 100 files, and a new version has a change in only one of those files, pnpm update will only add 1 new file to the store, instead of cloning the entire dependency just for the singular change.

All the files are saved in a single place on the disk. When packages are installed, their files are hard-linked from that single place, consuming no additional disk space. This allows you to share dependencies of the same version across projects.

As a result, you save a lot of space on your disk proportional to the number of projects and dependencies, and you have a lot faster installations!

可以看到,pnpm 采用了和 npm 还有 yarn完全不同的思路,对于 npm 还有 yarn 来说,不同项目的node_modules是没法复用的,也就是说如果我有两个项目,哪怕这两个项目安装的很多包都是同一个版本,也还是需要安装多份,这无疑占用了更多的磁盘空间,同时也会带来更长的下载时间,而pnpm对于这个问题采取的解决方案是在磁盘的一块连续地址上存储所有下载的npm包,并且通过硬链接的方式将npm包链接到各个项目的node_modules中,这样一来,所有的项目都可以共享同一npm包,不再需要额外的下载包的时间,同时,pnpm 还对npm包进行了diff,只会将包中变更的文件加到本地存储中,举个例子,两个项目同时引用了一个包的A和B版本,A和B版本有99个文件是相同的,只有一个文件是不同的,那么pnpm会复用这99个文件,通过硬链接的方式把新版包中不变的文件指向原来已经存储过的文件,以此避免存储重复的文件。通过这种方式,pnpm能在不同项目中共享npm包,在节省磁盘空间的同时也提高了包的安装速度

硬链接(Hard Link):硬链接是文件系统中对文件物理物理索引的引用,当移动或删除原始文件时,硬链接不会被破坏,因为它所引用的是文件的物理数据而不是文件在文件结构中的位置,如果删除的文件有相应的硬链接,那么这个文件依然会保留,直到所有对它的引用都被删除,对硬链接的操作其实本质上就是对已有文件进行操作

避免 phantom dependencies

  • 什么是 phantom dependcies?

首先我们先来了解下什么是phantom dependcies,举个例子,我们想在我们的项目里引用 A 包,同时 B 又依赖了 A 包,当我们使用npm或者yarn安装完A之后,node_modules的结构会是啥呢?

node_modules
    - A
        -index.js
        -package.json
    - B
        -index.js
        -package.json

相信大家对这个结构也不陌生,这也就是我们经常遇到的我只是在package.json里添加了几个包,装完之后node_modules多了一堆包,因为npm / yarn的安装会把包里面依赖的包也给安装到node_modules,也就是安装之后node_modules其实是一个拍平的结构,避免出现依赖嵌套的结构,这样带来了不少好处,比如node_modules层级不会太深,同时因为node_modules的包查找机制,在某个包内的node_modules里没有找到依赖包,会从上一级的node_modules中查找,如果有相同版本的包则不会重复安装,这样就解决了大量重复的包的安装问题,但同时flat的结构带来了一个非常明显的安全性问题,那就是可以在项目内非法访问依赖的包。可以看到,在这个例子里,其实我们只添加了A的依赖,但是引用B这个包也能正常使用,不会报错,一旦某天A的新版本升级了依赖B,有了break change,或者不再使用B这个包,那我们的项目就会直接报错了

  • Pnpm 怎么解决phantom dependcies的问题

与 npm / yarn 不同,pnpm重新设计了一套node_modules的结构,以此从根本上解决了phantom dependcies的问题,让我们来看下pnpm安装后node_modules的结构

首先,我们运行一下pnpm add react,然后看下node_modules下的结构

node_modules
│   ├── .pnpm
│   └── react -> .pnpm/react@17.0.2/node_modules/react

可以看到node_modules下非常的整洁,只有一个react的包,我们都知道react引用了非常多的包,而这里node_modules下的包和我们在package.json下声明的依赖一模一样,不会有任何依赖被安装到node_modules下,这样就没法在项目里非法访问依赖,保证了安全性,这里你肯定会好奇,那pnpm把这些依赖装到哪去了呢?别急,我们接着往下看

仔细观察一下,会发现这里react其实是一个软链接,链接到了.pnpm文件夹下的react,让我们打开.pnpm文件夹看看

软链接(符号链接):一个指向文件的快捷方式,在访问软链接时,系统会把路径替换问软链接指向的文件

硬链接和软链接的区别:

  • 硬链接是同一个文件的不同名称,可以理解为从不同的角度看同一个事物,所有对硬链接的操作都是对文件本身的操作,删除一个硬链接就像限制你的视角,文件本身并不会消失,当所有的硬链接都被删除,则文件本身也被删除了
  • 软链接只是一个快捷方式,删除所有的软链接也不会对文件有任何影响,对软链接的所有操作会被文件系统替换为对软链接指向的文件的操作,如果原始文件的位置被移动了,那么软链接就找不到对应的文件了,改动自然也无法同步到文件上,与硬链接不同,因为软链接只是个快捷方式,所以软链接甚至可以指向不存在的文件

总结来说

硬链接就是文件本身,一个不同的名字(引用)

软链接本质上是另外一个文件,只不过在访问时文件系统会替换软链接为指向文件

.pnpm
│   │   ├── js-tokens@4.0.0
│   │   │   └── node_modules
│   │   │       └── js-tokens
│   │   │           ├── CHANGELOG.md
│   │   │           ├── LICENSE
│   │   │           ├── README.md
│   │   │           ├── index.js
│   │   │           └── package.json
│   │   ├── lock.yaml
│   │   ├── loose-envify@1.4.0
│   │   │   └── node_modules
│   │   │       ├── js-tokens -> ../../js-tokens@4.0.0/node_modules/js-tokens
│   │   │       └── loose-envify
│   │   │           ├── LICENSE
│   │   │           ├── README.md
│   │   │           ├── cli.js
│   │   │           ├── custom.js
│   │   │           ├── index.js
│   │   │           ├── loose-envify.js
│   │   │           ├── node_modules
│   │   │           │   └── .bin
│   │   │           │       └── loose-envify
│   │   │           ├── package.json
│   │   │           └── replace.js
│   │   ├── node_modules
│   │   │   ├── .bin
│   │   │   │   └── loose-envify
│   │   │   ├── js-tokens -> ../js-tokens@4.0.0/node_modules/js-tokens
│   │   │   ├── loose-envify -> ../loose-envify@1.4.0/node_modules/loose-envify
│   │   │   └── object-assign -> ../object-assign@4.1.1/node_modules/object-assign
│   │   ├── object-assign@4.1.1
│   │   │   └── node_modules
│   │   │       └── object-assign
│   │   │           ├── index.js
│   │   │           ├── license
│   │   │           ├── package.json
│   │   │           └── readme.md
│   │   └── react@17.0.2
│   │       └── node_modules
│   │           ├── loose-envify -> ../../loose-envify@1.4.0/node_modules/loose-envify
│   │           ├── object-assign -> ../../object-assign@4.1.1/node_modules/object-assign
│   │           └── react
│   │               ├── LICENSE
│   │               ├── README.md
│   │               ├── build-info.json
│   │               ├── cjs
│   │               │   ├── react-jsx-dev-runtime.development.js
│   │               │   ├── react-jsx-dev-runtime.production.min.js
│   │               │   ├── react-jsx-dev-runtime.profiling.min.js
│   │               │   ├── react-jsx-runtime.development.js
│   │               │   ├── react-jsx-runtime.production.min.js
│   │               │   ├── react-jsx-runtime.profiling.min.js
│   │               │   ├── react.development.js
│   │               │   └── react.production.min.js
│   │               ├── index.js
│   │               ├── jsx-dev-runtime.js
│   │               ├── jsx-runtime.js
│   │               ├── node_modules
│   │               │   └── .bin
│   │               │       └── loose-envify
│   │               ├── package.json
│   │               └── umd
│   │                   ├── react.development.js
│   │                   ├── react.production.min.js
│   │                   └── react.profiling.min.js
│   └── react -> .pnpm/react@17.0.2/node_modules/react

点开根目录下react软链接到.pnpm文件夹下的react@17.0.2,会发现react本身以及react所有的依赖项都被安装到了react的node_modules下,而react的依赖项又是软链接到最外层安装的包下,这时候我们再点开pnpm-lock文件就会发现,.pnpm目录下文件的结构和pnpm-lock下的依赖顺序和结构完全一致,这样一来包之间的依赖就十分清晰,同时还避免了非法引用依赖的问题

自带 monorepo 支持

随着前端项目规模越来越大,相互之间的依赖原来越多,越来越多的大型前端项目采用monorepo的方式组织项目(即把多个项目放在一个git仓库下管理,共享的代码可以弄成包在不同项目中引用),目前比较常用的是方式是使用lerna + yarn的组合,但lerna + yarn的组合带来的一个问题就是上手成本较高,相比之下,pnpm自带workspace,使用者很快就能享受到monorepo带来的便利之处,要使用pnpm的workspace功能,只需要一步

在根目录下新建一个pnpm-workspace.yaml文件,标识这是一个workspace,并填入workspace路径

packages:
  # 所有在 packages/子目录下的 package
  - 'packages/**'
  # 不包括在 test 文件夹下的 package
  - '!**/test/**'

是的,你没有看错,这样我们就配置好了,接下来让我们在根目录下建几个文件夹,感受一个monorepo带来的便利

├── packages
│   ├── a
│   ├── b

在a和b的package.json中,我们分别把name设置成@test/a和@test/b

让我们给每个packages都装上react试试

pnpm install react react-dom -w

image

可以看到,react和react-dom被安装在了workspace根目录下,所有packages都可以共享这个依赖,除此之外,pnpm也支持针对某一特定的package安装依赖,可以使用以下命令只在a这个package下安装react

pnpm i react -r --filter @test/a

接下来让我们来看看pnpm是怎么处理本地包之间的依赖的,我们在packages/a这个文件夹下建立一个index.js文件,并且导出一个函数,之后我们运行一下以下命令

pnpm i @test/a -r --filter @test/b

image
image

运行完成之后,可以看到packages/b下多了一个node_modules,而这里已经有了本地的@test/a包,这里pnpm就已经帮我们把包链接起来了,我们可以在不同的包之间共享本地代码,就像从npm上安装的包一样,同时,当我们访问b的package.json的时候就可以看到@test/a包指向workspace。当然,如果这里你想直接把packages下的包安装到顶层让所有packages都能访问到,使用 pnpm intsall @test/a -w即可,怎么样,是不是很简单呢?

总结

本文粗略的对pnpm的核心思想还有优势做了简单的介绍,当然在软件开发中,是没有银弹存在的,尽管有着以上的各种优势,pnpm相比于npm/yarn也有着一个最大的劣势,那就是迁移成本,对于存量大型项目来说,迁移pnpm无法保证和npm/yarn的行为完全一致,这其中可能还要踩不少坑,对于新项目而言,pnpm无疑是值得一试的选择,能大幅提升开发体验.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容