前言

分享 pnpm 设计的初衷,帮助大家了解到 pnpm 是如何解决 npm/yarn 的设计缺陷的,以及 pnpm 是如何改进的。

依赖管理的始末

npm2

使用早期的 npm1/2 安装依赖,node_modules 文件夹会以递归的形式呈现,严格按照 package.json 结构以及次级依赖的 package.json 结构将依赖安装到它们各自的 node_modules 中,直到次级依赖不再依赖其它模块。

就像下面这样,tea-app 依赖 tea-component 作为次级依赖,tea-component 会安装到 tea-component 的 node_modules 里面:

node_modules
└─ tea-app
   ├─ index.js
   ├─ package.json
   └─ node_modules
      └─ tea-component
         ├─ index.js
         └─ package.json

假设项目的中的两个依赖同时依赖了相同的次级依赖,那么它们二者的次级依赖将会被重复安装:

node_modules
├─ tea-app
│  ├─ index.js
│  ├─ package.json
│  └─ node_modules
│      └─ tea-component
│          ├─ index.js
│          └─ package.json
└─ tea-chart
   ├─ index.js
   ├─ package.json
   └─ node_modules
       └─ tea-component
           ├─ index.js
           └─ package.json

这只是简单的例子,那如果 tea-component 还依赖别的包,别的包又依赖另外的包...... 在真实的开发场景中其问题还会更加恶劣:

  1. 依赖层级太深,会导致文件路径过长
  2. 重复的包被安装,导致 node_modules 文件体积巨大,占用过多的磁盘空间

npm3

npm3/yarn 开始,相比 npm1/2 项目依赖管理的方式有了很大的改变,不再是以往的“嵌套式”而是采用了“扁平化”方式去管理项目依赖。

这里继续拿上面的例子,tea-app 和 tea-chart 都依赖了 tea-component,依赖安装后呈现的是下面的这种扁平化目录:

node_modules
├─ tea-component
│  ├─ index.js
│  └─ package.json
├─ tea-app
│  ├─ index.js
│  └─ package.json
└─ tea-chart
   ├─ index.js
   └─ package.json

扁平化的目录的确解决了上一小节暴露的一些问题,同时也暴露了新的问题:

  • Phantom dependencies

称为“幽灵依赖”,指的是在项目内引用未在 package.json 中定义的包。这个问题在 npm3 展现,因为早期的树形结构导致了依赖冗余和路径过深的问题,npm3 之后采用扁平化的结构,一些第三方包的次级依赖提升到了与第三方包同级。

一旦出现幽灵依赖的问题,可能会导致意想不到的错误,所以一定要正视:

  1. 不兼容的版本(例如某一个 api 进行了重大更新)
  2. 有可能会丢失依赖(某依赖不再依赖呈现在我们项目中的幽灵依赖)

    // tea-component 就属于是幽灵依赖,因为它是属于 tea-app、tea-chart 的次级依赖。
    import { Button } from 'tea-component';
  3. NPM doppelgangers

称为“分身依赖”,在 monorepo 项目中非常常见,项目中依赖的第三方包以及第三方包所依赖的同名包都会被重复安装。

在实际开发中也会出现这样的情景,假设 tea-app、tea-form 依赖 tea-component@2.0.0,tea-chart 依赖 tea-component@3.0.0,这时候会造成依赖冲突,解决冲突的方式会将对应的冲突包放到对应依赖目录的 node_mudules 中,类似下面结构:

node_modules
├─ tea-component@3.0.0
│  ├─ index.js
│  └─ package.json
├─ tea-app
│  ├─ index.js
│  ├─ package.json
│  └─ node_modules
│      └─ tea-component@2.0.0
│          ├─ index.js
│          └─ package.json
├─ tea-form
│  ├─ index.js
│  ├─ package.json
│  └─ node_modules
│      └─ tea-component@2.0.0
│          ├─ index.js
│          └─ package.json
└─ tea-chart
   ├─ index.js
   └─ package.json

