npm /yarn 原理
依赖地狱
早期的 npm 会直接安装依赖,如果依赖存在依赖,则在依赖里加入 node_modules。
假如有两个依赖 A
, B
。 A
依赖于 C
D
,而 B
依赖于 C
E
。则依赖目录是这样的:
node_modules
├─ A
│ └─ node_modules
│ └─ C
│ └─ D
├─ B
│ └─ node_modules
│ └─ C
└─ └─ E
从依赖目录可以看出,假如A
依赖于C@1.0.0
, B
也依赖于C@1.0.0
。则会同时安装两份C@1.0.0
。很明显,有一份C@1.0.0
是重复的。
对于大型项目,依赖会特别多,个别依赖也会嵌套比较深。这样就会导致不同的依赖分支里会有大量的重复依赖,且依赖嵌套过深的话会导致 windows 目录路径过长问题。由于是嵌套导致的,这个时期的依赖安装被称为依赖地狱。
依赖扁平化
为了解决依赖嵌套的问题, npm3.x 开始(yarn 也是这个时期出现的),开始依赖扁平化。也就是把依赖的依赖提升到 node_modules 根目录下,重复的依赖不再安装。这样就可以实现依赖共享。
比如上面这个例子,依赖目录如下:
node_modules
├─ A
├─ B
├─ C@1.0.0
├─ D
└─ E
这样好像解决了重复依赖的问题?只能说是解决了,但没有完全解决。
假如 A
依赖于 C@1.0.0
和 D
,B
依赖于 C@2.0.0
和 E
,同时 E
又依赖于 C@2.0.0
。则依赖扁平化的安装如下:
node_modules
├─ A
├─ C@1.0.0
├─ D
├─ B
│ └─ node_modules
│ └─ C@2.0.0
│─ E
│ └─ node_modules
└─ └─ C@2.0.0
安装的时候,C@1.0.0
D
E
会被提升到 node_modules 根目录。但是由于版本不一致,B
和 E
依赖的 C@2.0.0
就只能放到自身的 node_modules 下,而不能共享提升的 C@1.0.0
。这样一来 C@2.0.0
就被安装了两份。
C@2.0.0
被叫做分身依赖。分身依赖就是相同版本的依赖被重复安装。
除了分身依赖之外,依赖扁平化还引入了新的问题。
不确定性
第一个问题就是不确定性。
假如A
依赖于C@1.0.0
, B
依赖于C@2.0.0
。那么依赖安装会提升的是C@1.0.0
还是C@2.0.0
呢?也就是下面这两种结构:
node_modules
├─ A
│─ C@1.0.0
├─ B
│ └─ node_modules
└─ └─ C@2.0.0
还是
node_modules
├─ A
│ └─ node_modules
│ └─ C@1.0.0
├─ B
└─ C@2.0.0
网上大部分说法是会根据 package.json
里面的顺序决定谁会被提升,放在前面的包依赖的内容会被优先提升。
看了下最新版本的 npm 源码,发现 npm 其实会先调用 localeCompare
来对依赖进行排序,也就是字典序在前面的包依赖的内容会被优先提升。这样即使修改package.json
里依赖的顺序,也不会影响依赖提升,解决了不确定性。
幽灵依赖
第二个问题就是幽灵依赖。
package.json
中不存在却可以直接使用的依赖就是幽灵依赖。 在依赖扁平化后,被提升的依赖就是幽灵依赖。
假如A
依赖于C@1.0.0
。
node_modules
├─ A
└─ C@1.0.0
C@1.0.0
就是幽灵依赖。我们可以在项目中直接使用C@1.0.0
,但是package.json
中并没有这个依赖项。
这看上去好像没什么问题,但是设想这种情况:有人发现依赖 A 不再使用,于是删除了 A。但是幽灵依赖C@1.0.0
依然在项目被使用着,这就会引起报错。
pnpm 的破局之法
pnpm 的出现解决了分身依赖和幽灵依赖的问题。那 pnpm 是如何解决的呢?
假如 A
依赖于 C@1.0.0
,那么通过 pnpm 安装后的依赖目录如下:
node_modules
├─ .pnpm
│ └─ node_modules // 非直接依赖的包, 都是符号连接
│
│ └─ A@1.0.0
│ └─ node_modules
│ └─ A -> <store>/A
│ └─ C -> ../../C@1.0.0/node_modules/C
│
│ └─ C@1.0.0
│ └─ node_modules
│ └─ C -> <store>/C
│
└─ A -> ./.pnpm/A@1.0.0/node_modules/A
node_modules 根目录下有一个 .pnpm
文件夹和 A
。这里的 A 是个软连接,指向它的硬连接。
可以简单理解为:
硬连接是指向实际存储位置的一个指针文件。
软连接是指向硬连接的一个指针文件。
所有的依赖都会在 .pnpm
文件夹罗列出来(不同版本可以看做是不同的依赖)。每个依赖有个 node_modules 文件夹,里面存放的是自身的硬连接和自身所依赖的依赖的软连接。
用图片来表示如下:
这样 pnpm 就保证了同一个版本的依赖只安装一份。依赖根目录存放的是所有的直接依赖的软连接。至于依赖之间的关系则同样是通过软连接来构建。
lock 文件
npm 的 package-lock.json
,yarn 的 yarn.lock
,以及 pnpm 的 pnpm-lock.yaml
都是 lock 文件,用于锁定依赖项的版本和下载源。
如果项目里存在 lock 文件,则安装依赖时候会按照 lock 文件里的版本安装。如果不存在,则会按照 package.json
里的版本安装,且根据你所安装的依赖生成一份 lock 文件。
很多人倾向于直接写死 package.json
里的版本。但是 package.json
里只能锁定直接依赖的版本,不能保证依赖的依赖版本不变。
lock 文件不仅能够锁定直接依赖版本还能锁定依赖的依赖版本。 个人比较建议上传 lock 文件到 git,这样能够保证团队的依赖一致。