一、前言
最近团队正在进行 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 层...
- 造成的问题:
⚠️ 问题
- 过长路径限制: 在 windows 下,创建太深的依赖树,可能会让文件路径超过 windows 文件路径最大长度限制(260 字符)。
- 磁盘空间浪费: 当多个包间有公共的依赖时,嵌套 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 结构。
- 造成的问题:
⚠️ 问题
- 幽灵依赖: 没有在 dependencies 里声明的依赖,代码中却却可以 require 进来。
【这样是有隐患的,因为没有显式依赖,万一有一天别的包不依赖这个包了,那你的代码也就不能跑了,因为你依赖这个包,但是现在不会被安装了】 - 依然存在磁盘空间浪费问题: 当遇到 A 包引用了 C@1.x,而 B 包引用了 C@2.x,由于只能提升一个版本,那其余版本的包还是复制了很多次,依然存在空间浪费问题;
【同时提升的依赖不确定,取决于安装顺序,所以后续出了 yarn.lock 和 package-lock.json 来解决这个问题】 - 复杂且慢:扁平化算法相当复杂,执行速度较慢
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;如果不存在,则先下载,然后再硬链接过来
- 优点:
✅ 优点
- 节省磁盘空间: 一个包全局只保存一份,使用时通过软硬链接,可以节省大量磁盘空间。
- 速度快: 通过链接的方式而不是复制,速度自然快了很多。
- 没有幽灵依赖:根目录下的 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
:安装到 peerDependencies 和 devDependencies 中-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_modules
与 package-lock.json
,再用 pnpm install
安装依赖即可。
旧的项目这样安装完依赖,运行时仍有可能报错,缺失某个包,这是因为 pnpm 的包安装策略与 yarn、npm 都有所差别。这时缺哪个包,就安装哪个包,大部分情况就OK了。
五、文末补充 - 软硬链接的区别
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...