写点什么

从 npm 发展历程看 pnpm 的高效

作者:虎妞先生
  • 2023-02-10
    北京
  • 本文字数:5630 字

    阅读完需:约 18 分钟

从npm发展历程看pnpm的高效

概述

pnpm - 速度快、节省磁盘空间的软件包管理器


perfomance npm ,即 pnpm (高性能 npm)

优势

  • 快速

  • pnpm 是同类工具速度的将近 2 倍

  • 高效

  • node_modules 中的所有文件均链接自单一存储位置

  • 支持 monorepos

  • pnpm 内置了对单个源码仓库中包含多个软件包的支持


注:这个东西这么读 monorepos = Monolithic repository /ˌmänəˈliTHik/ /rəˈpäzəˌtôrē/


  • 严格

  • pnpm 创建的 node_modules 默认并非扁平结构,因此代码无法对任意软件包进行访问


以上是 4 条优势是官网的说明和宣传,后面我们会针对 npm 的发展历史中存在的问题


来对比说明,pnpm 的提出动机,pnpm 的优势在哪里,为什么具备这些优势。

npm

npm 全称,Node Package Manager node 包管理工具


执行 npm install 之后。npm 帮我们下载对应的依赖包并解压到本地缓存,然后构造 node_modules 目录结构,写入依赖文件,对应的 node_modules 内部结构也经历了几个版本的变化。

npm v1/v2 嵌套依赖

最开始其实没有注重 npm 包的管理,只是简单的嵌套依赖,这种方式层级依赖结构清晰


但是随着 npm 包的增多,项目的迭代扩展,重复包越下载越多,造成了空间浪费,导致前端本地项目 node_modules 动辄上百 M


在业务开发中,安装几个项目,项目体积好几 G,对使用者们极其不友好。


入下图所示,依赖包 C 在 AB 中都被引用了, 被重复下载了两次,其实是两个完全相同的东西。


从我们现在的角度看,完全没有必要。


npm v3 扁平化

node_modules 体积过大,嵌套过深


npm 团队也意识到这个问题,通过扁平化的方式,将子依赖安装到了主依赖所在项目中,以减少依赖嵌套太深,和重复下载安装的问题。


如下图所示,A 的依赖项 C 被提升到了顶层,如果后续有安装包,也依赖 C,会去上一级的 node_modules 查找,如果有相同版本的包,则不会再去重复下载,直接从上一层拿到需要的依赖包 C


说明:为什么自己的 node_modules 没有 C,也能在上层访问到 C 呢?


require 寻找第三方包,会每层级依次去寻找 node_modules,所以即便本层级没有 node_moudles,上层有,也能找到



扁平化方式解决了相同包重复安装的问题,也一定程度上解决了依赖层级太深的问题。


为什么说是一定程度上?


因为如上图所示,B 依赖的 C v2.0.0,并没有提升,依然是嵌套依赖。


因为在两个依赖包 C 的版本号不一致,只能保证一个在顶层,上图所示 C v1.0.0 被提升了,v2.0.0 没有被提升,后续 v2.0.0 还是会被重复下载,所以当出现多重依赖时,依然会出现重复安装的问题。\


而且这个提升的顺序,也不是根据使用量优先提升,而是根据先来先服务原则,先安装的先提升。这会导致不确定性问题,随着项目迭代,npm i 之后得到的 node_modules 目录结构,有可能不一样。


与此同时,我们把 C,提升到了顶层,即使项目 package.json,没有声明过 C,但是也可以在项目中引用到 C,这就是幽灵依赖问题。




可以说 npm v3 在解决嵌套依赖,重复安装问题的同时,又带来了新的问题。

npm v5 lock

npm v5 借鉴 yarn 的思想,新增了 package-lock.json


该文件里面记录了 package.json 依赖的模块,以及模块的子依赖。并且给每个依赖标明了版本、获取地址和验证模块完整性哈希值。


通过 package-lock.json,保障了依赖包安装的确定性与兼容性,使得每次安装都会出现相同的结果。


这个就解决了不确定性的问题

