npm、yarn、pnpm

npm、Yarn 和pnpm 是Node.js 的三种主流包管理器,主要区别在于依赖包的管理方式和性能优化:npm 是标准包管理器,Yarn 引入了缓存和锁文件提高速度和确定性,而pnpm 通过共享存储区和硬链接/符号链接技术,实现更快的安装速度、更小的磁盘占用和更严格的依赖管理,有效解决了幽灵依赖问题。

npm (Node Package Manager)

特性

Node.js 官方的标准包管理器,通过 package.json 文件管理项目依赖。

安装方式

早期版本使用串行下载,导致安装速度较慢,但后续版本有所优化。

依赖管理

npm v3 之前,node_modules 的目录结构是嵌套的。每个包的依赖会放在自己的 node_modules 子目录中,这样会导致同一个依赖在不同层级重复出现,路径可能会非常深(Windows 下甚至会出现 "路径过长" 错误)。下面这里 C@1.0.0 被安装了两次,浪费磁盘空间。

依赖包被平铺地存储在每个项目的 node_modules 目录中,导致重复存储占用大量磁盘空间。npm v3 开始引入 依赖扁平化(deduplication/flattening) 机制。当安装依赖时,npm 会尽可能把依赖提升到项目根目录的 node_modules 中,这样可以让多个包共享同一个依赖副本。减少了重复安装,缩短了文件路径,提高了安装速度。虽然比嵌套结构好,但平铺存储仍然有缺点。(1)磁盘空间浪费依然存在。每个项目仍然会在自己的 node_modules 中保存一份依赖。如果你有 10 个项目都用到了 lodash,硬盘上就有 10 份 lodash 副本。(2)依赖树不直观。node_modules 结构与 package.json 声明的依赖关系不一致。调试依赖问题时比较困难。(3)幽灵依赖(Phantom dependencies)子依赖被提升到顶层后,项目可以直接引用它,即使它没在 package.json 中声明。这会导致依赖关系不清晰,可能引发生产环境问题

javascript 复制代码
project-嵌套结构
├── node_modules
│   ├── A
│   │   ├── node_modules
│   │   │   ├── C@1.0.0
│   ├── B
│   │   ├── node_modules
│   │   │   ├── C@1.0.0
javascript 复制代码
project-扁平化结构
├── node_modules
│   ├── A
│   ├── B
│   ├── C@1.0.0  ← 被 A 和 B 共享
锁文件

引入 package-lock.json 来记录依赖版本,保证每次安装的确定性,但存在"幽灵依赖"问题。

Yarn

采用与npm 类似的方式进行依赖的扁平化管理,但也存在一定的重复存储和幽灵依赖问题。

特性

Facebook 开发的包管理器,旨在解决npm 的性能问题,提供更快的安装速度。

安装速度

通过并行下载和本地缓存,速度显著优于传统npm。

依赖管理

采用与npm 类似的方式进行依赖的扁平化管理,但也存在一定的重复存储和幽灵依赖问题。

锁文件

使用 yarn.lock 文件来保证依赖的确定性安装。

pnpm

● 特性:一种更高效的包管理器,通过独特的依赖管理机制提供诸多优势。

● 安装速度:利用硬链接和符号链接,在全局存储区共享依赖,避免了重复下载,速度更快。

● 磁盘空间:使用中心化的存储区管理所有依赖包,项目内的依赖通过链接指向存储区,大大节省了磁盘空间。

● 依赖管理:创建非扁平化的 node_modules 目录结构,通过符号链接组织依赖关系,有效解决了npm 和Yarn 存在的幽灵依赖问题。

● Monorepo 支持:由于其高效的依赖管理,pnpm 在Monorepo(单仓多项目)场景下表现尤为出色。

主要区别总结

● 依赖存储方式:npm 和Yarn 采用各自项目的 node_modules 目录进行依赖复制,而pnpm 则使用共享的全局存储区,并通过硬链接和符号链接来管理依赖。

● 磁盘占用:pnpm 极大地减少了磁盘空间占用,因为相同的依赖不会在每个项目中重复存储。

● 安装速度:pnpm 的共享存储和硬链接机制使其安装速度通常最快。

● 依赖管理:pnpm 的非扁平化 node_modules 结构解决了幽灵依赖问题,提供了更严格的依赖管理。

● 兼容性:pnpm 兼容npm 的 package-lock.json 和Yarn 的 yarn.lock 文件,方便迁移和集成。

