浅析高性能包管理工具 Pnpm

一、前言

最近团队正在进行 C 端主项目的 Vue3 升级工作,在升级 Vue3 之后,项目依赖的各种第三方包大概有十多个有版本变更,package.json 和 Vue2 版本有很大的差异。

同时,也有在做 Vue2 版本主项目的其他迭代需求。

这样问题就来了,常常是自己很专注的研究 Vue3 升级的变动点时,后端同学突然找过来,需要配合改动一个字段 balabala...

... okok,改呗,毕竟迭代需求的优先级高于技改。

于是:

  • 先暂存代码,切换到 feature/dev-xxx 分支
  • 删除依赖:rm -rf node_modules 【耗时 15s】
  • npm 安装依赖:npm install 【下午最新实测,耗时 4min !!!】
  • 改完之后,再切换回 Vue3 升级分支,重复上述操作【耗时 4min 15s】

这样来回切换一次,宝贵的 10min 浪费了,十分影响效率,于是想起来了 20 年就听过的 pnpm,一直没有机会实践,这次刚好实践一下!

  • 第一次使用 pnpm 安装,由于大部分的包都已经下载过,安装起来十分速度, 【耗时 44s】
  • 把 node_modules 删除之后,再次用 pnpm 安装, 【只耗时 4.8s】!!
  • 这个速度的提升就十分明显了!

二、包管理器的依赖处理简史

整个依赖处理的历史可以划分为两个阶段,第一个阶段就是 npm 的 v3 版本发布之前,第二个阶段就是 npm 的 v3 版本发布之后。

2.1 Stage1:Before npm@3 【16-17年】

  • 依赖处理方式: 嵌套 node_modules
  • 🌰 例子 :
    • 使用 nvm 切换 node 版本至 4.x,对应 npm 版本为 2.x,在空项目中安装 express
    • 可以看到随便展开一个 node_modules 嵌套都有 4 层...
  • 造成的问题:

⚠️ 问题

  1. 过长路径限制: 在 windows 下,创建太深的依赖树,可能会让文件路径超过 windows 文件路径最大长度限制(260 字符)。
  2. 磁盘空间浪费: 当多个包间有公共的依赖时,嵌套 node_modules 会导致同样的依赖被复制很多次,占据比较大的磁盘空间。

2.2 Stage2:After npm@3 or yarn

  • 依赖处理方式: 扁平 node_modules
  • 🌰 例子 :
    • 同上,将 node_modules 删除,使用 yarn 安装 express
    • 可以看到,node_modules 下的包全都平铺开来,大部分文件下是没有 node_modules 了

可是为何我明明就装个 express ,为什么 node_modules 里面多了这么多东西?

是因为 yarn / npm 会将所有的依赖都被提升到node_modules目录下,不再有很深层次的嵌套关系。

这样在安装新的包时,根据 node require 机制,会不停往上级的node_modules当中去找,如果找到相同版本的包就不会重新安装,解决了大量包重复安装的问题,而且依赖层级也不会太深。

    • 但是某些文件下还是有 node_modules 的,例如:当遇到 A 包引用了 C@1.x,而 B 包引用了 C@2.x,由于只能提升一个版本, 故 B 包内依然还是用嵌套的方式
    • 举个例子🌰
    • 假如现在项目依赖两个包 foo 和 bar,这两个包的依赖又是这样的:
    • 那么 npm/yarn install 的时候,通过扁平化处理之后,究竟是这样
    • 还是这样?
    • 答案是: 都有可能。取决于 foo 和 bar 在 package.json中的位置,如果 foo 声明在前面,那么就是前面的结构,否则是后面的结构。
    • 这就是为什么会产生依赖结构的不确定问题,也是 lock 文件诞生的原因,无论是 package-lock.json (npm 5.x才出现),还是 yarn.lock,都是为了保证 install 之后都产生确定的 node_modules 结构。
  • 造成的问题:

