pnpm
是一款当代备受关注的 新兴(问题较多) 包管理工具,使用过的同学们都会被它极快的安装速度、极少的磁盘存储空间所吸引!
首先,为什么会出现pnpm
?作者一开始对yarn
的发布有很高的期待,但是发布后并没有满足作者的一些期待,反而让作者有些失望。
After a few days, I realized that Yarn is just a small improvement over npm. Although it makes installations faster and it has some nice new features, it uses the same flat node_modules structure that npm does (since version 3). And flattened dependency trees come with a bunch of issues
几天后,我意识到 Yarn 只是对 npm 的一个小小的改进。尽管它使安装速度更快,并且具有一些不错的新功能,但它使用与npm相同的平面node_modules结构(自版本 3 起)。扁平化的依赖树带来了一系列问题
(具体后面会讲)
至于为什么叫pnpm
?是因为pnpm
作者对现有的包管理工具,尤其是npm
和yarn
的性能特别失望,所以起名叫做performance npm
,即pnpm
(高性能npm)
如何突显pnpm
的性能优势?在pnpm
官网上,提供了一个benchmarks图表,它比对了项目在npm、pnpm、yarn(正常版本和PnP版)中,install
、update
场景下的耗时:
下面表格是上图中的具体数据:
action | cache | lockfile | node_modules | npm | pnpm | Yarn | Yarn PnP |
---|---|---|---|---|---|---|---|
install | 1m 12.2s | 15.7s | 22.1s | 27.5s | |||
install | ✔ | ✔ | ✔ | 1.6s | 1.3s | 2.6s | n/a |
install | ✔ | ✔ | 9.5s | 4s | 8.6s | 1.9s | |
install | ✔ | 14.2s | 7.9s | 14.2s | 7.4s | ||
install | ✔ | 25.4s | 13s | 15.3s | 21.1s | ||
install | ✔ | ✔ | 2.1s | 1.8s | 8.3s | n/a | |
install | ✔ | ✔ | 1.6s | 1.4s | 9.4s | n/a | |
install | ✔ | 2.1s | 5.9s | 15s | n/a | ||
update | n/a | n/a | n/a | 1.6s | 12.1s | 18.7s | 32.4s |
可以看到pnpm(橘色)
有很明显性能提升,在我们项目实践中(基于gitlib
)提升更明显(cache-paths
跟store dir
搭配使用后)
在讨论性能提升原因之前,我们需要先了解下现有包管理工具中node_modules
存在的问题
node_modules 安装方式
目前有两种安装方式:`Nested installation`、`Flat installation`Nested installation 嵌套安装
在 npm@3 之前,node_modules结构是干净
、可预测
的,因为node_modules 中的每个依赖项都有自己的node_modules文件夹,在package.json中指定了所有依赖项。例如下面所示,项目依赖了foo
,foo
又依赖了bar
,依赖关系如下图所示:
node_modules
└─ foo
├─ index.js
├─ package.json
└─ node_modules
└─ bar
├─ index.js
└─ package.json
上面结构有两个严重的问题:
- package中经常创建太深的依赖树,这会导致 Windows 上的目录路径过长问题
- 当一个package在不同的依赖项中需要时,它会被多次复制粘贴并生成多份文件
Flat installation 扁平安装
为了解决上述问题,npm 重新考虑了node_modules结构并提出了扁平化结构。在npm@3+ 和 yarn中,node_modules 结构变成如下所示:
node_modules
├─ foo
| ├─ index.js
| └─ package.json
└─ bar
├─ index.js
└─ package.json
可以看到,hoist
机制下,bar
被提升到了顶层。如果同一个包的多个版本在项目中被依赖时,node_modules结构又是怎么样的?
例如:一个项目App
直接依赖了A(version: 1.0)
和C(version: 1.0)
,A
和C
都依赖了不同版本的B
,其中A
依赖B 1.0
,C
依赖B 2.0
,可以通过下图清晰的看到npm2
和npm3+
结构差异:
包B 1.0
被提升到了顶层,这里需要注意的是,多个版本的包只能有一个
被提升上来,其余版本的包会嵌套安装到各自的依赖当中(类似npm2
的结构)。
至于哪个版本的包被提升,依赖于包的安装顺序!
依赖变更会影响提升的版本号,比如变更后,有可能是B 1.0
,也有可能是 B 2.0
被提升上来(但只能有一个版本提升)
细心的小伙伴可能发现,这其实并没有解决之前的问题,反而又引入了新的问题
npm3+和yarn存在的问题
Phantom dependencies 幽灵依赖
Phantom dependencies 被称之为幽灵依赖或幻影依赖,解释起来很简单,即某个包没有在package.json
被依赖,但是用户却能够引用到这个包。
引发这个现象的原因一般是因为 node_modules 结构所导致的。例如使用 npm或yarn 对项目安装依赖,依赖里面有个依赖叫做 foo
,foo
这个依赖同时依赖了 bar
,yarn 会对安装的 node_modules 做一个扁平化结构的处理,会把依赖在 node_modules 下打平,这样相当于 foo
和 bar
出现在同一层级下面。那么根据 nodejs 的寻径原理,用户能 require 到 foo
,同样也能 require 到 bar
。
nodejs的寻址方式:(查看更多)
- 对于核心模块(core module) => 绝对路径 寻址
- node标准库 => 相对路径寻址
- 第三方库(通过npm安装)到node_modules下的库(可以在node环境中输入
module.paths
查看):
3.1. 先在当前路径下,寻找 currentProject/node_modules/xxx
3.2 递归从下往上,到上级路径寻找,例如 ../node_modules/xxx
3.3 循环步骤3.2
3.4 在全局环境路径下寻找,例如 .node_modules/xxx
3.5 在用户目录下寻找,例如 users/金虹桥程序员/.node_modules/xxx 或者 users/金虹桥程序员/node_libraries/xxx
3.6 node安装目录下查找,例如 nodejs/lib/node/.node_modules/xxx
NPM doppelgangers NPM分身
这个问题其实也可以说是 hoist 导致的,这个问题可能会导致有大量的依赖的被重复安装.举个例子:项目中有packageA
、packageB
、packageC
、packageD
。packageA
依赖packageX 1.0和packageY 1.0,packageB
依赖packageX 2.0和packageY 2.0,packageC
依赖packageX 1.0和packageY 2.0,packageD
依赖packageX 2.0和packageY 1.0。
在npm2时,结构如下
- package A
- packageX 1.0
- packageY 1.0
- package B
- packageX 2.0
- packageY 2.0
- package C
- packageX 1.0
- packageY 2.0
- package D
- packageX 2.0
- packageY 1.0
在npm3+和yarn中,由于存在hoist机制,所以X和Y各有一个版本被提升了上来,目录结构如下
- package X => 1.0版本
- package Y => 1.0版本
- package A
- package B
- packageX 2.0
- packageY 2.0
- package C
- packageY 2.0
- package D
- packageX 2.0
如上图所示的packageX 2.0和packageY 2.0被重复安装多次,从而造成 npm 和 yarn 的性能一些性能损失。
这种场景在monorepo 多包场景下尤其明显,这也是yarn workspace
经常被吐槽的点,另外扁平化的算法实现也相当复杂,改动成本很高。
那么pnpm
是如何解决这种问题的呢?
pnpm的破解之道:网状 + 平铺的node_modules结构
pnpm
的用户可能会发现它node_modules
并不是扁平化结构,而是目录树的结构,类似npm version 2.x
版本中的结构,如下图所示
同时还有个.pnpm
目录,如下图所示
.pnpm
以平铺的形式储存着所有的包,正常的包都可以在这种命名模式的文件夹中被找到(peerDep例外):
.pnpm/<organization-name>+<package-name>@<version>/node_modules/<name>
// 组织名(若无会省略)+包名@版本号/node_modules/名称(项目名称)
我们称.pnmp
为虚拟存储目录,该目录通过<package-name>@<version>
来实现相同模块不同版本之间隔离和复用,由于它只会根据项目中的依赖生成,并不存在提升,所以它不会存在之前提到的Phantom dependencies问题!
那么它如何跟文件资源进行关联的呢?又如何被项目中使用呢?
答案是Store
+ Links
!
Store
pnpm
资源在磁盘上的存储位置。
pnpm
使用名为 .pnpm-store的 store dir,Mac/linux中默认会设置到{home dir}>/.pnpm-store/v3
;windows下会设置到当前盘的根目录下,比如C(C/.pnpm-store/v3
)、D盘(D/.pnpm-store/v3
)。
具体可以参考 @pnpm/store-path
这个 pnpm
子包中的代码:
const homedir = os.homedir()
if (await canLinkToSubdir(tempFile, homedir)) {
await fs.unlink(tempFile)
// If the project is on the drive on which the OS home directory
// then the store is placed in the home directory
return path.join(homedir, relStore, STORE_VERSION)
}
由于每个磁盘有自己的存储方式,所以Store会根据磁盘来划分。 如果磁盘上存在主目录,存储则会被创建在
<home dir>/.pnpm-store
;如果磁盘上没有主目录,那么将在文件系统的根目录中创建该存储。 例如,如果安装发生在挂载在/mnt
的文件系统上,那么存储将在/mnt/.pnpm-store
处创建。 Windows系统上也是如此。
可以在不同的磁盘上设置同一个存储,但在这种情况下,pnpm
将复制包而不是硬链接它们,因为硬链接只能发生在同一文件系统同一分区上。
windows store如下图所示
pnpm install
的安装过程中,我们会看到如下的信息,这个里面的Content-addressable store
就是我们目前说的Store
CAS 内容寻址存储,是一种存储信息的方式,根据内容而不是位置进行检索信息的存储方式。
Virtual store 虚拟存储,指向存储的链接的目录,所有直接和间接依赖项都链接到此目录中,项目当中的.pnpm目录
如果是 npm 或 yarn,那么这个依赖在多个项目中使用,在每次安装的时候都会被重新下载一次
如图可以看到在使用 pnpm 对项目安装依赖的时候,如果某个依赖在 sotre 目录中存在了话,那么就会直接从 store 目录里面去 hard-link,避免了二次安装带来的时间消耗,如果依赖在 store 目录里面不存在的话,就会去下载一次。
看到这里,你应该对Store有了一些简单的了解,接着我们来看下项目中的文件如何跟Store关联。
Links(hard link & symbolic link)
还记得文章刚开始,放了两张beachmark的图表,图表上可以看到很明显的性能提升(如果你使用过,感触会更明显)!pnpm 是怎么做到如此大的提升的呢?一部分原因是使用了计算机当中的 Hard link ,它减少了文件下载的数量,从而提升了下载和响应速度。
hard link
通过hard link
, 用户可以通过不同的路径引用方式去找到某个文件,需要注意的是一般用户权限下只能硬链接到文件,不能用于目录。
pnpm
会在Store
(上面的Store) 目录里存储项目 node_modules
文件的 hard links
,通过访问这些link直接访问文件资源。
举个例子,例如项目里面有个 2MB 的依赖 react
,在 pnpm 中,看上去这个 react
依赖同时占用了 2MB 的 node_modules 目录以及全局 store 目录 2MB 的空间(加起来是 4MB),但因为 hard link
的机制使得两个目录下相同的 2MB 空间能从两个不同位置进行CAS寻址
直接引用到文件,因此实际上这个react
依赖只用占用2MB 的空间,而不是4MB。
因为这样一个机制,导致每次安装依赖的时候,如果是个相同的依赖,有好多项目都用到这个依赖,那么这个依赖实际上最优情况(即版本相同)只用安装一次。
而在npm
和yarn
中,如何一个依赖被多个项目使用,会发生多次下载和安装!
如果是 npm 或 yarn,那么这个依赖在多个项目中使用,在每次安装的时候都会被重新下载一次。
如图可以看到在使用 pnpm 对项目安装依赖的时候,如果某个依赖在 store 目录中存在了话,那么就会直接从 store 目录里面去 hard-link,避免了二次安装带来的时间消耗,如果依赖在 store 目录里面不存在的话,就会去下载一次。
通过Store
+ hard link
的方式,不仅解决了项目中的NPM doppelgangers问题,项目之间也不存在该问题,从而完美解决了npm3+
和yarn
中的包重复问题!
如果随着项目越来越大,版本变更变多,历史版本的资源会堆积,导致Store
目录越来越大,那如何解决这个问题呢?
针对这个现象,pnpm 提供了一个命令来解决这个问题: pnpm store | pnpm。
同时该命令提供了一个选项,使用方法为 pnpm store prune
,它提供了一种用于删除一些不被全局项目所引用到的 packages 的功能,例如有个包 axios@1.0.0
被一个项目所引用了,但是某次修改使得项目里这个包被更新到了 1.0.1
,那么 store 里面的 1.0.0 的 axios 就就成了个不被引用的包,执行 pnpm store prune
就可以在 store 里面删掉它了。
该命令推荐偶尔进行使用,但不要频繁使用,因为可能某天这个不被引用的包又突然被哪个项目引用了,这样就可以不用再去重新下载这个包了。
symbolic link
由于hark link
只能用于文件不能用于目录,但是pnpm
的node_modules
是树形目录结构,那么如何链接到文件?
通过symbolic link
(也可称之为软链或者符号链接)来实现!
通过前面的讲解,我们知道了pnpm
在全局通过Store
来存储所有的node_modules依赖,并且在.pnpm/node_modules
中存储项目的hard links,通过hard link
来链接真实的文件资源,项目中则通过symbolic link
链接到.pnpm/node_modules
目录中,依赖放置在同一级别避免了循环的软链。
pnpm
的 node_modules
结构一开始看起来很奇怪:
- 它完全适配了 Node.js。
- 包与其依赖被完美地组织在一起。
有 peer 依赖的包的结构更加复杂一些,但思路是一样的:使用软链与平铺目录来构建一个嵌套结构。
假设我们有个mono repo,它有repo A
、repo B
、repo C
和repo D
4个repo。每个repo有各自的一些依赖项(包括dependencies和peerDependencies),假定结构如下图所示:(需要注意有个peer dep)
下面是pnpm workspace
中,比较清晰(不清晰的话留言,我可以改改!)说明了Store
和Links
间的相互关系:
官网也更新了类似的调用关 图,大家也可以看看!
PeerDependencies
pnpm 的最佳特征之一是,在一个项目中,`package`的一个特定版本将始终只有一组依赖项。 这个规则有一个例外 -那就是具有 [peer dependencies ](https://docs.npmjs.com/files/package.json#peerdependencies)的`package`。通常,如果一个package
没有 peer 依赖项(peer dependencies),它会被硬链接到其依赖项的软连接(symlinks)旁的 node_modules
,就像这样:
如果 foo
有 peer 依赖(peer dependencies),那么它可能就会有多组依赖项,所以我们为不同的 peer 依赖项创建不同的解析:
pnpm
创建 foo@1.0.0_bar@1.0.0+baz@1.0.0
或foo@1.0.0_bar@1.0.0+baz@1.1.0
内到foo
的软链接。 因此,Node.js 模块解析器将找到正确的 peers。
peerDep
的包命名规则如下(看起来就很麻烦)
.pnpm/<organization-name>+<package-name>@<version>_<organization-name>+<package-name>@<version>/node_modules/<name>
// peerDep组织名(若无会省略)+包名@版本号_组织名(若无会省略)+包名@版本号/node_modules/名称(项目名称)
如果一个
package
没有 peer 依赖(peer dependencies),不过它的依赖项有 peer 依赖,这些依赖会在更高的依赖图中解析, 则这个传递package
便可在项目中有几组不同的依赖项。 例如,a@1.0.0
具有单个依赖项b@1.0.0
。b@1.0.0
有一个 peer 依赖为c@^1
。a@1.0.0
永远不会解析b@1.0.0
的 peer, 所以它也会依赖于b@1.0.0
的 peer 。
如果需要解决peerDep引入的多实例问题,可以通过 .pnpmfile.cjs
文件更改依赖项的依赖关系。
pnpm和npm、yarn的功能差异
下面图表摘自官网,感兴趣的同学可以自行查阅。功能 | pnpm | Yarn | npm |
---|---|---|---|
工作空间支持(monorepo) | ✔️ | ✔️ | ✔️ |
隔离的 node_modules | ✔️ - 默认 | ✔️ | ✔️ |
提升的 node_modules | ✔️ | ✔️ | ✔️ - 默认 |
Plug'n'Play | ✔️ | ✔️ - 默认 | ❌ |
零安装 | ❌ | ✔️ | ❌ |
修补依赖项 | ❌ | ✔️ | ❌ |
管理 Node.js 版本 (pnpm独有) | ✔️ | ❌ | ❌ |
有锁文件 | ✔️ - pnpm-lock.yaml | ✔️ - yarn.lock | ✔️ - package-lock.json |
支持覆盖 | ✔️ | ✔️ - 通过 resolutions | ✔️ |
内容可寻址存储(CAS) (pnpm独有) | ✔️ | ❌ | ❌ |
动态包执行 | ✔️ - 通过 pnpm dlx | ✔️ - 通过 yarn dlx | ✔️ - 通过 npx |
从图表可以看到有两个是pnpm
独有的实现:管理 Node.js 版本
和内容可寻址存储(CAS)
其中CAS前面已经介绍过了,我们讲一下管理 Node.js 版本
。
这个在.npmrc
文件中,Node模块设置中使用use-node-version
进行配置(其它配置信息)
use-node-version
用于指定应用于项目运行时的确切 Node.js 版本,支持semver版本设置。设置后, pnpm 将自动安装指定版本的 Node.js 并将其用于执行 pnpm run
命令或 pnpm node
命令。
// 指定版本16.x
use-node-version=^16.x
当前安装的是14.x,使用上述配置后,在执行时会有一个warning: WARN Unsupported engine: wanted: {"node":">=14.0.0"} (current: {"node":"^16.x","pnpm":"6.22.2"})
但不影响执行结果
workspace
pnpm
跟npm
、yarn
一样,也内置了对monorepo的支持,使用起来比较简单,在项目根目录中新建pnpm-workspace.yaml
文件,并声明对应的工作区就好。
packages:
# 所有在 packages/ 子目录下的 package
- 'packages/**'
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
从版本 3.7 开始支持工作区协议workspace:
。当使用此协议时,pnpm 将拒绝解析除本地工作区 package
之外的任何内容。 因此,如果您设置为 "foo": "workspace:2.0.0"
时,安装将会失败,因为 "foo@2.0.0"
不存在于工作空间中。这个特性在monorepo当中特别有用。
可以通过修改配置link-workspace-packages
来改变包的使用方式(远端下载 or 本地)。
link-workspace-packages
有三个值
- true 默认,使用本地可用的packages;
- false 禁用后,将从registry 下载安装到本地,并被使用(类似yarn\npm 安装)
- deep 自 v5 版本起可用,本地packages当中的依赖项(sub package)也可以被链接到并使用
它支持两种引用方式:别名引用和相对引用。
别名引用
假如工作区有一个名为 local-package
的包,可以通过这样引用 "local-package": "workspace:"
。如果是其它别名,可以这么引用: "ref-package": "workspace:local-package@*"
相对引用
假如packages下有同层级的repoA
、repoB
,其中repoA
依赖于repoB
,则可以写作"repoA": "workspace:../repoB"
。
发布前,这两种引用方式,都会被替换为常规的版本规范。
filter 过滤
通过该参数,允许我们将命令运用到指定的包上面,类似于jQuery
跟Dom
的选择器,写法如下
pnpm <command> --filter <package_selector>
同时它也支持链式调用,可以一次写多个调用,如下所示,
pnpm <command> --filter selector1 --filter selector2 -- filter=!selector3
Lerna中也支持选择器,参数是
scope
,下面的文章对比了Lerna
和pnpm
的选择器,感兴趣的同学可以看一下
Lerna 与 pnpm 选择器
matching 匹配
匹配规则支持三种维度匹配:packageName(包名)
、dirName (目录名)
、git commit/branch(git提交或分支名)
packageName(包名)
支持包名通配符匹配、包和依赖项(直接和间接依赖)匹配、依赖项(直接和间接依赖)匹配、包被依赖项(直接和间接依赖)匹配、被依赖项(直接和间接依赖)匹配
// 包名通配符匹配,其中scope是可选的,当单个scope时:`--filter=core` 将选择 `@babel/core`;多scope不生效
pnpm test --filter "@babel/core"\
pnpm test --filter "@babel/*"\
pnpm test --filter "*core"
// 包和依赖项(直接和间接依赖)匹配,要选择一个软件包及其依赖项 (直接和非直接) 在包名称后加上省略号: `<package_name>...`
pnpm test --filter foo...
pnpm test --filter "@babel/preset-*..." // 可选择一组根目录包
// 依赖项(直接和间接依赖)匹配,要选择一个软件包及其依赖项 (直接和非直接), 在包名前添加一个山形符号加上上面提到的省略号
pnpm test --filter foo^...
pnpm test --filter "@babel/preset-*^..." // 可选择一组根目录包
// 包被依赖项(直接和间接依赖)匹配,在包名前添加一个省略号: `...<package_name>`。
pnpm test --filter ...foo // 将运行 `foo` 以及依赖于它的所有包的测试:
// 被依赖项(直接和间接依赖)匹配
pnpm test --filter "...^foo" // 将运行所有依赖于 `foo` 的包的测试
细心的小伙伴可能发现了,核心就是:一个元素packageName
+ 两个操作(...
和^
)的排列组合
packageName
名称,支持通配符...
选择包及其依赖项,需要放在包名前或包名后。...packageName
被 依赖项匹配packageName...
依赖项匹配
^
排除当前包,可以单独使用,但是推荐和...
(依赖项)搭配使用
packageName、 ...packageName、packageName...、...packageName...、...^packageName、packageName^...、...^packageName^...、...^packageName...、...packageName^...
这样看的话会清晰很多。
dirName (目录名)
支持相对路径引用(通常是POSIX格式)、指定目录项目的形式(可以搭配操作一起使用)
// 相对路径引用
pnpm <cmd> --filter ./packages
// 搭配操作符(`...`和`^`)
pnpm <cmd> --filter ...{<directory>}
pnpm <cmd> --filter {<directory>}...
pnpm <cmd> --filter ...{<directory>}...
git commit/branch(git提交或分支名)
,这个和packageName
的用法类似,但是它可以和前面两者搭配使用
// 运行自 `master` 以来所有变动过的包以及被其依赖的包的测试
pnpm test --filter "...[origin/master]"
// 和`package-name`搭配使用
pnpm <cmd> --filter "...@babel/*{components}[origin/master]"
// 和`dir-name`搭配使用
pnpm <cmd> --filter "...{packages}[origin/master]..."
excluding 排除
只要在开头添加一个!
,过滤规则选择器都会变为排除项。
在
gitbash
、zsh
等shell中, "!" 应转义:\!
. 否则会报bash: !{packageName}: event not found
错误
multiplicity 混合
因为filter
支持链式调用,所以这两种场景可以混合使用。例如:有个组织@fe
,里面存在包A、B、C、D,我们需要选中不含B包的其它包来执行build命令,可以这么写
pnpm build --filter @fe/* --filter=!@fe/B
混合的情况下(Filter1、Filter2、...、FilterN),Filter1会先生效,然后是Filter2,直到N,跟ECMAScript
类似,当然真实业务中可能会更复杂,具体还需要按场景分析。
一开始Filtering区分不明显,后来本文作者发了个PR重新修改了文档结构,方便理解
command 命令
参照官网,按照使用场景进行分类,列举出部分命令的用法,所有命令请前往 官网 查看
manage depencies 管理依赖
pnpm add
add
命令是老朋友了,跟yarn add
类似,安装package以及依赖的package,默认是安装到dependencies
中。注意的是在workspace中,如果想要安装在root workspace中需要添加-w
或者--ignore-workspace-root-check
,安装到packages中需要使用--filter
,否则会安装失败
5种安装姿势:
- npm(默认): workspace中 会先确认改包是否被引用,是的话根据使用版本来安装; 非workspace中,默认会从
npm registry
安装最新的package。例如:pnpm add express@nightly
(tag)、pnpm add express@1.0.0
(version)、pnpm add express@2 react@">=0.1.0 <0.2.0"
(semantic versioning)。 - workspace: workspace安装依赖时, 会从已配置的源处进行安装,当然取决于是否设置了
link-workspace-packages
,以及是否使用了workspace: range protocol
。 - local file system:本地安装有两种安装方式,源文件和本地目录。
- reomote tarball:远端安装必须钥匙一个可访问的URL。
- git repository:git安装通过git clone从git 作者处安装。
常用的参数选项
--save-prod, -P
:安装到dependencies--save-dev, -D
:安装到devDependencies--save-optional, -O
:安装到optionalDependencies--save-peer
:安装到peerDependencies和devDependencies中--global
:安装全局依赖。--workspace
:仅添加在 workspace 找到的依赖项。
pnpm remove
别名: rm, uninstall, un
从 node_modules
和项目的 package.json
中移除包。参数跟add
类似,不展开说了
pnpm install
别名: i
pnpm install
用于安装项目所有依赖。在CI环境中, 如果存在需要更新的 lockfile 会安装失败,所以每次版本更新后,本地一定要install后再提交,否则会导致版本发布失败。
这里讲一下--fix-lockfile
和--shamefully-hoist
。
--fix-lockfile
参数自动修复损坏的 lock 文件入口,首次安装时候特别有用,如果遇到某个包找不到,可能是幻影依赖的问题,需要手动添加依赖或者排查原因。--shamefully-hoist
创建一个扁平node_modules
目录结构, 类似于npm
或yarn
。 这是非常不推荐的,但是确实某些场景下可以解决迁移后无法使用额问题
pnpm import
import
命令支持从其它格式的lock文件生成pnpm-lock.yaml
文件,目前支持三种格式源文件
package-lock.json
npm-shrinkwrap.json
yarn.lock
(v6.14.0 起)
个人认为这个命令跟lerna import
搭配起来使用更好,lerna import
导入git提交历史 (了解更多),一个负责生成pnpm-lock.yaml
文件,这样可以完全还原项目的提交历史和版本依赖。
pnpm prune
prune
移除项目中不需要的依赖包,配置项支持 --prod
(删除在 devDependencies
中指定的包)和
--no-optional
(删除在 optionalDependencies
中指定的包。).
当全局或者单例模式下使用store-dir
时会尤其有用,可以用脚本周期性的删除历史版本依赖。
WARNING prune 命令目前不支持在
monorepo
中递归执行。 可以删除一个只安装 production 依赖的monorepo
的几个node_modules
文件夹,然后重新再用pnpm install --prod
安装。
review dependencies 查看依赖
pnpm list
别名: ls
。
此命令会以一个树形结构输出所有的已安装package
的版本及其依赖。添加参数--json
后会输出JSON格式的日志。
run scripts运行脚本
pnpm run
别名: run-script
。
运行一个在 package
的 manifest 文件中定义的脚本。
假如您有个 start
脚本配置在了package.json
中,像这样:
"scripts": {
"start": "start-storybook -s ./assets -p 23762 -c __storybook"
}
您现在可以使用 pnpm run start
运行该脚本! 很简单吧? 对于那些不喜欢敲键盘而浪费时间的人要注意的另一件事是,所有脚本都会有 pnpm 命令的别名,所以最终 pnpm run start
的简写是 pnpm start
(仅适用于那些不与已有的pnpm 命令相同名字的脚本)。注意不要命令里面嵌套pnpm run command
,否则会造成循环执行。
总结
文章写到这里基本结束了,简单回顾下文章的内容。node_modules
随着设计的变化,出现了嵌套安装和扁平安装两种方式,当然它们都有各自的优缺点pnpm
是如何通过非扁平化的安装方式解决现有node_modules
出现的问题- 特有的功能集:
管理 Node.js 版本
和内容可寻址存储(CAS)
- 常用的命令:
install
、add
、remove
、update
、publish
等 workspace
如何使用filter
过滤选择器的一些常见用法:matching
匹配、excluding
排除和multiplicity
混合