🌟 pnpm 包依赖关系处理

  • pnpm 存储依赖的底层逻辑
    • 平时我们装依赖,文件会直接存在项目的 node_modules 里,但 pnpm 不这么干 ------ 它先把所有依赖(比如 foo 库、bar 库)统一存到一个 "公共仓库"(叫 "内容可寻址存储",你可以理解成 "全电脑共用的依赖仓库"),然后在项目的 node_modules 里,用 "硬链接" 把仓库里的文件 "借" 过来用。
    • 硬链接的好处:不是复制文件,只是建了个 "快捷方式" 指向仓库里的真实文件,所以不管多少项目用同一个依赖,都只占一份空间,超省内存。
  • 给每个依赖建立专属文件夹
    • pnpm 会在 node_modules 里先建一个 .pnpm 文件夹,然后给每个依赖(比如 foo@1.0.0、bar@1.0.0)单独建个子文件夹,每个子文件夹里再套一层 node_modules,最后用硬链接把 "公共仓库" 里的依赖文件拉进来。比如 bar@1.0.0 的文件夹里,bar 相关的 index.js、package.json,都是指向公共仓库的硬链接,不是复制的新文件。这么绕一层的原因很简单:
      • 让依赖自己能 "找到自己":比如 foo 库想读自己的 package.json,直接写 require('foo/package.json') 就能找到,不用乱找路径;
      • 避免 "循环报错":比如 A 依赖 B,B 又依赖 A,这么建文件夹能防止链接绕圈圈。
  • 用 "符号链接" 搭出 "依赖关系网"
    • 依赖的文件就位后,pnpm 再用 "符号链接"(就是咱们平时说的 "软链接",一个指向其他文件夹的快捷方式)把它们串起来,满足 "谁依赖谁" 的需求:
      • 因为 foo 依赖 bar,就给 foo@1.0.0 文件夹里的 node_modules 建个 bar 的软链接,指向 .pnpm 里的 bar@1.0.0 文件夹 ------ 这样 foo 要调用 bar 时,就能通过这个链接找到;
      • 因为 foo 是咱们项目直接用的依赖,再给根目录的 node_modules 建个 foo 的软链接,指向 .pnpm 里的 foo@1.0.0 文件夹 ------ 这样咱们写代码时,直接 import foo 就能用。
  • 这个结构的两大好处
    • 避免套娃:传统的依赖管理会把很多依赖 "提到" 根目录的 node_modules 里,导致有些没在 package.json 里声明的依赖,项目也能用上(相当于 "偷偷用别人的东西"),后续很容易出问题。
    • 幽灵依赖:而 pnpm 默认只让项目用 "明确声明过的依赖",只有你在 package.json 里写了的,才能通过链接找到 ------ 这样能避免很多 "明明本地能跑,线上跑不了" 的蠢错误。

有些第三方库本身写得不规范(比如它用了某个依赖,但没在自己的 package.json 里声明),如果按 strict 模式来,这些库会报错。所以 pnpm 默认加了个 "妥协方案":把所有依赖偷偷 "提" 到 .pnpm/node_modules 里,让这些不规矩的库也能找到依赖。如果想关掉这个妥协,把配置里的 hoist 设为 false 就行。

总结一下:pnpm 就是用 "硬链接省空间、软链接搭关系" 的方式,把 node_modules 管理得又省内存又规整,还能减少依赖混乱导致的错误,只是文件夹结构看起来绕了点,但跟 Node.js 的规则是完全兼容的。

相关推荐
天生我材必有用_吴用5 小时前
Vue3 + VitePress 搭建组件库文档平台(结合 Element Plus 与 Arco Design Vue)—— 超详细图文教程
前端
liu****5 小时前
基于websocket的多用户网页五子棋(八)
服务器·前端·javascript·数据库·c++·websocket·个人开发
San305 小时前
深入理解 JavaScript 函数:从基础到高阶应用
前端·javascript·node.js
ttyyttemo5 小时前
Column,rememberScrollState,记住滚动位置
前端
芒果茶叶6 小时前
并行SSR,SSR并行加载
前端·javascript·架构
vortex56 小时前
解决 Kali 中 Firefox 下载语言包和插件速度慢的问题:配置国内镜像加速
前端·firefox·腾讯云
修仙的人6 小时前
Rust + WebAssembly 实战!别再听说,学会使用!
前端·rust
maxine6 小时前
JS Entry和 HTML Entry
前端
用户63310776123666 小时前
Who is a Promise?
前端