package-lock.json 文件字段说明


  • name:项目的名称;

  • version:项目的版本;

  • lockfileVersion:lock 文件的版本;

  • requires:使用 requires 来跟踪模块的依赖关系;

  • dependencies:项目的依赖


  • version 表示实际安装的版本;

  • resolved 用来记录下载的地址,registry 仓库中的位置;

  • requires 记录当前模块的依赖;

  • integrity 用来从缓存中获取索引,再通过索引去获取压缩包文件

npm install 过程

至此我们也可以顺带总结一下 npm install 的全过程


npm install 先检测是有 package-lock.json 文件:


  • 没有 package-lock.json 文件

  • 分析依赖关系,这是因为我们可能包会依赖其他的包,并且多个包之间会产生相同依赖的情况;

  • 从 registry 仓库中下载压缩包(如果我们设置了镜像,那么会从镜像服务器下载压缩包);

  • 获取到压缩包后会对压缩包进行缓存(从 npm5 开始有的, npm config get cache 可以查看地址)

  • 将压缩包解压到项目的 node_modules 文件夹中

  • 有 package-lock.json 文件

  • 检测 lock 中包的版本是否和 package.json 中一致

  • 不一致,那么会重新构建依赖关系,直接会走上面的流程;

  • 一致的情况下,会去优先查找缓存

  • 缓存没有找到,从 registry 仓库下载,直接走上面流程;

  • 命中缓存会获取缓存中的压缩文件

  • 将压缩文件解压到 node_modules 文件夹中;


pnpm

综上,基于 npm 扁平化 node_modules 的结构下,虽然解决了依赖嵌套、重复安装的问题,但多重依赖和幽灵依赖并没有好的解决方式。


pnpm 出现就是为了解决现在 npm 存在的问题,正如官网 pnpm 所形容自己的是一款速度快,节省磁盘空间的软件包管理器。


前置知识 软链接 &硬链接

简单理解

硬链接就是多个文件名指向了同一个文件,这多个文件互为硬链接。


像是 JS 中的两个相同的对象,a 和 b 的真实内容指向堆中同一个地址,修改一个,同时改变,一荣俱荣,一损俱损。删除一个,并不影响另一个。


let a = {test:1} let b = aa.test = 2console.log(b) // {test:2}
复制代码


软链接就是快捷方式,是一个单独文件。


就像我们电脑桌面上的快捷方式,大小只有几字节,指向源文件,点击快捷方式,其实执行的就是源文件。

专业理解

在 Linux 的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。A 是 B 的硬链接(A 和 B 都是文件名)则 A 文件中的 inode 节点号与 B 文件的 inode 节点号相同,即一个 inode 节点对应两个不同的文件名,两个文件名指向同一个文件,


软硬链接 是 linux 中解决文件的共享使用问题的两个方式,目的也是为了节省磁盘空间。


大家可以去网上找找专业教程,或者报名山月的 linux 训练营,这里就不展开说了。

node_modules 的层级结构

比如某项目中,package.json 里声明了 A 和 B,


A 的 package.json 里声明了 C v1.0.0,B 的 package.json 里声明了 C v2.0.0



进行 pnpm i 之后,node_modules 的层级结构如下


双键头代表硬链接


单箭头代表软链接


node_modules|_ A -> .pnpm/A@1.0.0/node_modules/A|_ B -> .pnpm/B@1.0.0/node_modules/B|_ .pnpm  |_ A@1.0.0    |_ node_modules      |_ A => pnpm/store/A       |_ C -> ../../C@1.0.0/node_modules/C  |_ B@1.0.0    |_ node_modules      |_ B => pnpm/store/B       |_ C -> ../../C@2.0.0/node_modules/C  |_ C@1.0.0    |_ node_modules      |_ C => pnpm/store/C   |_ C@2.0.0    |_ node_modules      |_ C => pnpm/store/C
复制代码


以 A 包为例,A 的目录下并没有 node_modules,是一个软链接,真正的文件位于 .pnpm/A@1.0.0/node_modules/A 并硬链接到全局 store 中。


A 和 B 是我们在项目 package.json 中声明的依赖包,node_modules 除了 A,B 没有其他包,说明不是扁平化结构。也就不存在 幽灵依赖的问题


