pnpm包管理工具介绍

pnpm(perfomance npm),意为高性能的npm。介绍pnpm之前先了解一下npm的发展历程和优缺点~

一、npm发展历程


在没有npm工具之前,项目中需要下载依赖包、JS库时,开发者通过官网或者github去下载。当项目越来越大,依赖包随之也变多,管理和下载这些依赖包变得繁琐。

npm(Node Package Manager)包管理工具解决了这个问题,用npm把这些代码集中到一起来管理。

Node.js内置的包管理工具就是npm,随着 node.js 的火爆,大家都开始使用 npm 来共享JS代码,JS库的作者们将自己的东西发布到了npm的服务器上,我们可以使用 npm install 去下载需要的依赖包。

执行npm install后大致的过程是从远程仓库下载资源包,校验完整性,并添加到缓存,同时解压到 <span data-morpho-text="node_modules">node_modules</span> 目录中。

其中node_modules的内部结构也经历了几个版本的变化。

1.1 npm@3之前

npm最开始其实没有注重npm包的管理,只是简单的嵌套依赖,这种方式层级依赖结构简单且清晰;

在npm@3之前,依赖结构如下:

node_modules 
└─ 依赖A 
   ├─ index.js 
   ├─ package.json 
   └─ node_modules 
       └─ 依赖B 
       ├─ index.js 
       └─ package.json
 └─ 依赖C 
   ├─ index.js 
   ├─ package.json 
   └─ node_modules 
       └─ 依赖B 
       ├─ index.js 
       └─ package.json

随着npm包的增多,项目的迭代扩展,重复包越下载越多,造成了空间浪费,导致前端本地项目node_modules 动辄上几百M。

在 windows系统中,文件路径最大长度为260个字符,嵌套层级过深可能导致不可预知的问题。

1.2 npm@3之后

npm团队意识到了这个问题,通过扁平化的方式,将子依赖安装到了主依赖所在项目中,从而减少依赖嵌套太深的问题。

在npm@3之后,依赖结构如下:(A、C均依赖B)

node_modules 
└─ 依赖A  
    ├─ index.js 
    ├─ package.json 
    └─ node_modules 
└─ 依赖C
    ├─ index.js 
    ├─ package.json 
    └─ node_modules 
└─ 依赖B 
    ├─ index.js 
    ├─ package.json 
    └─ node_modules 

从上面的目录结构来看,扁平化方式在一定程度上解决了相同包重复安装的问题,和依赖层级太深的问题。

为什么说一定程度上呢?因为npm在构建依赖时,不管是直接依赖还是子依赖,都会按照扁平化的原则,优先将其放置在 node_modules 根目录中,在这个过程中,如果遇到相同的模块,会检查已放置在依赖树中的模块是否符合新模块的版本范围,如果符合,则跳过;如果不符合,则在当前模块的 node_modules 下放置新模块。

根据这种原则,如果A依赖[email protected]版本、C依赖[email protected]版本,node_modules目录结构如下:

但也会存在以下这种结构。哪个包被提升,取决于哪个包先被安装。

存在的问题:

1、如果上述的项目中依赖D,D又依赖B1.0版本,那么仍然会存在重复安装的问题;

2、node_modules结构不稳定;

