npm/yarn
介绍下npm/yarn在依赖管理方面存在的缺陷, 方便后续了解pnpm
早期的npm
使用早期的npm安装依赖, node_modules 文件夹会以递归的形式呈现,严格按照 package.json 结构以及次级依赖的 package.json 结构将依赖安装到它们各自的 node_modules 中,直到次级依赖不再依赖其它模块
就像下面这样
node_modules
└─ A
├─ index.js
├─ package.json
└─ node_modules
└─ B
├─ index.js
└─ package.json
假设项目中的两个依赖项依赖了相同的依赖项
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
真实项目中问题会更加严重, 相同的包被重复安装, 占用空间巨大
npm v3 / yarn
npm v3和yarn 采用了新的项目依赖管理方式, 实现了扁平化安装
继续拿上面的例子举例, A@1.0.0和B@1.0.0都依赖了C@1.0.1
node_modules
├─ A@1.0.0
│ ├─ index.js
│ └─ package.json
├─ B@1.0.0
│ ├─ index.js
│ └─ package.json
└─ C@1.0.1
├─ index.js
└─ package.json
包C被提升到了顶层,这里需要注意的是,多个版本的包只能有一个被提升上来,其余版本的包会嵌套安装到各自的依赖当中(类似npm2的结构)。
上面的方式, 虽然解决了嵌套过深和重复安装的问题, 但是也有很多问题
例如:
A@1.0.0 依赖 C@1.0.1, B@1.0.0 依赖 C@1.0.2,那么生成的 node_modules 结构什么样的呢?
node_modules
├── A@1.0.0
├── B@1.0.0
└── node_modules
└── C@1.0.2
├── C@1.0.1
// 还是下面这种
node_modules
├── A@1.0.0
└── node_modules
└── C@1.0.1
├── B@1.0.0
├── C@1.0.2
其实是都有可能,这就依赖于 A 和 B 在 package.json中的位置。
npm v5 扁平 + lock
为了解决 node_modules 结构的不确定性,于是在 v5 版本中默认会生成 package-lock.json文件 。
我们来看看只安装 swiper 这个库对应的 package-lock.json 文件。
// package.json
{
"name": "test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"license": "ISC",
"dependencies": {
"swiper": "^8.0.7"
}
}
// package-lock.json
{
"name": "test",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"dom7": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.4.tgz",
"integrity": "sha512-DSSgBzQ4rJWQp1u6o+3FVwMNnT5bzQbMb+o31TjYYeRi05uAcpF8koxdfzeoe5ElzPmua7W7N28YJhF7iEKqIw==",
"requires": {
"ssr-window": "^4.0.0"
}
},
"ssr-window": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz",
"integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ=="
},
"swiper": {
"version": "8.0.7",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-8.0.7.tgz",
"integrity": "sha512-GHjDfxSZdupfU7LrSVOpaNaT7R1D2zxopPGBFz1UOXOtsYvVJLg0k6NvkTAD7qn0ASl5pTti82qoYwvYvIkg4g==",
"requires": {
"dom7": "^4.0.4",
"ssr-window": "^4.0.2"
}
}
}
}
package-lock.json文件记录了每一个包的版本 和其所依赖的其他包版本, 下一次安装的时候可以通过这个文件来安装. 由 package-lock.json 文件和 package.json 文件能确保始终得到一致的 node_modules 目录结构
npm和yarn共有问题
NPM分身
举个例子:
A@1.0.0 --> X@1.0.0和Y@1.0.0 B@1.0.0 --> X@2.0.0和Y@2.0.0 C@1.0.0 --> X@1.0.0和Y@2.0.0 D@1.0.0 --> X@2.0.0和Y@1.0.0
安装后
- X@1.0.0
- Y@1.0.0
- A@1.0.0
- B@1.0.0
- X@2.0.0
- Y@2.0.0
- C@1.0.0
- Y@2.0.0
- D@1.0.0
- X@2.0.0
X@2.0.0和Y@2.0.0还是会被安装多次, 存在性能问题
幽灵依赖
X@1.0.0 没有在package.json被依赖, 但是我们却可以引用这个包, 引发这个现象的原因一般是因为 node_modules 结构所导致的。
pnpm
使用 pnpm 安装,pnpm 会将依赖存储在位于 ~/.pnpm-store 目录下。只要你在同一机器下,下次安装依赖的时候 pnpm 会先检查 store 目录,如果有你需要的依赖则会通过一个硬链接丢到到你的项目中去,而不是重新安装依赖。
node modules中会存在一个.pnpm目录, 以平铺的形式储存着所有的包,正常的包都可以在这种命名模式的文件夹中被找到 (.pnpm/+@/node_modules/)
Linux 中包括两种链接:
- 硬链接(hard link)
- 软链接(soft link),软链接又称为符号链接(symbolic link)
inode
每一个文件都有一个唯一的 inode,它包含文件的元信息,在访问文件时,对应的元信息会被 copy 到内存去实现文件的访问。
hard link
硬链接可以理解为是一个相互的指针,创建的 hardlink 指向源文件的 inode,系统并不为它重新分配 inode。
硬链接不管有多少个,都指向的是同一个 inode 节点,这意味着当你修改源文件或者链接文件的时候,都会做同步的修改。
每新建一个 hardlink 会把节点连接数增加,只要节点的链接数非零,文件就一直存在,不管你删除的是源文件还是 hradlink。只要有一个存在,文件就存在
soft link
软链接可以理解为是一个单向指针,是一个独立的文件且拥有独立的 inode,永远指向源文件,这就类比于 Windows 系统的快捷方式。
举个例子
项目依赖 B@1.0.0, B@1.0.0依赖A@1.0.0
node_modules
└── .pnpm
├── A@1.0.0
│ └── node_modules
│ └── A --> <store>/A
│ ├── index.js
│ └── package.json
└── B@1.0.0
└── node_modules
└── B --> <store>/B
├── index.js
└── package.json
B@1.0.0依赖A@1.0.0
node_modules
└── .pnpm
├── A@1.0.0
│ └── node_modules
│ └── A --> <store>/A
└── B@1.0.0
└── node_modules
├── B --> <store>/B
└── A -> ../../A@1.0.0/node_modules/A
最后,pnpm会通过软链链接项目直接的依赖
node_modules
├── B -> ./.pnpm/B@1.0.0/node_modules/B
└── .pnpm
├── A@1.0.0
│ └── node_modules
│ └── A --> <store>/A
└── B@1.0.0
└── node_modules
├── B --> <store>/B
└── A -> ../../bar@1.0.0/node_modules/A