.pnpm 中存放着所有的包。最终硬链接指向指向全局 pnpm 仓库里的 store 目录下。


也就是说,我们所有的包,最终都以硬链接的形式,最终都在全局 pnpm/store 中,可以使得不同的项目从全局 store 寻找到同一个依赖,大大节省了磁盘空间


如果上面这个文件列表不够直观,大家也可以看我参考官网画的结构图


生产验证

全局安装 brew install pnpm


以我自己基于 vue-cli 封装的一个移动端项目 vue-template 为例


github 地址如下


基于vue-cli二次封装的移动端框架,vue3 +vue-cli4 + webpack5 + 多入口打包 + 自动生成项目模版 + pinia + 数据持久化 + 路由动画 + axios二次封装

npm i 之后

查看 node_modules 体积 293M


du -h -s node_modules293M node_modules
复制代码


查看 package-lcok.json 中重复文件,以 postcss 为例,一眼就看到了两个版本的 postcss 版本,


查看 node_modules 只有一个版本的 postcss 包会被提升,其他版本的就会被重复下载


pnpm i 之后

查看 node_modules 体积 251M


du -h -s node_modules\251M node_modules
复制代码


切换到 node_modules 目录下,查看所有文件信息


cd node_modulesls -alh
复制代码


以 axios 库为例,只有 37B,只是一个快捷方式,axios 软链接指向 .pnpm/axios@0.26.1/node_modules/axios



切换到.pnpm 目录下,查看所有文件信息


cd .pnpmls -alh
复制代码


我们看到 postcss 三个版本文件夹,说明现在项目里依赖三个版本的 postcss



切换到 postcss@7.0.39 目录,查看文件信息


cd postcss@7.0.39/node_modules/postcssstat -x package.json
复制代码


值得关注的属性有两个,一个是 Links,表示硬链接个数,一个是 Inode



我们可以通过 Inode 去查询所有的硬链接


find . -inum 8177610
复制代码


可以看到,在全局 Library/pnpm/store/下对应的文件目录


4 条记录 也对应了 links:4


对比

对比发现,当一个项目时,两者差距不大。


举一个极端的例子,当有 10 个相同项目时,npm 的 node_modules 将达到 2930M,将近 3 个 G,而 pnpm 依旧能保持 全局 253M 的体积,此时优势已经很明显了。


我们在业务开发时,其实一般都通用的模版,所以项目的依赖基本上一致,我觉得 pnpm 还是非常好的。

全局安装目录 pnpm-store 的目录结构

pnpm└── store    └── v3        └── files            ├── 00              - cd3e571524c095736              - 02a74db92f0368580            ├── 01            ├── 02
复制代码


上图是我们全局目录下 pnpm 的目录结构。


我们在全局目录里存放的不是 npm 包的源码,而是 hash 文件,这里采用了基于文件内容寻址方案。


简单来说就是文件内容被加密成了 64 位 hash 值,hash 值都是唯一的,如果文件内容不变,hash 值也不会变。


这个非常适合 npm 的安装包,一般来说,依赖包的更新都是向下兼容的,两个版本的包差别只是部分,而我们使用 hash 存储,会根据文件内容变化,只会存储变化的部分,相同的部分,生成的 hash 不会变,只存储一份就够了,一定程度上,也节省了磁盘空间。

pnpm 弊端

调试问题

所有项目引用的包都在全局一个地方,如果想对某个包进行调试,其他项目正好引用了,本地运行也会收到影响。

兼容问题

symlink 即软连接的方式可能会在 windows 存在一些兼容的问题,但是针对这个问题,pnpm 也提供了对应的解决方案:在 win 系统上使用一个叫做 junctions 的特性来替代软连接,这个方案在 window 上的兼容性要好于 symlink


我没有 windows 电脑,没有实验过,这条是从官网挪过来了。


我理解的是 window 下也是可以使用的,pnpm 已经帮我们做了兼容,只是没有使用软链接的方案。

pnpm 常见问题

为什么使用硬链接? 为什么不直接创建到全局存储的软链接?

这个问题非常复杂,说来话长,我一点点分析,我花了很多功夫在这个问题上,目前也没有答案,和大家分享一下我的调研结果。

