- 作者:老汪软件技巧
- 发表时间:2024-11-14 00:02
- 浏览量:
npm && yarn的问题幽灵依赖 稳定性风险
假设如下情况 项目里引入了一个第三方包 这个三方包依赖了100个其他包。 因为 npm和Yarn使用的 扁平化node_modules结构。 所有的依赖都是平铺在node_modules中的。
所以此时node_modules目录已经有101个依赖包。如果我们使用了一个没有在package.json中声明但是存在于node_modules文件夹下的包 此时通过node的寻址规则 项目是可以正常解析到的。
这个不存在package.json中的包就是一个幽灵依赖。
这是非常危险的情况 因为我们的依赖是不稳定的 如果这个第三方依赖包 升级了版本 从原来的100个依赖 变成了 10个依赖 其中90个可能都是找不到的 这时我们的项目再运行就会有问题了。
版本分身 磁盘占用
假设如下情况 我们直接在项目中引入了lodash@4.x.x 然后又依赖的其他的第三方库 这些第三方引入了lodash@3.x.x和lodash@2.x.x等等 那么只有lodash@4.x.x会被平铺在最外层 其他的依然会嵌套在子包中
node_modules/
├── lodash@4.x.x
├── A/
└── node_modules/
└── lodash@2.x.x
├── B/
└── node_modules/
└── lodash@3.x.x
├── C/
└── node_modules/
└── lodash@2.x.x
同一个包的不同版本会被重复下载对此,这只是其中一个项目 如果我们电脑上有几十个几百个项目呢?会对磁盘存储造成浪费。
软链接
symlink1和symlink2都是独立的文件,它们内部包含指向/path/to/original/file的路径信息。这两个软链接可以独立存在,但它们都指向同一个目标文件。
硬链接
hardlink1和hardlink2共享相同的数据块,所以它们在文件系统中是完全等价的文件。
pnpm解决幽灵依赖
我们有一个 monorepo 项目,其中包含两个子项目project-a和project-b,它们的依赖关系如下:
在传统的npm或Yarn环境下,由于node_modules目录是扁平化的,当npm install或yarn install之后,某个子项目(比如project-a)即使没有显式声明对moment的依赖,它也能访问到moment。
安装后的目录如下:
my-monorepo/
├── node_modules/
│ ├── lodash/
│ ├── moment/
├── packages/
│ ├── project-a/
│ │ ├── package.json
│ │ └── index.js
│ └── project-b/
│ ├── package.json
│ └── index.js
└── package.json
在npm或Yarn安装后,执行以下代码在project-a是可行的,但具有幽灵依赖的风险:
const lodash = require('lodash');
console.log(lodash.isEmpty({}));
const moment = require('moment');
console.log(moment().format()); // 没有在 package.json 中声明,但可以访问
在传统的npm和Yarn中,因为node_modules目录结构是扁平化处理的,导致project-a可以访问到project-b的moment依赖。
使用pnpm安装依赖后的项目结构:
my-monorepo/
├── node_modules/
│ ├── .pnpm/
│ │ ├── lodash@4.17.21/
│ │ │ └── node_modules/
│ │ │ └── lodash/
│ │ ├── moment@2.29.1/
│ │ │ └── node_modules/
│ │ │ └── moment/
│ ├── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash
│ ├── moment -> .pnpm/moment@2.29.1/node_modules/moment
├── packages/
│ ├── project-a/
│ │ ├── node_modules
│ │ │ └── lodash -> ../../../node_modules/.pnpm/lodash@4.17.21/node_modules/lodash
│ │ └── index.js
│ └── project-b/
│ ├── node_modules
│ │ └── moment -> ../../../node_modules/.pnpm/moment@2.29.1/node_modules/moment
│ └── index.js
└── package.json
在pnpm这种结构下,每个子项目只能访问其node_modules下的包,这些包是通过软链接指向.pnpm目录中的实际位置。
project-a只能访问到其明确声明的依赖lodash,无法访问moment:
const lodash = require('lodash');
console.log(lodash.isEmpty({}));
try {
const moment = require('moment');
console.log(moment().format());
} catch (error) {
console.error('moment is not a declared dependency in project-a');
}
执行上述代码时将会抛出错误,因为moment存在于project-b中,而不是project-a的有效依赖范围之内。
节约磁盘空间
在使用pnpm时,pnpm在用户主目录下维护一个全局缓存目录(例如:~/.local/share/pnpm/store),存储所有包的实际数据
my-monorepo/
├── packages/
│ ├── project-a/
│ │ ├── package.json
│ │ └── node_modules/
│ │ └── lodash -> ../../.pnpm/lodash@4.17.21/node_modules/lodash (软链接)
│ ├── project-b/
│ │ ├── package.json
│ │ └── node_modules/
│ │ └── lodash -> ../../.pnpm/lodash@4.17.21/node_modules/lodash (软链接)
├── node_modules/
│ ├── .pnpm/
│ │ ├── lodash@4.17.21/
│ │ │ └── node_modules/
│ │ │ └── lodash/ 【这个目录中的内容是硬链接 指向全局中的lodash】
│ └── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash (软链接)
└── pnpm-workspace.yaml
根node_modules & 子包的node_modules目录:
里面的内容实际都是软链接,指向 .pnpm目录中的实际包文件
.pnpm目录
.pnpm目录是pnpm用来存储实际包文件的地方。每一个包版本有一个独立的目录,包括它的所有文件。
.pnpm目录中的包文件使用了硬链接机制,从全局存储中引用实际文件。这些包文件存储在全局缓存中,而.pnpm目录中的文件只是硬链接到全局缓存的位置。
其他的部分都是使用的软链接引用的项目地址。
通过这种方式可以 大大减少了磁盘占用 提高了资源利用率。
在多项目环境中,pnpm的机制确保依赖管理更严谨、资源利用更高效,从而为开发者提供了一个更稳定、高效的依赖管理工具。