npm、pnpm、tnpm的区别

133 阅读8分钟

包管理工具的历程

包管理工具的历程

2010年之前:前端依赖项需要开发着手动下载再保存到存储库中(会带来许多麻烦)

2010年1月: npm第一个版本正式发布;

2015年5月: npm发布v3.x 版本;

tnpm的出现,具体年份不知,比pnpm早很多;

2016年1月: pnpm 提交诞生并发布第一个版本;

2016年: yarn发布第一个v0.x 正式版本;

2016年: npm 发布v4.x 版本;

2017年: npm 发布v5.x 版本;

2018年: npm 发布v6.x 版本;

2020年: yarn 发布正式版本 2;

2020年: npm 发布v7.x 版本;

2021年: yarn 发布正式版本 3;

2021年: pnpm发布6.0 版本;

2022年: npm 发布v9.x 版本;

npm

npm是最原始的包管理工具,此后的pnpm、tnpm这些,都是在它的基础上进行二次封装的。

最开始它存在挺多缺点,比如:幽灵依赖、版本冲突、下载速度慢、内存占用大等。

npm的进化历程:

npm.v2

使用嵌套依赖管理方式,这样如果项目依赖关系较深,就会出现地狱嵌套,如同函数嵌套的回调地狱问题一样。

如果你想要删除某个node_modules包,由于嵌套的太深,还会出现删不掉的问题,并且包嵌套过多,会给内存空间带来很多不利影响,这样非常难以维护,还会存在版本冲突的问题。

问题具体化:

重复依赖问题

如图,A和C都需要依赖B,这时候,B就会被重复安装,就会产生重复安装问题。

npm.v3

通过扁平化处理后,解决了嵌套问题,引入hoist(提升)机制, 从一定程度上解决了 重复依赖 和 嵌套地狱的问题。

提升机制解释

处理 A 的依赖 B 时,会将其提升到顶级依赖,如果后续有安装包也需要依赖 B,就会去上一级的包中查找,如果有相同版本的包,就不会再去重复下载,直接从上一层拿到需要的依赖包B。

但还是存在一定问题 ,扁平化也存在缺点: 首先它会遍历所有的项目依赖关系,然后再决定如何生成扁平的node_modules目录结构,npm必须为所有使用到的模块构建一个完整的依赖关系树,这是一个非常耗时间的过程,会导致npm的使用非常慢。

问题具体化:

版本冲突

当包对依赖项目的版本要求不同时,会产生如下结果:

比如下图,A需要依赖版本B1,C需要依赖版本B2,那么当检测到A时,会把B.v1提升到顶层,当检测到C时,发现顶层已经存在B.v1,无法将B.v2再提升,就会将B2放到C的依赖下面进行嵌套。

由于一个根目录只能存放同一个包的一个版本文件,导致还是没有完全做到扁平化处理,当版本冲突的依赖一多,又会出现嵌套问题。

不确定性

在检测过程中,提升机制是根据“先安装先提升”原则,而不是谁先被使用,谁就先提升,这样会导致,当不同的用户执行npm install后,拿到的包目录顺序结果产生不同

npm.v5

受到yarn.lock的启发,新增了 package-lock.json文件,这个文件记录了package.json依赖的模块,以及模块的子依赖,并且给每个依赖都标明了版本、路径,并且验证了模块的完整性这样就解决了v3中的目录不确定性问题。

同时引入了缓存机制,可以在全局模块中安装依赖,这样当安装其他依赖项,需要用到相同版本的依赖,可以直接到全局文件中寻找,就能真正地避免重复安装的问题。

具体解释:

验证模块的完整性的过程

首先lock文件的integrity字段中存在一个哈希值,当执行npm i命令的时候,npm会下载对应模块的压缩文件,并进行SHA256加密算法的哈希值计算,在下载依赖的过程中,会对比两个哈希值是否一致,如果一致则允许下载,否则报错。

{
  "name": "example-package",
  "version": "1.0.0",
  "dependencies": {
    "module-name": {
      "version": "1.2.3",
      "resolved": "https://registry.npmjs.org/module-name/-/module-name-1.2.3.tgz",
      "integrity": "sha512-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
      "dev": true
    }
  }
}

npm未解决问题

直到现在,npm的最大问题依然存在,幽灵依赖带来的缺陷,也会带来别的麻烦。

幽灵依赖解释

项目的依赖树中存在一些没有在package.json中明确声明的依赖。这些依赖通常是由其他依赖项间接引入的,而不是直接在项目中声明的。