首先,pnpm 官网如此解释

直接软链至全局存储与 Node 的 --preserve-symlinks 标志一起使用是可行的,但是,该方法附带了个自己的问题,因此我们决定使用硬链接。


大意就是可以做,但我们不想,因为会引发新的问题。

require 直接引入软链接

软链接的文件中,使用 require 直接引用的包会报错,软链接会从文件原始位置开始查找依赖。


我们希望的是软链可以将其他地方的目录增加到依赖查找路径中。


有兴趣可以去看 github 关于软链接引用报错的讨论,这时已经有人提出使用硬链接https://github.com/nodejs/node/issues/3402


我们实验一下


如下图,建立两个文件夹 a,b



a/index.js 中写入,b 中安装 qs 库


const test = require('qs')console.log(test)
复制代码


b 中建立 index.js 的软链接 index-s.js


执行 node index-s.js 发现找不到模块


因为软链接中的 require 软链接会从文件原始位置开始查找依赖,a 中没有 node_modules,直接报错了,但是如果是硬链接则不存在这样的问题


--preserve-symlinks

最后 node 官方,增加了--preserve-symlinks来专门处理软链接的引用路径问题。


Node.js 有这样一个选项:–preserve-symlinks,可以设置成按照软链所在的位置查找依赖。

新的问题

–preserve-symlinks 会引发新的问题,但是我查阅了 github 的 issues,有好几百条的讨论,没有看到有详细解释清楚这个问题的,我现在大概的理解就是 node 官方对软链接支持的不够好,即使提出了–preserve-symlinks,也有问题,所以 pnpm 团队不用了。


有兴趣可以看看老外们的讨论

https://github.com/npm/npm/issues/15133


后来,我在 node.js 中文文档里找到着这么一句,但是自己没有验证


使用 --preserve-symlinks 会有其他方面的影响。 比如,如果符号连接的原生模块在依赖树里来自超过一个位置,它们会加载失败。 (Node.js 会将它们视为两个独立的模块,且会试图多次加载模块,造成抛出异常。)


https://www.nodeapp.cn/cli.html#cli_preserve_symlinks


最终作者抛弃了这个方案

总结

最后我们再翻译翻译,pnpm 官网的这些话


节省磁盘空间


pnpm 通过 hard link(硬连接) 机制,把包都存储在全局的 pnpm/store/目录下。当安装软件包时,其包含的所有文件都会硬链接自此位置,而不会占用额外的硬盘空间。pnpm 对于同一个包不同的版本也仅存储其增量改动的部分。


快速


安装包之前,如果已经在全局安装过,就不会被再次下载了,节省了安装时间。随着项目增多,效果会越来越明显。


支持单体仓库


pnpm 提供工作空间 workspace 能力,就是保证一个仓库内多个项目的 package.json 有自己生效的范围。这个 yarn npm 也支持,不算 pnpm 的突出点。我对 monorepos 也没有研究过,这块等后续有时间了,可以对比三个工具的 workspace 专题讨论。


严格


pnpm 默认创建了一个非扁平化的 node_modules,因此代码无法访问未声明的包,解决了 npm 存在的幽灵依赖问题。

待研究的问题

  • pnpm-lock.yaml 文件里的属性和生成过程

  • pnpm 对 peerDependencies 的处理

  • 老项目使用 yarn 或者 npm 如何迁移

  • pnpm npm yarn 工作空间 workspace 的研究

  • Java 的 meavns 是怎么管理依赖包的,和前端有什么区别

参考链接

pnpm 官网 https://pnpm.io/zh/


软链、硬链对 Node.js 包寻找的影响 https://meixg.cn/2021/01/25/ln-nodejs/#软链依赖目录


前端包管理工具 npm yarn cnpm npx https://juejin.cn/post/7102442481920425997


pnpm 有什么优势 https://q.shanyue.tech/engineering/751.html#pnpm

发布于: 刚刚阅读数: 3
用户头像

虎妞先生

关注

还未添加个人签名 2017-12-22 加入

还未添加个人简介

评论

发布
暂无评论
从npm发展历程看pnpm的高效_npm_虎妞先生_InfoQ写作社区