npm、yarn、pnpm

424 阅读6分钟

npm

嵌套的 node_modules 结构(v1 / v2)

存在的问题

存在依赖地狱的问题和重复依赖安装的问题。

扁平化 (v3)

采用扁平化的方式,将子依赖「提升」(hoist),解决了依赖地狱的问题。

依赖查找算法:在安装新的包时,会不停往上级node_modules中查找。如果找到相同版本的包就不会重新安装,在遇到版本冲突时才会在模块下的 node_modules 目录下存放该模块子依赖,解决了大量包重复安装的问题,依赖的层级也不会太深。

存在的问题

幽灵依赖问题

指在 package.json 中未定义的依赖,但项目中依然可以正确地被引用到,这会存在潜在的问题

不确定性

同样的 package.json 文件,install 依赖后可能不会得到同样的 node_modules 目录结构,这是其依赖安装算法决定的。

依赖重复问题

依然存在重复的依赖问题(因为同一父级的 node_modules 只能存在一个公共依赖)。这种情况下,如果依赖自身存在副作用,那么就会导致多次副作用。

扁平化 + lock (v5)

在npm v5中新增了package-lock.json。当项目有package.json文件并首次执行npm install安装后,会自动生成一个package-lock.json文件,该文件里面记录了package.json依赖的模块,以及模块的子依赖。并且给每个依赖标明了版本、获取地址和验证模块完整性哈希值。通过package-lock.json,保障了依赖包安装的确定性与兼容性,使得每次安装都会出现相同的结果。

yarn

提升安装速度

在 npm 中安装依赖时,安装任务是串行的,会按包顺序逐个执行安装,这意味着它会等待一个包完全安装,然后再继续下一个。yarn 采用了并行操作,在性能上有显著的提高。而且在缓存机制上,yarn 会将每个包缓存在磁盘上,在下一次安装这个包时,可以脱离网络实现从磁盘离线安装。

lockfile 解决不确定性

在依赖安装时,会根据 package.josn 生成一份 yarn.lock 文件。 lockfile 里记录了依赖,以及依赖的子依赖,依赖的版本,获取地址与验证模块完整性的 hash。 即使是不同的安装顺序,相同的依赖关系在任何的环境和容器中,都能得到稳定的 node_modules 目录结构,保证了依赖安装的确定性。

与 npm 一样的弊端

yarn 依然和 npm 一样是扁平化的 node_modules 结构,没有解决幽灵依赖依赖分身问题。

yarn lock vs npm lock

  • 文件格式不同,npm v5 使用的是 json 格式,yarn 使用的是自定义格式
  • package-lock.json 文件里记录的依赖的版本都是确定的,不会出现语义化版本范围符号(~ ^ *),而 yarn.lock 文件里仍然会出现语义化版本范围符号
  • package-lock.json 文件内容更丰富,实现了更密集的锁文件,包括子依赖的提升信息
  • npm v5 只需要 package.lock 文件就可以确定 node_modules 目录结构
  • yarn.lock 无法确定顶层依赖,需要 package.json 和 yarn.lock 两个文件才能确定 node_modules 目录结构。node_modules 目录中 package 的位置是在 yarn 的内部计算出来的,在使用不同版本的 yarn 时可能会引起不确定性。

pnpm

在不考虑循环依赖的情况下,实际的项目依赖结构图为有向无环图(DAG),但是npm和yarn通过文件目录和node resolve算法模拟的实际上是有向无环图的一个超集(多出了很多错误祖先节点和兄弟节点之间的链接),这导致了很多的问题。pnpm通过硬链接与符号链接结合的方式,更加精确的模拟DAG来解决yarn和npm的问题。

非扁平化的node_modules

硬链接(hard link)节约磁盘空间

硬链接以理解为源文件的副本,使得用户可以通过不同的路径引用方式去找到某个文件,他和源文件一样的大小但是事实上却不占任何空间。pnpm 会在全局 store 目录里存储项目 node_modules 文件的硬链接。硬链接可以使得不同的项目可以从全局 store 寻找到同一个依赖,大大节省了磁盘空间。该策略会将包安装在系统的全局 store 中,依赖的每个版本只会在系统中安装一次。

符号链接(symbolic link)创建嵌套结构

软链接可以理解为快捷方式,pnpm在引用依赖时通过符号链接去找到对应磁盘目录(.pnpm)下的依赖地址。非扁平化结构。

优点

这套全新的机制设计地十分巧妙,不仅兼容 node 的依赖解析,同时也解决了:

  1. 幽灵依赖问题:只有直接依赖会平铺在 node_modules 下,子依赖不会被提升,不会产生幽灵依赖。
  2. 依赖分身问题:相同的依赖只会在全局 store 中安装一次。项目中的都是源文件的副本,几乎不占用任何空间,没有了依赖分身。
  3. 同时,由于链接的优势,pnpm 的安装速度在大多数场景都比 npm 和 yarn 快 2 倍,节省的磁盘空间也更多。

缺点

  1. 由于 pnpm 创建的 node_modules 依赖软链接,因此在不支持软链接的环境中,无法使用 pnpm,比如 Electron 应用。
  2. 忽略了 package-lock.json。npm 的锁文件旨在反映平铺的 node_modules 布局,但是 pnpm 默认创建隔离布局,无法由 npm 的锁文件格式反映出来,而是使用自身的锁文件pnpm-lock.yaml。
  3. 因为依赖源文件是安装在 store 中,调试依赖或 patch-package 给依赖打补丁也不太方便,可能会影响其他项目。
  4. 子依赖提升到同级的目录结构,虽然由于 Node.js 的父目录上溯寻址逻辑,可以实现兼容。但对于类似 Egg、Webpack 的插件加载逻辑,在用到相对路径的地方,需要去适配。

useful posts