⚠️ 问题

  1. 幽灵依赖: 没有在 dependencies 里声明的依赖,代码中却却可以 require 进来。
    【这样是有隐患的,因为没有显式依赖,万一有一天别的包不依赖这个包了,那你的代码也就不能跑了,因为你依赖这个包,但是现在不会被安装了】
  2. 依然存在磁盘空间浪费问题: 当遇到 A 包引用了 C@1.x,而 B 包引用了 C@2.x,由于只能提升一个版本,那其余版本的包还是复制了很多次,依然存在空间浪费问题;
    【同时提升的依赖不确定,取决于安装顺序,所以后续出了 yarn.lock 和 package-lock.json 来解决这个问题】
  3. 复杂且慢:扁平化算法相当复杂,执行速度较慢

2.3 Pnpm

  • 依赖处理方式:依赖包 ---(软链接)--- > .pnpm ----(硬链接) ---> 全局的 Store
  • 🌰 例子 :
    • 同上,将 node_modules 删除,使用 pnpm 来安装
    • 打开 node_modules 可以看到,确实不是扁平化的了,依赖了 express,那 node_modules 下就只有 express,没有幽灵依赖
    • 同时下面还有个 .pnpm 文件夹,展开 .pnpm 后可以看到,所有的依赖都在这里铺平了
    • 所有的依赖都是从全局 store 硬连接到了 node_modules/.pnpm 下,然后包和包之间的依赖关系是通过软链接组织的
  • 原理: 依赖包 ---(软链接)--- > .pnpm ----(硬链接) ---> Store
    • node_modules下除了package.json中的依赖,还有一个.pnpm,所有的依赖包在.pnpm是平级结构,命名形式:包名@版本号
    • .pnpm是个一个虚拟store(Virtual store),里面的依赖包 硬链接 到真实Store(Content-addressable store)中,真实Store才是依赖包文件真正的存储位置
    • package.json中的依赖(比如express)通过 软链接 ,指向.pnpm下对应的依赖包
    • 每次pnpm安装先检查Store,如果已经存在,直接通过硬链接的形式连接到.pnpm;如果不存在,则先下载,然后再硬链接过来
  • 优点:

优点

  1. 节省磁盘空间: 一个包全局只保存一份,使用时通过软硬链接,可以节省大量磁盘空间。
  2. 速度快: 通过链接的方式而不是复制,速度自然快了很多。
  3. 没有幽灵依赖:根目录下的 node_modules 只有 package.json 中声明的包,不存在幽灵依赖

三、Pnpm 的基本使用

3.1 安装pnpm

首先,需要在系统中全局安装pnpm。可以使用以下命令进行安装:

npm install -g pnpm

3.2 初始化项目

在要使用pnpm管理的项目根目录下执行以下命令,初始化项目:

csharp 复制代码
pnpm init

这将创建一个新的package.json文件,用于管理项目的依赖包。

3.3 安装依赖包

使用以下命令来安装依赖包:

css 复制代码
pnpm install [package-name]

可以一次性安装多个依赖包,也可以通过在命令后面添加 --save 或 --save-dev 参数将依赖包添加到 package.json 文件中的 dependencies 或 devDependencies 字段中。

  • 常用的参数选项
    • -save-prod, -P:安装到 dependencies
    • -save-dev, -D:安装到 devDependencies
    • -save-optional, -O:安装到 optionalDependencies
    • -save-peer:安装到 peerDependenciesdevDependencies
    • -global:安装全局依赖。
    • -workspace:仅添加在 workspace 找到的依赖项。

3.4 运行项目

安装完依赖包后,可以使用以下命令来运行项目:

arduino 复制代码
pnpm run [script-name]

这里的script-name是在package.json文件中定义的脚本名称。

3.5 更新依赖包

当需要更新依赖包时,可以使用以下命令来更新:

css 复制代码
pnpm update [package-name]

3.6 删除依赖包

如果要删除某个已安装的依赖包,可以使用以下命令:

css 复制代码
pnpm uninstall [package-name]

3.7 查看已安装的依赖包

可以使用以下命令来查看已安装的依赖包列表:

bash 复制代码
pnpm ls

3.8 清理缓存