这时候会发现一个问题,tea-app、tea-form 的 node_modules 下都有重复且版本相同的 tea-component@2.0.0,这个问题就是我们正在所说的“分身依赖”的问题。这个问题就会导致 tea-app 中的 ConfigProvider 组件和 tea-form 的不是一个实例,无法生效。

常见的问题:

  1. 项目打包会将这些“重身”的依赖都进行打包,增加产物体积
  2. 无法共享库实例,引用的得到的是两个独立的实例
  3. 重复 TypeScript 类型,可能会造成类型冲突

结论

  1. 扁平化的 node_modules 结构允许访问没有在 package.json 中声明的依赖。
  2. 安装效率低,大量依赖被重复安装,磁盘空间占用高。
  3. 多个项目之间已经安装过的的包不能共享,每次都是重新安装。

主角登场

image.png

Who Is It ?

Fast, disk space efficient package manager (速度快、节省磁盘空间的软件包管理器)

image.png

当使用 npm 或 Yarn 时,如果你有 100 个项目,并且所有项目都有一个相同的依赖包,那么, 你在硬盘上就需要保存 100 份该相同依赖包的副本。然而,如果是使用 pnpm,依赖包将被 存放在一个统一的位置,因此:

  1. 如果你对同一依赖包需要使用不同的版本,则仅有 版本之间不同的文件会被存储起来。例如,如果某个依赖包包含 100 个文件,其发布了一个新 版本,并且新版本中只有一个文件有修改,则pnpm update只需要添加一个 新文件到存储中,而不会因为一个文件的修改而保存依赖包的 所有文件。
  2. 所有文件都保存在硬盘上的统一的位置。当安装软件包时, 其包含的所有文件都会硬链接自此位置,而不会占用 额外的硬盘空间。这让你可以在项目之间方便地共享相同版本的 依赖包。

最终结果就是以项目和依赖包的比例来看,你节省了大量的硬盘空间, 并且安装速度也大大提高了!

Who's Using This?

image.png

性能基准

image.png

结合多个使用场景与官方产出的性能基准报告可以看到 pnpm 的效率要远远高于 npm/yarn。

依赖安装

使用 pnpm 安装,pnpm 会将依赖存储在位于 .pnpm-store 目录下。只要你在同一机器下,下次安装依赖的时候 pnpm 会先检查 store 目录,如果有你需要的依赖则会通过一个硬链接丢到到你的项目中去,而不是重新安装依赖。

pnpm 在输出易懂方面也略胜一筹,可以看到你复用了多少包和需要重新下载了多少包。

依赖管理原理

pnpm 会将依赖存储在 store 目录下,通过符号链接的方式仅将项目的直接依赖项添加到 node_modules 的根目录下。

当使用 npm 或 yarn 安装依赖包时,所有软件包都将被提升到 node_modules 的 根目录下。其结果是,源码可以访问 本不属于当前项目所设定的依赖包。

image.png

Linux 软硬链接

  • inode

每一个文件都有一个唯一的 inode,它包含文件的元信息,在访问文件时,对应的元信息会被 copy 到内存去实现文件的访问。

可以通过 stat 命令去查看某个文件的元信息。

硬链接

ln main.js hardlink.js
  1. 具有相同inode节点号的多个文件互为硬链接文件;
  2. 删除硬链接文件或者删除源文件任意之一,文件实体并未被删除;
  3. 只有删除了源文件和所有对应的硬链接文件,文件实体才会被删除;
  4. 硬链接文件是文件的另一个入口;
  5. 可以通过给文件设置硬链接文件来防止重要文件被误删;

软链接

ln -s main.js softlink.js
  1. 软链接类似windows系统的快捷方式;
  2. 软链接里面存放的是源文件的路径,指向源文件;
  3. 删除源文件,软链接依然存在,但无法访问源文件内容;
  4. 软链接失效时一般是白字红底闪烁;
  5. 软链接和源文件是不同的文件,文件类型也不同,inode号也不同;