比如你的依赖文件是如下代码的样子

{  
  "dependencies": {  
    "minimatch": "^3.0.4"  
  },  
  "devDependencies": {  
    "rimraf": "^2.6.2"  
  }
}

在代码中按照如下使用依赖,那么没有在上一段代码中出现的 A 和 B 就是幽灵依赖

var minimatch = require('minimatch');  
var A = require('A'); // 👻  
var B = require('B'); // 👻

pnpm

pnpm是现在非常主流的一款包管理工具,

采用操作系统中硬链接和软链接的方式,来进行依赖的管理, 有效地解决了大部分幽灵依赖的问题。

具体解释:

硬链接(Hard Link)

前置知识

概念:硬链接是文件系统中的一个链接,它指向磁盘上的数据。当创建一个硬链接时,实际上是在创建一个和原始文件相同的入口点,但是不占用额外的磁盘空间。这个新的链接和原始文件共享相同的数据块,任何一个文件的修改都会反映在另一个上。(总结:直接指向文件数据块的一种链接

缺点:硬链接不能跨文件系统创建,也不能用于链接目录,但如果原始文件被删除,硬链接依然可以访问数据。

软链接(符号链接 Symbolic Link)

前置知识

概念:软链接是一个特殊类型的文件,它包含了另一个文件的路径。类似于 Windows 系统中的快捷方式。与硬链接不同,软链接可以指向目录,也可以跨文件系统。(总结:一种符号路径,指向另一个文件目录

缺点:软链接指向文件或目录的路径,如果原始文件被删除,会导致指向的文件找不到,软链接就会失效。

在pnpm中的具体应用:

硬链接(磁盘共享策略): 当你安装一个包时,pnpm 会首先检查.pnpm-store中是否已有该包,如果已经存在并且版本匹配,pnpm 会直接使用全局中的包,而不需要再次下载,避免了重复依赖的问题。

软链接(符号链接):pnpm 中,使用符号链接将全局存储中的依赖映射回到项目的 node_modules 目录中。因为 Node.js 会忽略符号链接并执行实际路径,所以在执行过程中,也不会产生耗时问题。

如下图:

项目需要依赖A,会在包的根目录下创建软链接A,指向.pnpm中的A依赖,如果.pnpm中的A又依赖于B,那么B也会被平铺在.pnpm中,A一样也能通过创建软链接来连接到依赖B,这样就解决了嵌套的问题。

总结:通过软链接来指向文件的目录,在目录下通过硬链接来指向store中的真实依赖数据。

解决幽灵依赖问题的方式:

对间接依赖妥协处理:考虑到一些第三方库可能默许幽灵依赖的出现,

pnpm 默认启用了 hoist 配置,这个配置会将一些间接依赖提升(hoist)到一个特殊的目录node_modules/.pnpm/node_modules中。这样做的目的是在保持依赖隔离的同时,允许某些特殊情况下的间接依赖被访问。

tnpm

在使用过程中,我注意到,tnpm同样也是通过软链接来解决重复依赖的问题。

为什么tnpm不和pnpm一样,让软硬链接结合使用?

pnpm正是因为软硬链接的引入,导致它与许多场景产生不兼容问题。

由于硬链接不支持指向文件目录,在对比下,tnpm最新的技术是通过 clonefile 来优化的。

clonefile是什么?

概念: 通常用于创建一个文件的精确副本(复制),它可以在文件系统级别实现高效的文件复制。通过这种方式,它可以在不占用额外的存储空间的情况下创建文件的副本(特别是在同一文件系统内部)。

clonefile依然使用了硬链接的理念,但它的好处是,能够处理文件的目录。

tnpm和npm的速度对比

在同一环境同一个项目中,使用time tnpm installtime pnpm install命令来测试两个包的速度,结果如下:

装相同的依赖项后

tnpm:

npm:

通过测试下来,发现两者速度差不多,那么为什么我们还要使用tnpm?

tnpm的优点

tnpm默认使用淘宝镜像源, 不需要用户再手动设置

通过并行安装依赖项来提高安装速度

使用软链接解决重复依赖问题

引入了更快的安装模式tnpm rapid,解决构建时依赖安装慢的问题,甚至速度能够超越pnpm。

虽然tnpm的使用中也会出现幽灵依赖问题,但“三条鱼”框架都设置了幽灵依赖检测机制,因此tnpm更贴切我们的业务。