pnpm会在全局的.pnpm目录下缓存依赖包,如果需要清理缓存,可以使用以下命令:

pnpm cache clean

以上是pnpm的基本使用方法。

四、已有 npm / yarn 项目迁移到 pnpm

如果你已经有一个项目在使用 npm 或 yarn,那么你想迁移到 pnpm 上,应该怎么做?

可能会三下五除二,直接 pnpm install 开干,那样就丧失了原来 lock 文件记录的版本优势。最佳姿势是使用 pnpm import

pnpm import 命令用于通过其他软件包管理器的 lockfile 文件生成 pnpm-lock.yaml

arduino 复制代码
pnpm import

支持的源文件包括:

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

xxx项目 为例:

生成一个 pnpm-lock.yaml 文件:

yaml 复制代码
lockfileVersion: '6.0'

settings:
  autoInstallPeers: true
  excludeLinksFromLockfile: false

dependencies:
  '@antv/f2':
    specifier: 3.8.13
    version: 3.8.13
  '@hb/H5Auth':
    specifier: 1.18.1
    version: 1.18.1
  '@hb/ab-sdk':
    specifier: 2.3.11
    version: 2.3.11
  '@hb/chaos':
    specifier: 1.1.2
    version: 1.1.2
  '@hb/demon-auth':
    specifier: 0.0.16
    version: 0.0.16
  '@hb/demon-config':
    specifier: 1.0.38
    version: 1.0.38

pnpm 可谓是非常贴心了。

之后删除 node_modulespackage-lock.json,再用 pnpm install 安装依赖即可。

旧的项目这样安装完依赖,运行时仍有可能报错,缺失某个包,这是因为 pnpm 的包安装策略与 yarn、npm 都有所差别。这时缺哪个包,就安装哪个包,大部分情况就OK了。

五、文末补充 - 软硬链接的区别


💡 Linux链接概念

Linux链接分两种,一种被称为硬链接(Hard Link),另一种被称为符号链接(Symbolic Link)。默认情况下,ln命令产生硬链接。

  • 硬连接:【实际上是一个指向实际内存中的指针】 硬连接指通过索引节点来进行连接。在Linux的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。在Linux中,多个文件名指向同一索引节点是存在的。一般这种连接就是硬连接。硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止"误删"的功能。 其原因如上所述,因为对应该目录的索引节点有一个以上的连接。只删除一个连接并不影响索引节点本身和其它的连接,只有当最后一个连接被删除后,文件的数据块及目录的连接才会被释放。 也就是说,文件真正删除的条件是与之相关的所有硬连接文件均被删除。
  • 软连接:【类似于我们桌面的快捷方式】 另外一种连接称之为符号连接(Symbolic Link),也叫软连接。软链接文件有类似于Windows的快捷方式。它实际上是一个特殊的文件。在符号连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息。

参考文献:

[1] pnpm 官方文档:pnpm.io/zh/

[2] Why should we use pnpm?:www.kochan.io/nodejs/why-...

[3] 关于现代包管理器的深度思考...:juejin.cn/post/693204...

[4] pnpm 是凭什么对 npm 和 yarn 降维打击的:juejin.cn/post/712729...

[5] 都2022年了,pnpm快到碗里来:juejin.cn/post/705334...

相关推荐
GDAL11 小时前
npm入门教程1:npm简介
前端·npm·node.js
乐迁~1 天前
关于npm源的切换及相关操作
npm
GDAL2 天前
npm入门教程13:npm workspace功能
前端·npm·node.js
wumu_Love2 天前
npm 和 node 总结
前端·npm·node.js
J不A秃V头A2 天前
报错:npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。
前端·npm·node.js
GDAL2 天前
npm入门教程14:npm依赖管理
前端·npm·node.js
GDAL5 天前
npm入门教程8:缓存管理
前端·缓存·npm
GDAL5 天前
npm入门教程18:npm发布npm包
前端·npm·node.js
GDAL5 天前
npm入门教程9:npm配置
前端·npm·node.js
HOOLOO5 天前
Laravel/Sail 中修改npm源的问题
npm·php·laravel