image.png

PNPM 机制

如果 store 目录里面拥有即将需要下载的依赖,下载将会跳过,会向对应项目 node_modules 中去建立硬链接,并非去重新安装它。这里就表明为什么 pnpm 性能这么突出了,最大程度节省了时间消耗和磁盘空间。

基于软链接的 node_modules

pnpm 输出的 node_modules 与 npm/yarn 有很大的出入,并非是先者那样的“扁平化目录”而是“非扁平化目录”。

创建两个目录并分别运行 npm add express,pnpm add express。

这是使用 npm 安装 node_modules 的结构:

.bin
accepts
array-flatten
body-parser
bytes
content-disposition
cookie-signature
cookie
debug
depd
destroy
ee-first
encodeurl
escape-html
etag
express

这个则是 pnpm 安装 node_modules 的结构:

.pnpm
.modules.yaml
express

打开 .pnpm 目录会发现这些依赖都被“扁平化”了,每个包都携带着自己的版本号。pnpm 这样设计的目的我理解其实是为了解决“分身依赖”的问题。

假设我们有这么一个情景,项目中依赖了 tea-app@1.0.0、tea-chart@1.0.0 和 tea-component@2.0.0。tea-chart 和 tea-app 依赖了 tea-component@1.0.0 那它引用关系是这样的:

node_modules
├─tea-app -> ./.pnpm/tea-app@1.0.0/node_modules/tea-app
├─tea-chart -> ./.pnpm/tea-chart@1.0.0/node_modules/tea-chart
├─tea-component -> ./.pnpm/tea-component@2.0.0/node_modules/tea-component
└─.pnpm
    ├─ tea-app@1.0.0
    │   └─ node_modules
    │       ├─ tea-component -> ../tea-component@1.0.0/node_modules/tea-component
    │       └─ tea-app -> <store>/tea-app
        ├─ tea-chart@1.0.0
    │   └─ node_modules
    │       ├─ tea-component -> ../tea-component@1.0.0/node_modules/tea-component
    │       └─ tea-chart -> <store>/tea-chart
        ├─ tea-component@1.0.0
    │   └─ node_modules
    │       └─ tea-component -> <store>/tea-component@1.0.0
    └─ tea-component@2.0.0
        └─ node_modules
            └─ tea-component -> <store>/tea-component@2.0.0

为什么需要通过软链接的方式去引用实际的依赖?

这样设计的目的是解决“幽灵依赖”的问题,只有声明过的依赖才会以软链接的形式出现在 node_modules 目录中。在实际项目中引用的是软链接,软链接指向的是 .pnpm 的真实依赖,所以在日常开发中不会引用到未在 package.json 声明的包。

PNPM 锁文件

pnpm 产出的是一个 pnpm-lock.yaml 格式的锁文件。

支持通过 pnpm import 从另一个包管理器的锁文件生成一个。支持的源文件:

  • package-lock.json
  • npm-shrinkwrap.json
  • yarn.lock

常用的命令

$ pnpm install express

$ pnpm update express

$ pnpm remove express

$ pnpm list

$ pnpm run <scripts>

$ pnpm publish

总结

npm2 是通过嵌套的方式管理 node_modules 的,会有同样的依赖复制多次的问题。

npm3+ 和 yarn 是通过铺平的扁平化的方式来管理 node_modules,解决了嵌套方式的部分问题,但是引入了幽灵依赖的问题,并且同名的包只会提升一个版本的,其余的版本依然会复制多次。

pnpm 则是用了另一种方式,不再是复制了,而是都从全局 store 硬连接到 node_modules/.pnpm,然后之间通过软链接来组织依赖关系。

这样不但节省磁盘空间,也没有幽灵依赖问题,安装速度还快,从机制上来说完胜 npm 和 yarn。

参考

pnpm vs npm

Flat node_modules is not the only way

Symlinked node_modules structure

Linux硬链接和软连接的区别与总结

标签: PNPM, 前端

添加新评论