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 部分字段说明:
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
可以看下面的图,更加直观:
- 在node_modules的根目录下存储的是我们的依赖包(文件名上带有版本号)和.pnpm文件夹,图中A 和 B 是我们在项目package.json中声明的依赖包,node_modules主目录下除了A,B 没有其它包,这样就避免了幽灵依赖的问题。而且依赖包均通过软链接的方式链接到.pnpm中的某个硬链接上。比如A的目录下并没有node_modules,它是一个软链接,真正的文件位于.pnpm/[email protected]/node_modules/A 。
- 再看.pnpm目录中,我们所有直接和间接依赖的包都平铺在这个目录中,没有层级嵌套的问题,最终均硬链接指向全局pnpm仓库里的store目录下的某个存储区域,这样不同的项目都能从全局 store 寻找到同一个依赖,大大节省了磁盘空间。
- 最后看下全局store具体结构,如下:
pnpm
└─ store
└─ v3
└─ files
├─ 00
- 0a346c6856ab86256d1e8e82e9a4b7b94d9e4afdae56419931b532d179ccf23dd0b0a61baebce0ab4f74ad92ddec77536a9e294d15088cec8d8177c274c794
- 0ae2a8f703cb4e7e02da38f41f921a7de3e8d05f0f24c8bd67d9e74cec05f238e8363fe06448ab961c0e95d736f7dedf17eb05d6cc13dae45ed08f5bb5eaed
- ...
├─ 0a
├─ 0b
pnpm/store里存放的不是npm 包的源码,而是hash文件。如果文件内容不变,hash 值也不会变。这个非常适合npm的安装包,一般来说,依赖包的更新都是向下兼容的,两个版本的包差别只是部分,使用hash存储,会根据文件内容变化,只会存储变化的部分,相同的部分,生成的hash不会变,只存储一份就够了,一定程度上,也节省了磁盘空间。
以pnpm test 为例,看到以下输出:
全局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是一个很好的选择。