前言
一般我们接触的包管理工具有 npm、yarn 以及pnpm,小柒在工作中基本上新项目都采用了pnpm 来作为包管理工具,那我们就来聊一聊换成 pnpm 的好处。
npm
npm 从 v1 -v3- v5 版本的迭代都有重大的改变,一起来下看吧~。
npm v1 嵌套
npm 在 v3 之前 node_modules 里的包都是嵌套的。
node_modules
├── A@1.0.0
│ └── node_modules
│ └── B@1.0.0
├── C@1.0.0
│ └── node_modules
│ └── B@2.0.0
└── D@1.0.0
└── node_modules
└── B@1.0.0
复制代码
随着项目越来越大,依赖包越来越多,这样也会带来一系列问题。
- 嵌套的层级加深,文件路径过长。
- 大量的包被重复安装。比如上面例子中的 B@1.0.0 就会被装两份。
npm v3 扁平
在 v3 版本,实现了扁平化安装依赖的模式, node_modules 中的包成打平状态。
node_modules
├── A@1.0.0
├── B@1.0.0
└── C@1.0.0
└── node_modules
└── B@2.0.0
├── D@1.0.0
复制代码
npm v3 的变化,虽然避免了嵌套过深以及重复安装的问题(注意多个版本的包只能有一个版本被提升),但是其存在很多不确定性(即生成的 node_modules 结构不确定)。
举个?:假设 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 目录结构,这样就保证了安装依赖的确定性。
yarn
yarn 1
yarn1 的出现是为了解决 npm v3 的问题,那时候还没有 npm v5。yarn install 生成的 node_modules 目录结构与 npm v5 相同,同时默认会生成一个 yarn.lock
文件。只要 yarn 的版本相同,yarn 安装依赖的确定性就能保证。
同前面的例子一样,我们使用 yarn 安装 swiper 生成的 yarn.lock 文件如下:
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
dom7@^4.0.4:
version "4.0.4"
resolved "https://registry.npmjs.org/dom7/-/dom7-4.0.4.tgz#8b68c5d8e5e2ed0fddb1cb93e433bc9060c8f3fb"
integrity sha512-DSSgBzQ4rJWQp1u6o+3FVwMNnT5bzQbMb+o31TjYYeRi05uAcpF8koxdfzeoe5ElzPmua7W7N28YJhF7iEKqIw==
dependencies:
ssr-window "^4.0.0"
ssr-window@^4.0.0, ssr-window@^4.0.2:
version "4.0.2"
resolved "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz#dc6b3ee37be86ac0e3ddc60030f7b3bc9b8553be"
integrity sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==
swiper@^8.0.7:
version "8.0.7"
resolved "https://registry.npmjs.org/swiper/-/swiper-8.0.7.tgz#9eefe26c703e627a6dc7237c0109e172ce06e3f6"
integrity sha512-GHjDfxSZdupfU7LrSVOpaNaT7R1D2zxopPGBFz1UOXOtsYvVJLg0k6NvkTAD7qn0ASl5pTti82qoYwvYvIkg4g==
dependencies:
dom7 "^4.0.4"
ssr-window "^4.0.2"
复制代码
对比一下 yarn.lock 文件和 package-lock.json 文件,我们可以发现一下几点不同:
-
package-lock.json 与 yarn.lock 格式上有差异。
-
npm v5 中只需要 package-lock.json 就可以保正确的 node_modules 目录结构,而 yarn 需要同时拥有 yarn.lock 文件和 package.json文件。(可参考Yarn 的确定性)
在使用 yarn 作为包管理工具时,我们也需要主要以下几点:
- yarn.lock 是自动生成的,不要手动修改它
- 将 yarn.lock 文件上传到 git
- 升级依赖时,使用
yarn upgrade
命令,不要手动修改 package.json 和 yarn.lock 文件 - 不得以不要把 lock 文件删掉,整个重装。这样会造成原本锁住的版本都放开了,执行
yarn install
的时候会根据 package.json 里定义的版本区间去找最新版,可能会造成你预期外的依赖也被更新了, 有可能会引入 bug。
yarn2
yarn2 版本是无 node_modules 模式,可以加快项目安装速度,同时大大缩减删除一整个项目的速度。[这里顺带提一嘴,不过多介绍 ?]
npm install -g yarn@berry
复制代码
pnpm
pnpm(perfomance npm) 现代包管理工具,其性能上有很大的提高。
基本使用
npm install -g pnpm // 全局安装 pnpm
pnpm add axios // 添加至dependencies
pnpm add axios -D // 添加至devDependencies
pnpm add -O [package] //保存到optionalDependencies
pnpm update // 更新
pnpm remove/uninstall // 删除
pnpm dlx // 从源中获取包而不将其安装为依赖项,热加载,并运行它公开的任何默认命令二进制文件。
pnpm link // 将本地项目连接到另一个项目,这里是硬连接。
复制代码
基本特性
- 本地安装包速度快: 相比于npm / yarn 快 2-3 倍。
- 磁盘空间利用高效: 及时版本不同也不会重复安装同一个包。
- 安全性高:避免了npm/yarn 非法访问依赖和二重身的风险
pnpm 是如何提升性能的?
一句话概括:pnpm 在安装依赖时使用了 hard link 机制,使得用户可以通过不同的路径去寻找某个文件。pnpm 会在全局的 store 目录下存储 node_modules 文件的 hard link。
下面先简单讲讲几个概念: hard link 、symlink 以及全局的 store 目录。
什么是 hard link 和 symlink
本质上都是文件访问的方式。
hard link(硬链接):如果 A 是 B 的硬链接,则 A 的 indexNode(可以理解为指针) 与 B 的 indexNode 指向的是同一个。删除其中任何一个都不会影响另外一个的访问。作用:允许一个文件拥有多个有效路径,这样用户可以避免误删。
symlink(软链接或符号链接symbolic link):类似于桌面快捷方式。比如 A 是 B 的软连接(A 和 B 都是文件名),A 和 B 的 indexNode 不相同,但 A 中只是存放这 B 的路径,访问 A 时,系统会自动找到 B。删掉 A 与 B 没有影响,相反删掉 B,A 依然存在,但它的指向是一个无效链接。
store 目录
store 目录一般在${os.homedir}/.pnpm-store/v3/files
这个目录下。 由于 pnpm 会在全局的 store 目录下存储 node_modules 文件的 hard link,这样在不同项目中安装同一个依赖的时候,不需要每次都去下载,只需要安装一次就行,避免了二次安装的消耗。这点 npm 、yarn 在不同项目上使用,都需要重新下载安装。
store 目录也会随着安装的包的数量越来越大,使用 pnpm store prune
命令可以删除不再被引用的包。(不推荐频繁使用)
pnpm 网状+平铺的node_modules 结构
我们同样使用 pnpm 来安装一下 swiper 包,此时会自动生成一个 pnpm-lock.yaml 文件。接着我们来看看在 pnpm 中 node_modules 结构与 npm 和 yarn 有什么不同。
安装 swiper 包后,根 node_modules 下会存在两个目录, 一个是 .pnpm 虚拟磁盘目录,用户不能直接从中 require;另一个 swiper 目录,正常node require 的路径, 这个 swiper 我们称之为 swiper 的软链 。当 node 解析依赖时,会通过这个软链来找到 swiper 的真实位置,swiper 真实的位置在 .pnpm/swiper@8.0.7/node_modules/swiper 下,这个文件称为 swiper 的硬链,会真实的链接到全局的 store 中。
由于兼容性问题,没有使用 symlink 代替 hard link 。实际上存在 store 目录里面的依赖也是可以通过软链接去找到的,node.js 本身提供了一个 –preserve-symlinks 的参数来支持 symlink ,但实际这个参数对应 symlink 的支持并不好,所以作者放弃了。
npm 与 yarn 的 共性问题
npm 与 yarn 在安装依赖虽然也实现了包打平,但还是存在两个问题:phatom 与 doppelgangers 。
- phatom (非法访问依赖):package.json 中只声明了 A, A 的 depdencies 有 B, 这样安装在 A 时 B 也会被安装,项目中还是可以 require 到 B。
- doppelgangers (二重身): 一个包的不同版本还是会重复安装(不能打平同一个包的不同版本),能会造成同一个包重复安装,性能还是会损失。
pnpm 中不会出现这两种情况,首先依赖的打平是在 .pnpm 的 node_modules 中,而.pnpm 是一个虚拟的磁盘目录,用户不能 require 到;其次pnpm 安装的依赖始终都是存在全局 store 目录下的 hard links,一份不同的依赖始终都只会被安装一次。
pnpm 独有的特点
pnpm 除了能解决上述 npm 与 yarn 的 共性问题外,其独有的特点如下。
- 管理 Node.js 版本:在 .npmrc 文件下,可配置运行项目的 node 版本。 pnpm 将自动安装指定版本的 Node.js 并将其用于执行
pnpm run
命令或pnpm node
命令。(可参考npmrc) - 内容可寻址存储(CAS):是一种存储机制,其中为固定数据分配了硬盘上的永久位置,并使用唯一的内容名称,标识符或地址进行寻址。
pnpm 的 Monorepo 策略
pnpm 对 Monorepo
也有很好的支持。那么如何使用 pnpm 来构建 Monorepo 项目?
-
全局配置
pnpm-workspace.yaml
packages: # all packages in subdirs of packages/ and components/ - 'packages/**' 复制代码
-
使用 pnpm 安装全局公用的包
pnpm install react react-dom -w // 根目录 pnpm i dayjs -r --filter packageName // 安装在指定的 packages下 复制代码
-
-w 将包安装在根目录下的 node_modules
-
-r 将包安装在指定 package 下,并需要配合指定的参数 –filter, 后面接 package 的name
-