浅析包管理工具对node_modules依赖处理的演进历史
一、npm 演进
npm 从 v1 -v3- v5 版本的迭代都有重大的改变,一起来下看吧~。
1、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 就会被装两份。
2、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
中的位置。
3、npm v5 扁平 + lock
为了解决 node_modules 结构的不确定性,于是在 v5 版本中默认会生成 package-lock.json
文件 。
package-lock.json 文件可以帮我们记录安装的每一个包版本和其所依赖的其他包版本,这样在下一次安装的时候就可以通过这个文件来安装。由 package-lock.json 文件和 package.json 文件能确保始终得到一致的 node_modules 目录结构,这样就保证了安装依赖的确定性。
二、yarn
1、yarn 1
yarn1 的出现是为了解决 npm v3 的问题,那时候还没有 npm v5。yarn install 生成的 node_modules 目录结构与 npm v5 相同,同时默认会生成一个 yarn.lock文件。只要 yarn 的版本相同,yarn 安装依赖的确定性就能保证。
npm v5 中只需要 package-lock.json 就可以保正确的 node_modules 目录结构,而 yarn 需要同时拥有 yarn.lock 文件和 package.json文件
在使用 yarn 作为包管理工具时,我们也需要主要以下几点:
- yarn.lock 是自动生成的,不要手动修改它
- 将 yarn.lock 文件上传到 git
- 升级依赖时,使用yarn upgrade命令,不要手动修改 package.json 和 yarn.lock 文件
- 不得以不要把 lock 文件删掉,整个重装。这样会造成原本锁住的版本都放开了,执行yarn install的时候会根据 package.json 里定义的版本区间去找最新版,可能会造成你预期外的依赖也被更新了, 有可能会引入 bug。
2、yarn2 版本是无 node_modules 模式,可以加快项目安装速度,同时大大缩减删除一整个项目的速度
三、pnpm
pnpm(perfomance npm) 现代包管理工具,其性能上有很大的提高
1、基本使用
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 // 将本地项目连接到另一个项目,这里是硬连接。
2、基本特性
- 本地安装包速度快: 相比于npm / yarn 快 2-3 倍
- 磁盘空间利用高效: 不会重复安装同一个包
- 安全性高:避免了npm/yarn 非法访问依赖即幽灵依赖和二重身的风险
四、pnpm 是如何提升性能的?
一句话概括:pnpm 在安装依赖时使用了 hard link 机制,使得用户可以通过不同的路径去寻找某个文件。pnpm 会在全局的 store 目录下存储 node_modules 文件的 hard link。
下面先简单讲讲几个概念: hard link 、symlink 以及全局的 store 目录。
1、什么是 hard link 和 symlink
本质上都是文件访问的方式。
hard link(硬链接):如果 A 是 B 的硬链接,则 A 的 indexNode(可以理解为指针) 与 B 的 indexNode 指向的是同一个。删除其中任何一个都不会影响另外一个的访问。作用是:允许一个文件拥有多个有效路径,这样用户可以避免误删。
symlink(软链接或符号链接):类似于桌面快捷方式。比如 A 是 B 的软连接(A 和 B 都是文件名),A 和 B 的 indexNode 不相同,但 A 中只是存放这 B 的路径,访问 A 时,系统会自动找到 B。删掉 A 与 B 没有影响,相反删掉 B,A 依然存在,但它的指向是一个无效链接。
2、store 目录
store 目录一般在${os.homedir}/.pnpm-store/v3/files 这个目录下。 由于 pnpm 会在全局的 store 目录下存储 node_modules 文件的 hard link,这样在不同项目中安装同一个依赖的时候,不需要每次都去下载,只需要安装一次就行,避免了二次安装的消耗。这点 npm 、yarn 在不同项目上使用,都需要重新下载安装。
store 目录也会随着安装的包的数量越来越大,使用 pnpm store prune
命令可以删除不再被引用的包。(不推荐频繁使用)
3、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 的支持并不好,所以作者放弃了。
4、解决了 npm 与 yarn 的 共性问题
npm 与 yarn 在安装依赖虽然也实现了包打平,但还是存在两个问题:phatom 与 doppelgangers
(1)phatom (非法访问依赖):package.json 中只声明了 A, A 的 depdencies 有 B, 这样安装在 A 时 B 也会被安装,项目中还是可以 require 到 B。
(2)doppelgangers (二重身): 一个包的不同版本还是会重复安装(不能打平同一个包的不同版本),能会造成同一个包重复安装,性能还是会损失。
pnpm 中不会出现这两种情况:
首先依赖的打平是在 .pnpm 的 node_modules 中,而.pnpm 是一个虚拟的磁盘目录,用户不能 require 到;
其次 pnpm 安装的依赖始终都是存在全局 store 目录下的 hard links,一份不同的依赖始终都只会被安装一次。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步