关于软件包的管理工具,大家比较熟知的是 npm
和 Yarn
,今天给大家介绍一个新的包管理工具pnpm。
pnpm是高性能的npm,通过内容可寻址存储(CAS)、符号链接(Symbolic Link)、硬链接(Hard Link)等管理依赖包,达到多项目之间依赖共享(节省存储空间),减少安装时间(安装快速)。
简介
pnpm
代表performant npm
(高性能的npm),同npm
和Yarn
,都属于Javascript
包管理安装工具,它较npm
和Yarn
在性能上得到很大提升,被称为快速的,节省磁盘空间的包管理工具。
当使用 npm
或 Yarn
时,如果你有 100 个项目使用了某个依赖(dependency),就会有 100 份该依赖的副本保存在硬盘上,而在使用 pnpm
时,依赖会被存储在内容可寻址的存储中,所以:
- 如果你用到了某依赖项的不同版本,只会将不同版本间有差异的文件添加到仓库。 例如,如果某个包有100个文件,而它的新版本只改变了其中1个文件。那么
pnpm update
时只会向存储中心额外添加1个新文件,而不会因为仅仅一个文件的改变复制整新版本包的内容。 - 所有文件都会存储在硬盘上的某一位置。 当软件包被安装时,包里的文件会硬链接到这一位置上对应的文件,而不会占用额外的磁盘空间。 这允许你跨项目地共享同一版本的依赖。
因此,您在磁盘上节省了大量空间,这与项目和依赖项的数量成正比,并且安装速度要快得多!
基本使用
1. 安装
通过npm
安装:npm install -g pnpm
;
通过 pnpm -v
命令查看已安装的pnpm
的版本。
2. 使用
执行命令 pnpm init
进行初始化,生成package.json
文件;
安装依赖:pnpm install xxx
;
使用命令 pnpm run xxx
运行package.json
中定义的scripts
脚本,启动服务即可。
3. 示例:创建一个vue3项目
通过pnpm create
使用vite
套件新建一个vue3
的项目
1 | # 使用pnpm create 启动套件(vite,只有存在的套件才可以)创建模板项目 |
通过上述操作,我们学到了pnpm
项目的初始化、安装依赖、启动服务等,可以运行起来,感受一下它和npm
运行速度的差异。
4. 常用命令
(1)设置源
- 查看源:
pnpm config get registry
- 切换源:
pnpm config set registry https://registry.npmmirror.com
(2)初始化
初始化package.json:
pnpm init
注意:
pnpm init
只能一键快速生成package.json
文件,如果要一步一步填写每个属性的值生成package.json
文件,则需要通过npm init
生成,如果要一键快速生成,需要增加-y
参数npm init -y
来生成。
(3)管理依赖
- 安装依赖包到
dependencies
:pnpm add <pkg>
- 安装依赖包到
devDependencies
:pnpm add -D <pkg>
- 安装依赖包到
optionalDependencies
:pnpm add -O <pkg>
- 全局安装依赖包:
pnpm add -g xxx
- 安装项目全部依赖:
pnpm install,别名pnpm i**
- 更新依赖包:
pnpm update,别名pnpm up
- 删除依赖包:
pnpm remove,别名pnpm rm/uninstall/un
(4)查看依赖
- 查看本地安装的依赖:
pnpm list,别名pnpm ls
- 查看全局安装的依赖:
pnpm list --global,别名pnpm ls --g
- 检查过期的依赖:
pnpm outdated
(5)运行脚本
- 运行自定义脚本:
pnpm run xxx,别名pnpm xxx
- 运行
test
测试脚本:pnpm test
- 启动套件创建项目:
pnpm create
- 运行
start
启动命令:pnpm start
(6)发布依赖包
- 发布依赖包:
pnpm publish
(7)管理node环境
可实现nvm
、n
等node
版本管理工具,安装并切换node.js
版本的功能。
- 本地安装并使用:
pnpm env use <node版本号>
- 全局安装并使用:
pnpm env use --global <node版本号>
(8)清理缓存
有时候,pnpm 的缓存可能会导致问题。你可以尝试清理 pnpm 的缓存然后重新安装依赖。使用 pnpm store prune
来清理缓存。
原理探究
1. node_modules
pnpm基于符号链接来创建非扁平化的
node_modules
对比npm
和pnpm
安装的node_modules
:
npm | pnpm |
---|---|
所有依赖包平铺在node_modules 目录,包括直接依赖包以及其他次级依赖包 |
node_modules 目录下只有.pnpm 和直接依赖包(vue、vite、... ),没有其他次级依赖包 |
没有符号链接 | 直接依赖包的后面有符号链接的标识 |
那pnpm
怎么管理这些依赖包的呢?带着这一问题,我们继续探究。
详细看一下pnpm
生成的node_modules
目录如下:
1 | ▾ node_modules |
node_modules
中只有一个 .pnpm
的文件夹以及三个符号链接@vitejs/plugin-vue
、 vite
和 vue
。 这是因为我们的项目只安装了@vitejs/plugin-vue
、 vite
和 vue
三个依赖,pnpm
使用符号链接的方式将项目的直接依赖添加到node_modules
的根目录下,也就是说node_modules目录下只有我们项目中依赖的dependencies、devDependencies和一个.pnpm目录。
以vite
依赖包举例,看一下vite
依赖包和.pnpm
目录里都有些什么:
展开vite
依赖包,我们会有两个疑问:
vite
是一个符号链接,那它的实际位置在哪里?- 依赖的其他次级依赖在哪里?
(1)vite的实际位置
.pnpm
称为虚拟存储目录,以平铺的形式储存着所有的项目依赖包,每个依赖包都可以通过.pnpm/<name>@<version>/node_modules/<name>
路径找到实际位置。
即直接依赖的vite
包 符号链接到路径:.pnpm/vite@2.9.12/node_modules/vite
,vite
包中的每个文件都是来自内容可寻址存储的硬链接。
1 | .pnpm/vite@2.9.12/node_modules/vite |
(2)vite的次级依赖
观察上面的目录结构,发现/node_modules/.pnpm/vite@2.9.12/node_modules/vite
目录下还是没有次级依赖的node_modules
。
pnpm 的 node_modules设计 ,包的依赖项与依赖包的实际位置位于同一目录级别。
所以 vite
的次级依赖包不在 .pnpm/vite@2.9.12/node_modules/vite/node_modules/
, 而是在 .pnpm/vite@2.9.12/node_modules/
,与vite
实际位置位于同一目录级别。
1 | ▾ node_modules |
这里的esbuild
等次级依赖包又是一个符号链接,仍符合刚才的逻辑,实际位置在.pnpm/esbuild@0.14.43/node_modules/esbuild
,包内的每个文件再硬链接到pnpm store
中的对应文件。
我们再通过官网提供的依赖图,再辅助理解一下node_modules
依赖包之间的关系。
项目依赖了bar@1.0.0
版本,bar
依赖了foo@1.0.0
版本,node_modules
下只有直接依赖包bar@1.0.0
符号链接和.pnpm
目录。
Node.js
解析时,bar@1.0.0
就会符号链接到实际位置.pnpm/bar@1.0.0/node_modules/bar
,包中的文件(并非包文件夹)都硬链接到.pnpm store
中的对应文件,foo@1.0.0
做为bar
的依赖,与bar
的实际位置处于同一层级,符号链接指向实际位置.pnpm/foo@1.0.0/node_modules/foo
,包中的文件再硬链接至.pnpm store
。
关于peerDependencies是怎么处理依赖的,可以看官网这篇文章
总结:pnpm使用符号链接Symbolic link(软链接)来创建依赖项的嵌套结构,将项目的直接依赖符号链接到node_modules的根目录,直接依赖的实际位置在.pnpm/<name>@<version>/node_modules/<name>
,依赖包中的每个文件再硬链接(Hard link)到.pnpm store
。
2. 包存储store
pnpm store
:pnpm
资源在磁盘上的存储位置
一般store
在Mac/Linux系统中,默认会设置到{home dir}>/.pnpm-store/v3
;windows下会设置到当前盘的根目录下,比如C(C:\.pnpm-store\v3
)、D盘(D:\.pnpm-store\v3
)。
可以通过执行pnpm store path
命令查看store存储目录的路径
进入store
存储路径,查看存储的内容如下:
files/xx/xxx
以文件夹进行分类,每个文件夹内包含重新编码命名后的文件,依赖包硬链接到此处对应的文件。
在项目中执行pnpm install
的时候,依赖包存在于store
中,直接创建依赖包硬链接到store
中,如果不存在,则从远程下载后存储在store
中,并从项目的node_modules
依赖包中创建硬链接到store
中。
上图中提示包从Content-addressable store
硬链接到Virtual store
,以及Content-addressable store
和Virtual store
的作用位置。
它是一种存储信息的方式,根据内容而不是位置进行检索信息的存储方式,被用于高速存储和检索的固定内容,如存储。这里的CAS作用于/Users/<username>/.pnpm-store/v3
目录。
- 虚拟存储 — Virtual store
指向存储的链接的目录,所有直接和间接依赖项都链接到此目录中,项目当中的.pnpm目录node_modules/.pnpm
。
因为这样的处理机制,每次安装依赖的时候,如果是相同的依赖,有好多项目都用到这个依赖,那么这个依赖实际上最优情况(即版本相同)只用安装一次。如果依赖包存在于pnpm store中,则从store目录里面去hard-link,避免了二次安装带来的时间消耗,如果不存在的话,就会去下载并存储在store中。
如果是 npm 或 Yarn,那么这个依赖在多个项目中使用,在每次安装的时候都会被重新下载一次。
对比发现pnpm install
安装速度相当之快!必须给个大大的赞!
❓紧接着会有人问,那一直往store里存储依赖包,store会不会越来越大?
官方提供了一个命令:pnpm store prune
,从存储中删除未引用的包。
未引用的包是系统上的任何项目中都未使用的包。 在大多数安装操作之后,包有可能会变为未引用状态。
官方举例:在 pnpm install
期间,包 foo@1.0.0
被更新为 foo@1.0.1
。 pnpm 将在存储中保留 foo@1.0.0
,因为它不会自动除去包。 如果包 foo@1.0.0
没有被其他任何项目使用,它将变为未引用。 运行 pnpm store prune
将会把 foo@1.0.0
从存储中删除 。
运行 pnpm store prune
是不会影响项目的。 如果以后需要安装已经被删除的包,pnpm 将重新下载他们。建议清理不要太频繁,以防在切换分支等时pnpm需要重新下载所有删除的包,减慢安装过程。
pnpm store的其他命令
pnpm store status
:查看store中已修改的包,如果包的内容与拆包时时相同的话,返回退出代码0。
pnpm store add
:只把包加入存储中,且没有修改存储外的任何项目或文件
pnpm store prune
:删除存储中未被引用的包
monorepo支持
pnpm
跟npm
和Yarn
一样,内置了对单一存储库monorepo的支持,只需要在项目根目录下创建 pnpm-workspace.yaml
文件,定义workspace
的根目录。
例如:
1 | pnpm-workspace.yaml |
1. Workspace协议(workspace:)
workspace:工作空间
默认情况下,如果可用的 packages
与已声明的可用范围相匹配,pnpm 将从工作空间链接这些 packages
。
例如,如果 bar
中有 "foo":"^1.0.0"
的这个依赖项,则 foo@1.0.0
链接到 bar
。 但是,如果 bar
的依赖项中有 "foo": "2.0.0"
,而 foo@2.0.0
在工作空间中并不存在,则将从 npm registry 安装 foo@2.0.0
,这种行为带来了一些不确定性。
pnpm
支持 workspace
协议(写法:workspace:<版本号>
)。 当使用此协议时,pnpm 将拒绝解析除本地 workspace
包含的 package
之外的任何内容。 因此,如果您设置为 "foo": "workspace:2.0.0"
时,安装将会失败,因为 "foo@2.0.0"
不存在于此 workspace
中。
接下来,我们使用vue代码库来理解一下Workspace协议:
代码库地址:github.com/vuejs/core
根目录可以看到有这两个文件pnpm-lock.yaml
、pnpm-workspace.yaml
, 其中lock文件为pnpm install
时生成的lock文件,space文件则为monorepo
仓库中必须需要的定义工作空间目录的文件。
(1)点开pnpm-workspace.yaml
文件:github.com/vuejs/core/…
我们看到文件内容为:
1 | packages: |
也就表示core/packages/*
这个目录下面所有的文件为workspace
的内容。
(2)点开package.json
文件:github.com/vuejs/core/…
我们看到用到本地workspace
包的都标注了workspace:*
协议,这样依赖的就一直是本地的包,而不是从npm registry
安装的包。
(3)我们验证一下到底依赖的是不是本地包
clone代码库到本地,pnpm install
安装依赖
查看core/node_modules/
文件夹,发现package.json
文件中依赖的@vue/xxx
、vue
包都已符号链接的形式存在,如下图:
按workspace:*
协议,打开packages/reactivity
文件夹,做一个测试,在index.js
文件中加入console.log('test')
,如下图:
这时候再打开node_modules/@vue/reactivity/index.js
文件,可以发现刚才在packages
里面改的内容,显示在了node_modules
目录下的包里。
打开磁盘上存储(pnpm store path
)的依赖包,并没有上面新增的console.log
,上面的改动只影响了本地依赖包,而不是远程install
下载后存储在磁盘上的包,也就是说符合workspace:协议引入的依赖包就是本地的workspace目录(即core/packages)下的包。
2. 别名引用
假如工作区有一个名为 foo
的包,可以通过这样引用 "foo": "workspace:"
,如果是其它别名,可以这么引用:"bar": "workspace:foo@*"
。
3. 相对引用
假如packages
下有同层级的foo
、bar
,其中bar
依赖于foo
,则可以写作"bar": "workspace:../foo"
。
4. 发布workspace包
当workspace
包打包发布时,将会动态替换这些workspace:
依赖。
假设我们的 workspace
中有 a
、 b
、 c
、 d
并且它们的版本都是 1.5.0
,如下:
1 | { |
将会被转化为:
1 | { |
现在很多很受欢迎的开源项目都适用了pnpm的工作空间功能,感兴趣的可以前往官网查看哦!
对比npm、Yarn
1. 性能对比
在pnpm
官网上,提供了一个benchmarks
基准测试图表,它展示了npm
、pnpm
、Yarn
、Yarn pnp
在install
、update
等场景下的耗时:
通过上图,可以看出pnpm
的运行速度基本上是npm
的两倍,运行速度排名pnpm > Yarn > npm
。
2. 功能对比
通过上图可以看出pnpm
独有的两个功能:
- 管理Node.js版本(
pnpm env use --global xxx
) - 内容可寻址存储(
CAS
)
3. 竞争
Yarn
Yarn
在 v3.1 添加了 pnpm
链接器。 因此 Yarn
可以创建一个类似于 pnpm
创建的 node_modules
目录结构。
此外,Yarn
团队计划实现内容可寻址存储,以提高磁盘空间效率。
npm
npm
团队决定也采用 pnpm
使用的符号链接的 node_modules
目录结构(相关 RFC)。
npm或Yarn 转 pnpm
可参考vue
代码库的这一次升级commit log
1. 全局安装pnpm
1 | npm install -g pnpm |
2. 删除npm
或yarn
生成的node_modules
1 | # 项目目录下运行或手动物理删除 |
3. pnpm import
从其他软件包管理器的lock
文件生成 pnpm-lock.yaml
,再执行pnpm install --frozen-lockfile
(相当于npm ci
)生成依赖,防止没有lock文件意外升级依赖包,导致项目出错
1 | # 生成`pnpm-lock.yaml` |
4. 删除npm
或yarn
生成的lock
文件
1 | # 删除package-lock.json |
5. 项目中的npm
命令等修改为pnpm
,包括README
文档、运行命令等
卸载pnpm
1. 卸载全局安装的包
通过pnpm ls --g
查看全局安装的包,只有通过pnpm install/add xxx --global
安装的包才为全局包哦!
(1)pnpm rm -g xxx
列出每个全局包进行删除
(2)pnpm root -g
找到全局目录的位置并手动删除它
2. 移除pnpm cli
(1)通过独立脚本安装的,可以通过rm -rf $PNPM_HOME
进行移除(谨慎:删除前确定好删除的内容)
(2)使用npm
安装的pnpm
,可以通过npm rm -g pnpm
进行移除
3. 删除全局内容可寻址存储
1 | rm -rf $(pnpm store path) |
如果您不在主磁盘中使用 pnpm
,您必须在使用 pnpm
的每一个磁盘中运行上述命令。 因为 pnpm
会为每一个磁盘创建一个专用的存储空间。
一些疑问
还有一些问题,需要进一步的验证和考究:
webpack
打包的时候,pnpm
依赖包之间引用是怎么处理的?- 手动修改
pnpm store
中的包的内容,其他引用地方是不是影响了? - 删除项目文件夹,
pnpm prune
的机制是什么,能否正确处理?