3、把package B提升到了顶层,这样导致package.json没有声明过B,但是也可以在项目中引用到B(幽灵依赖问题

1.3 npm@5

npm@5借鉴yarn的思想,新增了 package-lock.json。

package-lock.json 部分字段说明:

Difference between package.json and package-lock.json | by Umar Farooque  Khan | Medium

  • name: 项目的名称;

  • version: 项目的版本;

  • lockfileVersion: lock文件的版本;

  • requires: 使用requires来跟踪模块的依赖关系;

  • dependencies: 项目的依赖;

    • version: 表示实际安装的版本;
    • resolved: 用来记录下载的地址,registry仓库中的位置;
    • requires: 记录当前模块的依赖;
    • integrity: 表明包完整性的hash值,可用来从缓存中获取索引,再通过索引去获取压缩包文件

可以看出package-lock.json里面记录了package.json依赖的模块、模块的子依赖,且给每个依赖标明了版本、获取地址和验证模块完整性哈希值。

package-lock.json能保证每次执行npm install后生成的node_modules目录结构一定是完全相同的,解决了上述的node_modules结构不稳定的问题。一般在项目应用中,将package-lock.json文件提到代码库中,可以保证团队开发依赖的一致性。

但是包非法访问的问题仍然存在。且扁平化算法的复杂度比较高,相对的比较耗时。尤其是大项目,依赖了很多很多包的时候,我们会明显的感觉到,npm的依赖安装变慢了。

二、pnpm介绍


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

pnpm出现就是为了解决现在npm存在的问题,正如pnpm官网所说,它是一款速度快,节省磁盘空间的软件包管理器。



2.1 软链接与硬链接

pnpm在构建依赖关系中,使用到了软硬链接,先了解一下软硬链接

硬链接(hard link):

在文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。

硬链接与源文件同时指向一个物理地址,它与源文件共享存储数据,拥有相同的Inode。

通过 ln 可以创建一个硬链接。

stat -x test.js

ln test.js test_hard.js

stat -x test.js

软链接(soft link、符号链接Symbolic link):

软链接可理解为指向源文件的指针,它是单独的一个文件,仅仅只有几个字节,它拥有独立的Inode,存储的是访问源文件的路径。

通过 ln -s> 可以创建一个软链接。

ln -s test.js test_soft.js
stat -x test.js

软链接与硬链接区别:

2.2 node_modules层级结构

包的依赖关系:

pnpm install之后,node_modules的结构如下:(-> 软链接 => 硬链接)

node_modules
|_ A -> .pnpm/[email protected]/node_modules/A
|_ B -> .pnpm/[email protected]/node_modules/B
|_ .pnpm
  |_ [email protected]
    |_ node_modules
      |_ A => pnpm/store/A 
      |_ C -> ../../[email protected]/node_modules/C
  |_ [email protected]
    |_ node_modules
      |_ B => pnpm/store/B 
      |_ C -> ../../[email protected]/node_modules/C
  |_ [email protected]
    |_ node_modules
      |_ C => pnpm/store/C 
  |_ [email protected]
    |_ node_modules
      |_ C => pnpm/store/C

可以看下面的图,更加直观:

  1. 在node_modules的根目录下存储的是我们的依赖包(文件名上带有版本号)和.pnpm文件夹,图中A 和 B 是我们在项目package.json中声明的依赖包,node_modules主目录下除了A,B 没有其它包,这样就避免了幽灵依赖的问题。而且依赖包均通过软链接的方式链接到.pnpm中的某个硬链接上。比如A的目录下并没有node_modules,它是一个软链接,真正的文件位于.pnpm/[email protected]/node_modules/A 。
  2. 再看.pnpm目录中,我们所有直接和间接依赖的包都平铺在这个目录中,没有层级嵌套的问题,最终均硬链接指向全局pnpm仓库里的store目录下的某个存储区域,这样不同的项目都能从全局 store 寻找到同一个依赖,大大节省了磁盘空间
  3. 最后看下全局store具体结构,如下:
pnpm
└─ store
    └─ v3
        └─ files
            ├─ 00
              - 0a346c6856ab86256d1e8e82e9a4b7b94d9e4afdae56419931b532d179ccf23dd0b0a61baebce0ab4f74ad92ddec77536a9e294d15088cec8d8177c274c794
              - 0ae2a8f703cb4e7e02da38f41f921a7de3e8d05f0f24c8bd67d9e74cec05f238e8363fe06448ab961c0e95d736f7dedf17eb05d6cc13dae45ed08f5bb5eaed
              - ...
            ├─ 0a 
            ├─ 0b

pnpm/store里存放的不是npm 包的源码,而是hash文件。如果文件内容不变,hash 值也不会变。这个非常适合npm的安装包,一般来说,依赖包的更新都是向下兼容的,两个版本的包差别只是部分,使用hash存储,会根据文件内容变化,只会存储变化的部分,相同的部分,生成的hash不会变,只存储一份就够了,一定程度上,也节省了磁盘空间。

以pnpm test 为例,看到以下输出:

React Query Auth Token Refresh

全局store:

node_modules的目录结构如下:

整体来看,pnpm解决了幽灵依赖问题,节省了磁盘空间,并且在重复安装依赖包时,不需要复制文件,速度非常快。

2.3 pnpm 常用命令

基本上与npm一致, 几乎等于0成本学习。

npm install pnpm -g pnpm安装

pnpm add <pkg>	安装软件包到 dependencies

pnpm add -D <pkg>	安装软件包到 devDependencies

pnpm add -g <pkg>	全局安装软件包

pnpm install 或 pnpm i	下载项目所有依赖项

pnpm update 或 pnpm up	遵循 package.json 指定的范围更新所有的依赖项

pnpm update -g <pkg>	从全局更新一个依赖包

pnpm remove <pkg>	从项目的 package.json 中删除相关依赖项

pnpm remove -D <pkg>	仅删除开发环境 devDependencies 中的依赖项

pnpm remove <pkg> -g	从全局删除一个依赖包

pnpm run <script> 或 pnpm <script>	运行脚

三、总结

3.1 优点


1、节省磁盘空间

pnpm通过硬连接机制,把包都存储在全局的pnpm/store/目录下。当安装软件包时,其包含的所有文件都会硬链接自此位置,而不会占用额外的硬盘空间。pnpm 对于同一个包不同的版本也仅存储其增量改动的部分。

2、快速

安装包之前,如果已经在全局安装过,就不会被再次下载了,节省了安装时间。随着项目增多,效果会越来越明显。

3、严格

pnpm 默认创建了一个非扁平化的 node_modules,因此代码无法访问未声明的包,解决了npm 存在的幽灵依赖问题。

3.2 缺点


1、调试问题

所有项目引用的包都在全局一个地方,如果想对某个包进行调试,其他项目本地运行也会受到影响。

2、兼容问题

软连接的方式在 windows 可能会存在一些兼容的问题,但是针对这个问题,pnpm 也提供了对应的解决方案:在 win 系统上使用一个叫做 junctions 的特性来替代软连接,这个方案在 window 上的兼容性要好于 symlink。(pnpm在windows中也可以使用,只不过使用junctions来代替软链接)

整体来说,pnpm 通过内容可寻址存储(CAS)、软硬链接等方式管理依赖包,解决了前端包管理工具一直存在的痛点问题。对于新项目来说,使用pnpm是一个很好的选择。