目录
1、简介
2、项目出现背景
3、安装方式
4、兼容性
5、故障排除
6、功能比较
7、幻影依赖
7.1 当前项目产生的幻影依赖
7.2 使用monorepo 方式产生的幻影依赖
8、基于符号链接的 node_modules 结构
9、使用pnpm进行Node版本管理
10、包存储(store)
10.1 存储路径已指定
10.2 存储路径未指定
1、简介
pnpm代表performant npm,即高性能的npm。
Pnpm 是一种快速、磁盘空间高效的包管理器:
- 快,比npm等其他包管理器快2倍左右。
- 高效,
node_modules
中的文件从单个内容可寻址存储器链接。 - 非常适合monorepos.
- 严格,一个包只能访问它的
package.json
中指定的依赖项。 - 确定行,有一个名为
pnpm-lock.yaml
的锁文件。 - 可以作为Node.js版本管理器。(类似于nvm等工具)
- 支持多平台(Windos、Linux、macOS)
- 久经考验自2016年以来,各种规模的团队都在生产中使用。
2、项目出现背景
Pnpm的出现主要是为了解决幻影依赖、运行效率,可靠、节省磁盘空间等问题而出现的。
当我们使用npm时,系统种可能会存在多个项目,多个项目种可能会多次引用同一版本的库,这样在系统种同一个库会在很多个项目下进行安装,而使用pnpm的,会在虚拟系统中进行寻址,这样可以避免项目重复的进行安装。
- 只有依赖库不同和版本不一样才会添加到pnpm的虚拟系统中,所以根据依赖库+版本号进行判断,是否添加到pnpm的虚拟系统中。
- 所有文件都存储到pnpm的虚拟系统中,安装软件包时,如果在虚拟系统中存在时,会执行硬链接或reflinks(写时复制)的方式进行安装,从而提升执行效率以及不消耗其他额外的磁盘空间。
3、安装方式
在windows系统中,用power shell执行以下命令:
iwr https://get.pnpm.io/install.ps1 -useb | iex
在Mac系统中,可以通过brew来安装
brew install pnpm
如果系统安装node.js,也可以通过 npm 进行安装。
npm install -g pnpm
以上是常用的安装方式,当然还有一些其他方式,例如通过Corepacck、winget、Scoop、Choco等方式进行安装。
4、兼容性
以下是过去的pnpm版本列表,其中包含相应的Node.js版本支持。
Node.js | pnpm 5 | pnpm 6 | pnpm 7 | pnpm 8 |
---|---|---|---|---|
Node.js 12 | 支持 | 支持 | ||
Node.js 14 | 支持 | 支持 | 支持 | |
Node.js 16 | 未知 | 支持 | 支持 | 支持 |
Node.js 18 | 未知 | 支持 | 支持 | 支持 |
Node.js 20 | 未知 | 未知 | 支持 | 支持 |
5、故障排除
如果pnpm损坏,并且无法通过重新安装进行修复,则可能需要从PATH中手动删除它。
让我们假设你在运行pnpm install
时出现以下错误:
C:\src>pnpm install
internal/modules/cjs/loader.js:883
throw err;
^
Error: Cannot find module 'C:\Users\Bence\AppData\Roaming\npm\pnpm-global\4\node_modules\pnpm\bin\pnpm.js'
←[90m at Function.Module._resolveFilename (internal/modules/cjs/loader.js:880:15)←[39m
←[90m at Function.Module._load (internal/modules/cjs/loader.js:725:27)←[39m
←[90m at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)←[39m
←[90m at internal/main/run_main_module.js:17:47←[39m {
code: ←[32m'MODULE_NOT_FOUND'←[39m,
requireStack: []
}
首先,通过运行以下命令来查找pnpm的位置:which pnpm
.如果你使用的是Windows,请在Git Bash中运行此命令。您将获得pnpm命令的位置,例如:
$ which pnpm
/c/Program Files/nodejs/pnpm
现在您知道了pnpm CLI的位置,打开该目录并删除所有与pnpm相关的文件(pnpm.cmd
、pnpx.cmd
、pnpm
等)。完成后,再次安装pnpm,它应该能按预期工作。
6、功能比较
我们通过以下表格,对比一下pnpm、yarn、npm的区别:
根据上图,可以看出pnpm的优势主要有内容寻址存储、缓存等。
7、幻影依赖
在项目中我们可以使用一个没有在package.json文件中定义的包时,这样就可能会导致幻影依赖出现。示例如下:
7.1 当前项目产生的幻影依赖
我们创建一个项目,安装一个开发依赖如下所示:
{
"name": "test_yilai",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"element-ui": "^2.15.13"
}
}
然后在入口文件index.js 引入其他的包试一下:
var deepMerge = require('deepmerge') // ? 是否可引入
var test = deepMerge.all([[10, 20, 30], [40, 50]])
console.log('test: ', test);
// test: [ 10, 20, 30, 40, 50 ]
根据以上测试,我们发现是可以引入的,如果这样,那么element-ui 中的引用在node_modules中时直接铺平的,所以就直接引入了。我们可以看下node_modules目录:
在实际运行中,可能会导致一些未知的bug:
- 未在package.json指定这个包的版本,随着版本的升级,依赖也会进行升级,这可能会导致原来引用包的方法,可能在新版本中已经不存在了。
- 如果别人引用这个包,或者别人未在开发依赖中安装这个包,就会导致缺少依赖的错误,或者导致这个未知bug在整个项目中覆盖。
7.2 使用monorepo 方式产生的幻影依赖
首先,先查看项目的目录结构如下所示:
在子项目test目录下创建了一个文件test.js,内容如下:
var deepMerge = require('deepmerge')
var test = deepMerge.all([[10, 20, 30], [40, 50]])
console.log('test: ', test);
// test: [ 10, 20, 30, 40, 50 ]
结论:
在文件中我们并没有在子项目的package.json 引入deepmerge的包,但是文件还是可以引用并执行的,所以在这里它引用到了跟项目中的element-ui的开发依赖,根项目也并没有引用deepmerge这个包,只是element-ui的依赖,直接铺平了,所以在这里也会导致幻影依赖问题的出现。
8、基于符号链接的 node_modules 结构
pnpm 的 node_modules
布局使用符号链接来创建依赖项的嵌套结构。
node_modules
中每个包的每个文件都是来自内容可寻址存储的硬链接。 假设您安装了依赖于 bar@1.0.0
的 foo@1.0.0
。 pnpm 会将两个包硬链接到 node_modules
如下所示:
node_modules
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
│ ├── index.js
│ └── package.json
└── foo@1.0.0
└── node_modules
└── foo -> <store>/foo
├── index.js
└── package.json
这是 node_modules
中的唯一的“真实”文件。 一旦所有包都硬链接到 node_modules
,就会创建符号链接来构建嵌套的依赖关系图结构。
您可能已经注意到,这两个包都硬链接到一个 node_modules
文件夹(foo@1.0.0/node_modules/foo
)内的子文件夹中。 这必要的:
- 允许包自行导入自己。
foo
应该能够require('foo/package.json')
或者import * as package from "foo/package.json"
。 - 避免循环符号链接。 依赖以及需要依赖的包被放置在一个文件夹下。 对于 Node.js 来说,依赖是在包的内部
node_modules
中或在任何其它在父目录node_modules
中是没有区别的。
安装的下一阶段是符号链接依赖项。 bar
将被符号链接到 foo@1.0.0/node_modules
文件夹:
node_modules └── .pnpm ├── bar@1.0.0 │ └── node_modules │ └── bar -> <store>/bar └── foo@1.0.0 └── node_modules ├── foo -> <store>/foo └── bar -> ../../bar@1.0.0/node_modules/bar
接下来,处理直接依赖关系。 foo
将被符号链接至根目录的 node_modules
文件夹,因为 foo
是项目的依赖项:
node_modules ├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo └── .pnpm ├── bar@1.0.0 │ └── node_modules │ └── bar -> <store>/bar └── foo@1.0.0 └── node_modules ├── foo -> <store>/foo └── bar -> ../../bar@1.0.0/node_modules/bar
这是一个非常简单的例子。 但是,无论依赖项的数量和依赖关系图的深度如何,布局都会保持这种结构。
让我们添加 qar@2.0.0
作为 bar
和 foo
的依赖项。 这是新的结构的样子:
node_modules ├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo └── .pnpm ├── bar@1.0.0 │ └── node_modules │ ├── bar -> <store>/bar │ └── qar -> ../../qar@2.0.0/node_modules/qar ├── foo@1.0.0 │ └── node_modules │ ├── foo -> <store>/foo │ ├── bar -> ../../bar@1.0.0/node_modules/bar │ └── qar -> ../../qar@2.0.0/node_modules/qar └── qar@2.0.0 └── node_modules └── qar -> <store>/qar
如您所见,即使图形现在更深(foo > bar > qar
),但目录深度仍然相同。
这种布局乍一看可能很奇怪,但它与 Node 的模块解析算法完全兼容! 解析模块时,Node 会忽略符号链接,因此当 foo@1.0.0/node_modules/foo/index.js
需要 bar
时,Node 不会使用在 foo@1.0.0/node_modules/bar
的 bar
,相反,bar
是被解析到其实际位置(bar@1.0.0/node_modules/bar
)。 因此,bar
也可以解析其在 bar@1.0.0/node_modules
中的依赖项。
我们也可以通过一张图,更加直观看下对应关系:
9、使用pnpm进行Node版本管理
安装 LTS 版本的 Node.js:
pnpm env use --global lts
安装 V16 版本的 Node.js:
pnpm env use --global 16
移除指定版本的 Node.js
pnpm env remove --global 14.0.0
列出本地或远程可用的 Node.js 版本
pnpm env list
10、包存储(store)
安装完pnpm之后,我们可以查看以下默认包储存的位置:
C:\Users\Administrator>pnpm store path
C:\Users\Administrator\AppData\Local\pnpm\store\v3
现在我在D盘创建一个项目,然后通过npm进行安装,如下所示:
发现在D盘创建了一个store(D:\.pnpm-store\v3)
我们也可以手动设置store的目录:
pnpm config set store-dir /path/to/.pnpm-store
包存储应与安装的位置处于同一驱动器和文件系统上,否则,包将被复制,而不是被链接。 这是由于硬链接的工作方式带来的一个限制,因为一个文件系统上的文件无法寻址另一个文件系统中的位置。
pnpm 在以下两种情况下的功能有所不同:
10.1 存储路径已指定
如果存储路径是通过 存储配置指定的,则存储与项目间的复制行为将会发生在不同的磁盘上。
如果您在磁盘 A
上执行 pnpm install
,则 pnpm 存储必须位于磁盘 A
。 如果 pnpm 存储位于磁盘 B
,则所有需要的包将被直接复制到项目位置而不是链接。
10.2 存储路径未指定
如果未设置存储路径,则会创建多个存储(每个驱动器或文件系统一个)。
Pnpm对应node_modules的结构:
.pnpm
.modules.yaml
Element-ui
Element-plus
下面2个包是对应的两个软链接。
打开element-plus这个软链接,看下目录结果,如下所示:
可以看到,没有发现element-plus的次级依赖文件,再找找,再.pnpm目录下找到了对应的次级依赖。
node_modules\.pnpm\registry.npmmirror.com+element-plus@2.3.7\node_modules\@element-plus
.pnpm/
以平铺的形式储存着所有的包,所以每个包都可以在这种命名模式的文件夹中被找到:
.pnpm/<name>@<version>/node_modules/<name>
.pnpm目录,我们称之为虚拟存储目录。
这个平铺的结构避免了 npm v2 创建的嵌套 node_modules
引起的长路径问题,但与 npm v3,4,5,6 或 yarn v1 创建的平铺的 node_modules
不同的是,它保留了包之间的相互隔离。
如上图所示,我们发现element-plus以及它的次级依赖都平铺安装在了同一个目录下,pnpm这样设计,也是为了避免了循环的软链。