惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
C
CERT Recently Published Vulnerability Notes
P
Proofpoint News Feed
Y
Y Combinator Blog
T
The Blog of Author Tim Ferriss
云风的 BLOG
云风的 BLOG
H
Help Net Security
Recorded Future
Recorded Future
The Register - Security
The Register - Security
F
Full Disclosure
N
Netflix TechBlog - Medium
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hackread – Cybersecurity News, Data Breaches, AI and More
爱范儿
爱范儿
Security Archives - TechRepublic
Security Archives - TechRepublic
Simon Willison's Weblog
Simon Willison's Weblog
Cisco Talos Blog
Cisco Talos Blog
I
InfoQ
T
Tenable Blog
T
Tor Project blog
人人都是产品经理
人人都是产品经理
D
DataBreaches.Net
NISL@THU
NISL@THU
Google DeepMind News
Google DeepMind News
博客园 - 叶小钗
B
Blog
V
V2EX
Jina AI
Jina AI
L
LangChain Blog
月光博客
月光博客
W
WeLiveSecurity
U
Unit 42
AWS News Blog
AWS News Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园 - 聂微东
V
Visual Studio Blog
A
Arctic Wolf
T
Tailwind CSS Blog
The Cloudflare Blog
SecWiki News
SecWiki News
S
SegmentFault 最新的问题
Hacker News - Newest:
Hacker News - Newest: "LLM"
宝玉的分享
宝玉的分享
MyScale Blog
MyScale Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Securelist
www.infosecurity-magazine.com
www.infosecurity-magazine.com
腾讯CDC
雷峰网
雷峰网

Xiaobin's Notes

Mac本地快速部署DeepSeek Electron的原理 如何让大语言模型输出JSON格式 webstorm 的 cpu 占用高 修改Joplin主题样式 ECMAScript 历代版本 React useEffect() Hook Vue 2.x 使用高德地图JS API 2.0加载起点终点路径轨迹 家庭用电插座 MacOS 14.4 引发Java 应用崩溃 重定向广告 Pyenv工具 ElasticSearch集群原理 ElasticSearch集群节点 Elasticsearch Mapping 参数 Elasticsearch元数据 Elasticsearch的数据类型 Go语言的向后兼容和toolchain规则 Go 1.21 新增特性
新一代包管理器 PNPM
xbl · 2024-06-16 · via Xiaobin's Notes
  • 依赖管理的始末
    • npm2
    • npm3
    • 结论
  • PNPM
    • 依赖安装
    • 依赖管理原理
    • PNPM 机制
      • 基于软链接的 node_modules
    • PNPM 锁文件
  • 总结

依赖管理的始末

npm2

使用早期的 npm1/2 安装依赖,node_modules 文件夹会以递归的形式呈现,严格按照 package.json 结构以及次级依赖的 package.json 结构将依赖安装到它们各自的 node_modules 中,直到次级依赖不再依赖其它模块。

就像下面这样,tea-app 依赖 tea-component 作为次级依赖,tea-component 会安装到 tea-component 的 node_modules 里面:

1
2
3
4
5
6
7
8
node_modules
└─ tea-app
├─ index.js
├─ package.json
└─ node_modules
└─ tea-component
├─ index.js
└─ package.json

假设项目的中的两个依赖同时依赖了相同的次级依赖,那么它们二者的次级依赖将会被重复安装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
node_modules
├─ tea-app
│ ├─ index.js
│ ├─ package.json
│ └─ node_modules
│ └─ tea-component
│ ├─ index.js
│ └─ package.json
└─ tea-chart
├─ index.js
├─ package.json
└─ node_modules
└─ tea-component
├─ index.js
└─ package.json

这只是简单的例子,那如果 tea-component 还依赖别的包,别的包又依赖另外的包…… 在真实的开发场景中其问题还会更加恶劣:

  1. 依赖层级太深,会导致文件路径过长
  2. 重复的包被安装,导致 node_modules 文件体积巨大,占用过多的磁盘空间

npm3

npm3/yarn 开始,相比 npm1/2 项目依赖管理的方式有了很大的改变,不再是以往的“嵌套式”而是采用了“扁平化”方式去管理项目依赖。

