PNPM原理介绍

393 阅读3分钟

Pnpm是什么

Fast, disk space efficient package manager

pnpm特性

pnpm依赖管理

在npm1, npm2中,node_modules中的依赖呈现嵌套结构:

node_modules
└─ foo
   ├─ index.js
   ├─ package.json
   └─ node_modules
      └─ bar
         ├─ index.js
         └─ package.json

这样的设计存在如下问题:

  1. 依赖层级太深,会导致文件路径过长的问题,尤其在 window 系统下。
  2. 大量重复的包被安装,文件体积超级大。比如跟 foo 同级目录下有一个baz,两者都依赖于同一个版本的lodash,那么 lodash 会分别在两者的 node_modules 中被安装,也就是重复安装。

从 npm3 开始,包括 yarn,都着手来通过扁平化依赖的方式来解决这个问题,解决了大量包重复安装的问题,而且依赖层级也不会太深。

node_modules
├─ foo
|  ├─ index.js
|  └─ package.json
└─ bar
   ├─ index.js
   └─ package.json

单扁平化依赖带来了新的问题:扁平化算法本身的复杂性很高,耗时较长;项目中仍然可以非法访问没有声明过依赖的包。

pnpm采用自己独特的机制解决了上述问题:

举个栗子

两个代码相同的项目,一个使用npm,一个使用pnpm,依赖均只有express,但是在代码中均使用iconv-lite(express的依赖),npm项目成功执行,pnpm失败。

 npm:   pnpm:    

\

npm使用扁平化的node_modules,存在安全问题,可以未经package.json声明而直接只用依赖;
pnpm使用了软链接的方式,真正的包安装在了.pnpm目录下,只会对项目中package.json中声明的依赖包,放置在node_modules目录下。

npm:      pnpm:  

进一步观察pnpm项目,node_modules中express是一个软链接,没有自己的node_modules,项目中真正的依赖包在.pnpm目录下面。

    Jinru

进入node_modules/.pnpm/express@4.18.1/node_modules目录进一步查看,只有express是真实目录,其他express的依赖又是软链接,真实的依赖包还是在.pnpm/name@x.x.x/node_modules/name目录下

\

.pnpm目录下呈现的是扁平的目录结构,但顺着软链接慢慢展开,其实依赖是嵌套的结构。

包本身依赖放在同一个node_module下面,与原生 Node 完全兼容,又能将 package 与相关的依赖很好地组织到一起,设计十分精妙。

上述机制能顺利运行的原因是Nodejs的node_modules目录加载机制,不断向上层目录的node_modules目录寻找包。

▾ node_modules
▾ .pnpm
▸ accepts@1.3.5
▸ array-flatten@1.1.1
...
▾ express@4.16.3
▾ node_modules
▸ accepts
▸ array-flatten
▸ body-parser
▸ content-disposition
...
▸ etag
▾ express
▸ lib
History.md
index.js
LICENSE
package.json
Readme.md

\

pnpm的依赖管理机解决了扁平化包带来的未声明依赖的安全问题。

Pnpm的高效存储

用 npm/yarn 的时候,每个项目的依赖都是单独安装,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。

但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,pnpm会在磁盘上新建一个中心化的pnpm store来保存所有的依赖, 后面在项目中再次使用都会直接使用 hardlink(硬链接),将项目中的包link到store,从而大大节省磁盘空间

Pnpm monorepo

pnpm需要通过 pnpm-workspace.yaml  文件定义工作空间,每个具体的包放在工作空间写入的目录下,且包名一般使用命名空间比如@mono/xxx

对于全局安装的依赖包,可使用 pnpm install typescript -D -W,局部package中使用的依赖包有两种方式添加:

  1. cd packges/xxx后,执行pnpm install xxx
  2. 不切换到package/xxx目录,直接在根目录下添加pnpm add xxx --filter @mono/package-a

项目内的包互相安装依赖:pnpm add @mono/utils --filter @mono/serve 

最终Monorepo项目的结构如下图,全局安装的依赖在root目录的node_modules下并软链接到.pnpm目录下;

局部包的依赖在自己的node_modules目录下,其中其他局部包软链接到项目其他包,网络下载的包软链接到.pnpm目录下。