PNPM(高性能的npm)介绍

目录

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的,会在虚拟系统中进行寻址,这样可以避免项目重复的进行安装。

  1. 只有依赖库不同和版本不一样才会添加到pnpm的虚拟系统中,所以根据依赖库+版本号进行判断,是否添加到pnpm的虚拟系统中。
  2. 所有文件都存储到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.cmdpnpx.cmdpnpm等)。完成后,再次安装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:

  1. 未在package.json指定这个包的版本,随着版本的升级,依赖也会进行升级,这可能会导致原来引用包的方法,可能在新版本中已经不存在了。
  2. 如果别人引用这个包,或者别人未在开发依赖中安装这个包,就会导致缺少依赖的错误,或者导致这个未知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 中每个包的每个文件都是来自内容可寻址存储的硬链接。 假设您安装了依赖于 [email protected] 的 [email protected]。 pnpm 会将两个包硬链接到 node_modules 如下所示:

node_modules

└── .pnpm

    ├── [email protected]

    │   └── node_modules

    │       └── bar -> <store>/bar

    │           ├── index.js

    │           └── package.json

    └── [email protected]

        └── node_modules

            └── foo -> <store>/foo

                ├── index.js

                └── package.json

这是 node_modules 中的唯一的“真实”文件。 一旦所有包都硬链接到 node_modules,就会创建符号链接来构建嵌套的依赖关系图结构。

您可能已经注意到,这两个包都硬链接到一个 node_modules 文件夹([email protected]/node_modules/foo)内的子文件夹中。 这必要的:

  1. 允许包自行导入自己。 foo 应该能够 require('foo/package.json') 或者 import * as package from "foo/package.json"
  2. 避免循环符号链接。 依赖以及需要依赖的包被放置在一个文件夹下。 对于 Node.js 来说,依赖是在包的内部 node_modules 中或在任何其它在父目录 node_modules 中是没有区别的。

安装的下一阶段是符号链接依赖项。 bar 将被符号链接到 [email protected]/node_modules 文件夹:

node_modules

└── .pnpm

    ├── [email protected]

    │   └── node_modules

    │       └── bar -> <store>/bar

    └── [email protected]

        └── node_modules

            ├── foo -> <store>/foo

            └── bar -> ../../[email protected]/node_modules/bar

接下来,处理直接依赖关系。 foo 将被符号链接至根目录的 node_modules 文件夹,因为 foo 是项目的依赖项:

node_modules

├── foo -> ./.pnpm/[email protected]/node_modules/foo

└── .pnpm

    ├── [email protected]

    │   └── node_modules

    │       └── bar -> <store>/bar

    └── [email protected]

        └── node_modules

            ├── foo -> <store>/foo

            └── bar -> ../../[email protected]/node_modules/bar

这是一个非常简单的例子。 但是,无论依赖项的数量和依赖关系图的深度如何,布局都会保持这种结构。

让我们添加 [email protected] 作为 bar 和 foo 的依赖项。 这是新的结构的样子:

node_modules

├── foo -> ./.pnpm/[email protected]/node_modules/foo

└── .pnpm

    ├── [email protected]

    │   └── node_modules

    │       ├── bar -> <store>/bar

    │       └── qar -> ../../[email protected]/node_modules/qar

    ├── [email protected]

    │   └── node_modules

    │       ├── foo -> <store>/foo

    │       ├── bar -> ../../[email protected]/node_modules/bar

    │       └── qar -> ../../[email protected]/node_modules/qar

    └── [email protected]

        └── node_modules

            └── qar -> <store>/qar

如您所见,即使图形现在更深(foo > bar > qar),但目录深度仍然相同。

这种布局乍一看可能很奇怪,但它与 Node 的模块解析算法完全兼容! 解析模块时,Node 会忽略符号链接,因此当 [email protected]/node_modules/foo/index.js 需要 bar 时,Node 不会使用在 [email protected]/node_modules/bar 的 bar,相反,bar 是被解析到其实际位置([email protected]/node_modules/bar)。 因此,bar 也可以解析其在 [email protected]/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\[email protected]\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这样设计,也是为了避免了循环的软链。 

猜你喜欢

转载自blog.csdn.net/u014388408/article/details/131407270