这里继续拿上面的例子,tea-app 和 tea-chart 都依赖了 tea-component,依赖安装后呈现的是下面的这种扁平化目录:

1
2
3
4
5
6
7
8
9
10
node_modules
├─ tea-component
│ ├─ index.js
│ └─ package.json
├─ tea-app
│ ├─ index.js
│ └─ package.json
└─ tea-chart
├─ index.js
└─ package.json

扁平化的目录的确解决了上一小节暴露的一些问题,同时也暴露了新的问题:

  • Phantom dependencies

称为幽灵依赖,指的是在项目内引用未在 package.json 中定义的包。这个问题在 npm3 展现,因为早期的树形结构导致了依赖冗余和路径过深的问题,npm3 之后采用扁平化的结构,一些第三方包的次级依赖提升到了与第三方包同级。

一旦出现幽灵依赖的问题,可能会导致意想不到的错误,所以一定要正视:

  1. 不兼容的版本(例如某一个 api 进行了重大更新)

  2. 有可能会丢失依赖(某依赖不再依赖呈现在我们项目中的幽灵依赖)

    1
    2

    import { Button } from 'tea-component';
  3. NPM doppelgangers

称为分身依赖依赖的同名包都会被重复安装。

在实际开发中也会出现这样的情景,假设 tea-app、tea-form 依赖 tea-component@2.0.0,tea-chart 依赖 tea-component@3.0.0,这时候会造成依赖冲突,解决冲突的方式会将对应的冲突包放到对应依赖目录的 node_mudules 中,类似下面结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
node_modules
├─ tea-component@3.0.0
│ ├─ index.js
│ └─ package.json
├─ tea-app
│ ├─ index.js
│ ├─ package.json
│ └─ node_modules
│ └─ tea-component@2.0.0
│ ├─ index.js
│ └─ package.json
├─ tea-form
│ ├─ index.js
│ ├─ package.json
│ └─ node_modules
│ └─ tea-component@2.0.0
│ ├─ index.js
│ └─ package.json
└─ tea-chart
├─ index.js
└─ package.json

这时候会发现一个问题,tea-app、tea-form 的 node_modules 下都有重复且版本相同的 tea-component@2.0.0,这个问题就是我们正在所说的“分身依赖”的问题。这个问题就会导致 tea-app 中的 ConfigProvider 组件和 tea-form 的不是一个实例,无法生效。

常见的问题:

  1. 项目打包会将这些“重身”的依赖都进行打包,增加产物体积
  2. 无法共享库实例,引用的得到的是两个独立的实例
  3. 重复 TypeScript 类型,可能会造成类型冲突

结论

  1. 扁平化的 node_modules 结构允许访问没有在 package.json 中声明的依赖。
  2. 安装效率低,大量依赖被重复安装,磁盘空间占用高。
  3. 多个项目之间已经安装过的的包不能共享,每次都是重新安装。

PNPM

Fast, disk space efficient package manager (速度快、节省磁盘空间的软件包管理器)

当使用 npm 或 Yarn 时,如果你有 100 个项目,并且所有项目都有一个相同的依赖包,那么, 你在硬盘上就需要保存 100 份该相同依赖包的副本。然而,如果是使用 pnpm,依赖包将被 存放在一个统一的位置,因此:

如果你对同一依赖包需要使用不同的版本,则仅有 版本之间不同的文件会被存储起来。例如,如果某个依赖包包含 100 个文件,其发布了一个新 版本,并且新版本中只有一个文件有修改,则pnpm update只需要添加一个 新文件到存储中,而不会因为一个文件的修改而保存依赖包的 所有文件。

所有文件都保存在硬盘上的统一的位置。当安装软件包时, 其包含的所有文件都会硬链接自此位置,而不会占用 额外的硬盘空间。这让你可以在项目之间方便地共享相同版本的 依赖包。

最终结果就是以项目和依赖包的比例来看,你节省了大量的硬盘空间, 并且安装速度也大大提高了!

依赖安装

