npm/yarn 依赖管理
npm1/2
早期npm安装依赖,node_modules文件以递归方式呈现,并且严格按照package.json结构以及次级依赖package.json结构安装
node_modules
└─ A
├─ index.js
├─ package.json
└─ node_modules
└─ B
├─ index.js
└─ package.json
└─ node_modules
└─ C
├─ index.js
└─ package.json
└─ node_modules
....
假如项目中依赖有相同的次级依赖,还会被重复安装
node_modules
├─ A
│ ├─ index.js
│ ├─ package.json
│ └─ node_modules
│ └─ C
│ ├─ index.js
│ └─ package.json
└─ B
├─ index.js
├─ package.json
└─ node_modules
└─ C
├─ index.js
└─ package.json
真实的开发场景中问题会更多
- 依赖层级过深,导致文件路径过长
- 重复包被安装,导致node_modules文件体积巨大,占用过多磁盘
- 模块实例不能共享。(例如,两个不同包引入的React不是一个模块实例,会导致内部变量无法共享,导致一些不可预知的bug)
npm >3.x & yarn
npm3版本以及yarn开始采用扁平化方式代替嵌套式方式管理项目依赖。
例如:A和B都依赖C依赖安装后
node_modules
├─ A
│ ├─ index.js
│ └─ package.json
├─ B
│ ├─ index.js
│ └─ package.json
└─ C
├─ index.js
└─ package.json
由于node的require机制(非node核心模块会递归上层node_modules目录查找)实现了依赖打平,解决了嵌套地狱的目录,但是确无法完成解决重复安装依赖的问题。
例如 B,C 同时依赖了A的2.0.0版本,还是会重复安装A@2.0.0两次
node_modules
├─ A@1.00
│ ├─ index.js
│ └─ package.json
├─ B
│ ├─ index.js
│ └─ package.json
| └─ node_modules
| └─ A@2.0.0
| ├─ index.js
| └─ package.json
└─ C
├─ index.js
└─ package.json
└─ node_modules
└─ A@2.0.0
├─ index.js
└─ package.json
那么npm/yarn会提升A@1.0.0还是A@2.0.0?事实上,这取决于package.json的顺序,这个就是为什么产生依赖树不确定的问题。
拍平依赖还很可能造成幽灵依赖的问题,比如,package.json里声明了A依赖,A依赖了B依赖,这种情况下,我们在代码中依然可以引用到B,如果A升级了版本,并且提升对于B的版本依赖,或者删除了B的版本依赖,这个时候我们代码就会出现不可预期的bug
这种扁平化的处理方式,依旧存在着问题
- 允许访问package.json中没有声明的依赖,造成幽灵依赖的问题
- 扁平化算法复杂性高,安装效率低。
- 依赖树不确定性。
- 无法完全解决依赖重复安装问题,导致分身依赖问题。
为了解决依赖结构的不确定性问题以及锁定唯一版本,yarn.lock,package-lock.json(npm > 5.x) 出现。 用来确保install之后都产生确定的node_modules结构。yarn.lock和package-lock.json不同的是一个是用yaml格式,一个用json格式,锁版本规则都是大版本根据package.json,小版本根据lock文件
pnp
yarn2.0 -pnp, 重写了node require机制,直接在node解析的时候把相应模板返回给node,抛去生成一个node_modules结构去给require查找。
好处:
- 节省了生成node_modules文件结构树的io操作时间,节省了node递归查找node_modules的时间
- 防止了幽灵依赖
- 防止了重复依赖的安装
问题
- 重写了node解析规则,需要兼容webpack,ts, vscode之类有自己模块解析的工具
pnpm
简介
从官方文档上的介绍来看,它是一个快速的,节省磁盘空间的包管理工具,号称比其他包管理器快俩倍,搬运下官网上的图。
可以看出从各个情况的速度对比,pnpm占很大优势。官方也给出了 The reason pnpm is fast。
上图是 npm & yarn 的安装流程。可以看出整个过程是统一解析,统一下载,统一解压。
上图是pnpm的安装流程。可以看出pnpm 没有安装的阻塞阶段。每个依赖都有自己的阶段,下一个阶段会尽快开始。
依赖管理
对于npm和yarn的幽灵依赖以及依赖重复问题,在pnpm里得到了很好的解决,pnpm的解决思路就是Store + Links。
Store
关于pnpm的store我们可以从安装中看出。
Content-addressable store 和 Virtual store 有分别是什么呢。
Content-addressable store:CAS内容寻址存储,不同于位置寻址,其可以根据内容来检索信息。
Virtual store: 虚拟存储目录 node_modules/.pnpm。
我理解的是如果某个依赖在 Content-addressable store目录中存在了话,那么就会直接从 Content-addressable store目录里面去 hard-link,在.pnpm下生成依赖的硬链接文件。
硬链接
硬链接本质是文件的同步副本,Windows系统的硬链接是一种针对文件的特殊快捷方式,只不过这种快捷方式的实现和一般的快捷方式不一样,是NTFS文件系统特有的属性之一。linux下硬链接引用的是文件系统种的物理索引(也称为inode),简单理解就是硬链接文件与源文件指向同一个磁盘空间,硬链接文件不占空间。
- 硬连接适用于在同一个卷的文件级别,不允许给目录创建硬链接;
- 硬连接是不能跨卷的,只有在同一文件系统中的文件之间才能创建链接。
软链接
linux下软链拥有自己的inode,文件本身存的是源文件的文件名和inode号,window下的软链类似快捷方式,包括Junction、symbolic-link。
node_modules结构
下面大致看下pnpm的node_modules的包结构。
node_modules
|__.pnpm
| |__D@1.0.0
| |__D@1.0.1
| |__A@1.0.0
| | |__node_modules
| | |__D
| | |__A
| |__B@1.0.0
| |__node_modules
| |__D
| |__B
|
|
|__A
|__B
可以看出外层严格按照package.json来,真正的依赖在相应的.pnpm下平铺,对应依赖的次级依赖全部到打平到相应的node_modules里,这样避免了能够引入到幽灵依赖
结合上面的结构目录图已经前置的软硬链接知识。来看下pnpm的依赖链接情况。
可以看出 package1 会通过JUNCTION软链接到.pnpm\package-test1-tang@1.0.0\node_modules\package-test1-tang
。 其最深路径为
.pnpm/<organization-name>+<package-name>@<version>/node_modules/<name>
组织名(若无会省略)+包名@版本号/node_modules/名称(项目名称)
这样可以解决掉路径过深的问题。那如果所有次级依赖也平铺到相应包的node_modules里会不会和外部被打平的依赖重复安装呢,可以看下。
真实情况其实是.pnpm下相关依赖下的重复次级依赖也会通过软链到外层拍平的依赖上,这样保证了依赖的次级依赖的完整和可看性,也不会重复安装,
整体的node_modules链接情况大致如下。
总结
yarn的出现解决了早期npm的不支持离线模式、树形结构的依赖、依赖安装不确定性等问题,但是npm的后期更新也解决了这些问题,pnpm的出现继承npm,yarn的优点,同时使用link+store的模式节省了安装时间和磁盘空间,解决了依赖重复安装以及幻影依赖的问题,由此看来pnpm目前的依赖管理更加的安全,高效,但是对于npm或者yarn,相信包的管理机制在后期的更新中都会得到更好的完善。