浅析高性能包管理工具 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/](https://link.juejin.cn?target=https%3A%2F%2Fpnpm.io%2Fzh%2F "https://pnpm.io/zh/") \[2\] Why should we use pnpm?:[www.kochan.io/nodejs/why-...](https://link.juejin.cn?target=https%3A%2F%2Fwww.kochan.io%2Fnodejs%2Fwhy-should-we-use-pnpm.html "https://www.kochan.io/nodejs/why-should-we-use-pnpm.html") \[3\] 关于现代包管理器的深度思考...:[juejin.cn/post/693204...](https://juejin.cn/post/6932046455733485575?searchId=20240119215654B7CC3E9364482574F510 "https://juejin.cn/post/6932046455733485575?searchId=20240119215654B7CC3E9364482574F510") \[4\] pnpm 是凭什么对 npm 和 yarn 降维打击的:[juejin.cn/post/712729...](https://juejin.cn/post/7127295203177676837?searchId=20240119215654B7CC3E9364482574F510 "https://juejin.cn/post/7127295203177676837?searchId=20240119215654B7CC3E9364482574F510") \[5\] 都2022年了,pnpm快到碗里来:[juejin.cn/post/705334...](https://juejin.cn/post/7053340250210795557?searchId=20240119215654B7CC3E9364482574F510 "https://juejin.cn/post/7053340250210795557?searchId=20240119215654B7CC3E9364482574F510")

相关推荐
2301_818732067 小时前
安装了node,但是cmd找不到node和npm,idea项目也运行失败 已解决
前端·npm·node.js
Sapphire~9 小时前
odoo-087 安装 npm (node ok npm not)
linux·运维·npm
Benny的老巢10 小时前
【n8n工作流入门02】macOS安装n8n保姆级教程:Homebrew与npm两种方式详解
macos·npm·node.js·n8n·n8n工作流·homwbrew·n8n安装
2301_8187320611 小时前
下载nvm后,通过nvm无法下载node,有文件夹但是为空 全局cmd,查不到node和npm 已解决
前端·npm·node.js
稀饭521 天前
用changeset来管理你的npm包版本
前端·npm
就知道你是成心的1 天前
npm pack 一键构建npm离线包
npm
GuMoYu1 天前
npm link 测试本地依赖完整指南
前端·npm
爱写程序的小高2 天前
npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree
前端·npm·node.js
程序员的程2 天前
我做了一个前端股票行情 SDK:stock-sdk(浏览器和 Node 都能跑)
前端·npm·github
爱写程序的小高2 天前
npm版本降级、nvm切换node版本、webpack版本与vue版本不一致
前端·npm·node.js