使用 pnpm 安装,pnpm 会将依赖存储在位于 .pnpm-store 目录下。只要你在同一机器下,下次安装依赖的时候 pnpm 会先检查 store 目录,如果有你需要的依赖则会通过一个硬链接到你的项目中去,而不是重新安装依赖。

依赖管理原理

pnpm 会将依赖存储在 store 目录下,通过符号链接的方式仅将项目的直接依赖项添加到 node_modules 的根目录下。

当使用 npm 或 yarn 安装依赖包时,所有软件包都将被提升到 node_modules 的 根目录下。其结果是,源码可以访问 本不属于当前项目所设定的依赖包。

PNPM 机制

如果 store 目录里面拥有即将需要下载的依赖,下载将会跳过,会向对应项目 node_modules 中去建立硬链接,并非去重新安装它。这里就表明为什么 pnpm 性能这么突出了,最大程度节省了时间消耗和磁盘空间。

基于软链接的 node_modules

pnpm 输出的 node_modules 与 npm/yarn 有很大的出入,并非是先者那样的“扁平化目录”而是“非扁平化目录”。

创建两个目录并分别运行 npm add express,pnpm add express。

这是使用 npm 安装 node_modules 的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.bin
accepts
array-flatten
body-parser
bytes
content-disposition
cookie-signature
cookie
debug
depd
destroy
ee-first
encodeurl
escape-html
etag
express

这个则是 pnpm 安装 node_modules 的结构:

1
2
3
.pnpm
.modules.yaml
express

打开 .pnpm 目录会发现这些依赖都被“扁平化”了,每个包都携带着自己的版本号。pnpm 这样设计的目的我理解其实是为了解决“分身依赖”的问题。

假设我们有这么一个情景,项目中依赖了 tea-app@1.0.0tea-chart@1.0.0tea-component@2.0.0。tea-chart 和 tea-app 依赖了 tea-component@1.0.0 那它引用关系是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
node_modules
├─tea-app -> ./.pnpm/tea-app@1.0.0/node_modules/tea-app
├─tea-chart -> ./.pnpm/tea-chart@1.0.0/node_modules/tea-chart
├─tea-component -> ./.pnpm/tea-component@2.0.0/node_modules/tea-component
└─.pnpm
├─ tea-app@1.0.0
│ └─ node_modules
│ ├─ tea-component -> ../tea-component@1.0.0/node_modules/tea-component
│ └─ tea-app -> <store>/tea-app
├─ tea-chart@1.0.0
│ └─ node_modules
│ ├─ tea-component -> ../tea-component@1.0.0/node_modules/tea-component
│ └─ tea-chart -> <store>/tea-chart
├─ tea-component@1.0.0
│ └─ node_modules
│ └─ tea-component -> <store>/tea-component@1.0.0
└─ tea-component@2.0.0
└─ node_modules
└─ tea-component -> <store>/tea-component@2.0.0

为什么需要通过软链接的方式去引用实际的依赖?

这样设计的目的是解决“幽灵依赖”的问题,只有声明过的依赖才会以软链接的形式出现在 node_modules 目录中。在实际项目中引用的是软链接,软链接指向的是 .pnpm 的真实依赖,所以在日常开发中不会引用到未在 package.json 声明的包。

PNPM 锁文件

pnpm 产出的是一个 pnpm-lock.yaml 格式的锁文件。

支持通过 pnpm import 从另一个包管理器的锁文件生成一个。支持的源文件:

  • package-lock.json
  • npm-shrinkwrap.json
  • yarn.lock

总结

npm2 是通过嵌套的方式管理 node_modules 的,会有同样的依赖复制多次的问题。

npm3+ 和 yarn 是通过铺平的扁平化的方式来管理 node_modules,解决了嵌套方式的部分问题,但是引入了幽灵依赖的问题,并且同名的包只会提升一个版本的,其余的版本依然会复制多次。

pnpm 则是用了另一种方式,不再是复制了,而是都从全局 store 硬连接到 node_modules/.pnpm,然后之间通过软链接来组织依赖关系。

这样不但节省磁盘空间,也没有幽灵依赖问题,安装速度还快,从机制上来说完胜 npm 和 yarn。