终结依赖困扰,pnpm带给你畅快无比的开发体验!

674 阅读6分钟

pnpm 简介

什么是 pnpm

pnpm(performance npm) 官方文档是这样定义的:

Fast, disk space efficient package manager

pnpm 是新一代包管理工具,它的优势在于:

1. Fast

pnpm is up to 2x faster than the alternatives

以 React 包为例来对比一下,可以看到,pnpm 在绝大多数场景下,包安装的速度都是明显优于 npm/yarn,速度会比 npm/yarn 快 2-3 倍。

4201432510.png

补充

yarn 还有 PnP 安装模式,直接去掉 node_modules,将依赖包内容写在磁盘,节省了 node 文件 I/O 的开销,这样也能提升安装速度。(具体原理可参考这篇文章

2. Efficient

Files inside node_modules are cloned or hard linked from a single content-addressable storage

pnpm 内部使用基于内容寻址的文件系统来存储磁盘上所有的文件,这个文件系统出色的地方在于:

  • 不会重复安装同一个包。用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用 hardlink(参考文章)。
  • 即使一个包的不同版本,pnpm 也会极大程度地复用之前版本的代码。举个例子,比如 lodash 有 100 个文件,更新版本之后多了一个文件,那么磁盘当中并不会重新写入 101 个文件,而是保留原来的 100 个文件的 hardlink,仅仅写入那一个新增的文件。

3. Supports monorepos

pnpm has built-in support for multiple packages in a repository

随着前端工程的日益复杂,越来越多的项目开始使用 monorepo。之前对于多个项目的管理,我们一般都是使用多个 git 仓库,但 monorepo 的宗旨就是用一个 git 仓库来管理多个子项目,所有的子项目都存放在根目录的packages目录下,那么一个子项目就代表一个package。如果你之前没接触过 monorepo 的概念,建议仔细看看这篇文章以及开源的 monorepo 管理工具lerna,项目目录结构可以参考一下 babel 仓库

pnpm 与 npm/yarn 另外一个很大的不同就是支持了 monorepo,体现在各个子命令的功能上,比如在根目录下pnpm add A -r, 那么所有的 package 中都会被添加 A 这个依赖,当然也支持 --filter 字段来对 package 进行过滤。

4. Strict

pnpm creates a non-flat node_modules by default, so code has no access to arbitrary packages

之前在使用 npm/yarn 的时候,由于 node_module 的扁平结构,如果 A 依赖 B, B 依赖 C,那么 A 当中是可以直接使用 C 的,但问题是 A 当中并没有声明 C 这个依赖。因此会出现这种非法访问的情况。但 pnpm 脑洞特别大,自创了一套依赖管理方式,很好地解决了这个问题,保证了安全性。

依赖管理方式

npm/yarn 的依赖管理方式

4201445900.png npm install 安装是在确定了包的版本后,获取包信息,构建依赖树然后进行扁平化处理,将所有依赖包安装在同一层级。

在使用依赖包的过程中,所有依赖都在同一层级去找,解决了 npm@3 之前的版本以下问题:

  • 将所有依赖包安装成树结构之后,大量重复的安装了依赖,造成安装速度非常慢,磁盘空间管理非常差
  • 依赖的层级太深导致文件路径过长

但是依赖扁平化后,就会出现下列问题:

  1. 依赖结构的不确定性
  2. 模块可以访问没有声明依赖的包
  3. 扁平化一个依赖树的算法非常复杂

第一点中的「不确定性」是如何体现的呢?假如现在项目依赖两个包 foo 和 bar,这两个包的依赖又是这样的: 4201451335.png 那么 npm/yarn install 的时候,通过扁平化处理之后,究竟是这样 4201451204.png 还是这样? 4201451086.png 答案是: 都有可能。取决于 foo 和 bar 在package.json中的位置,如果 foo 声明在前面,那么就是前面的结构,否则是后面的结构。

这就是为什么会产生依赖结构的不确定问题,也是 lock 文件诞生的原因,无论是package-lock.json还是yarn.lock,都是为了保证 install 之后都产生确定的 node_modules 结构。

尽管如此,npm/yarn 本身还是存在扁平化算法复杂和 package 非法访问的问题,影响性能和安全。

pnpm 的依赖管理方式

树结构会出现问题,扁平化后也会出现问题,那么 pnpm 是怎么解决的呢?

例子

以安装 express 为例,通过pnpm install express安装后查看 node_modules:

▾ node_modules
  ▸ .pnpm
  ▸ express
  .modules.yaml

我们直接就看到了 express,但值得注意的是,这里仅仅只是一个软链接,里面并没有 node_modules 目录,如果是真正的文件位置,那么根据 node 的包加载机制,它是找不到依赖的。那么它真正的位置在哪呢? 我们继续在 .pnpm 当中寻找,最后在 .pnpm/express@4.17.1/node_modules/express下面找到了!

▾ node_modules
  ▾ .pnpm
    ▸ accepts@1.3.7
    ▸ array-flatten@1.1.1
    ...
    ▾ express@4.17.1
      ▾ node_modules
        ▸ accepts
        ▸ array-flatten
        ▸ body-parser
        ▸ content-disposition
        ...
        ▸ etag
        ▾ express
          ▸ lib
            History.md
            index.js
            LICENSE
            package.json
            Readme.md

并且 express 的依赖都在.pnpm/express@4.17.1/node_modules下面,这些依赖也全都是软链接,例如.pnpm/express@4.17.1/node_modules/etag链接到的真实地址是.pnpm/etag@1.8.1/node_modules/etag

解释

.pnpm 目录下虽然呈现的是扁平的目录结构,但仔细想想,顺着软链接慢慢展开,其实就是嵌套的结构!将包本身和依赖放在同一个 node_module 下面,与原生 Node 完全兼容,又能将 package 与相关的依赖很好地组织到一起,设计十分精妙。

根目录下的 node_modules 下面不再是眼花缭乱的依赖,而是跟 package.json 声明的依赖基本保持一致。即使 pnpm 内部会有一些包会设置依赖提升,会被提升到根目录 node_modules 当中,但整体上,根目录的 node_modules 比以前还是清晰和规范了许多,所以它不会存在之前提到的能访问没有声明依赖的包这个问题。

然后使用 Store + Links 和文件资源进行关联。简单说pnpm把会包下载到一个公共目录,如果某个依赖在 sotre 目录中存在了话,那么就会直接从 store 目录里面去 hard-link,避免了二次安装带来的时间消耗,如果依赖在 store 目录里面不存在的话,就会去下载一次。

4201453845.png

pnpm 日常使用及遇到的问题

日常使用

// 安装 axios
pnpm i

// 安装 axios
pnpm add axios
// 安装 axios 并将 axios 添加至 devDependencies
pnpm add axios -D
// 安装 axios 并将 axios 添加至 dependencies
pnpm add axios -S

// 卸载 axios
pnpm remove axios
// monorepo 项目的 package-a 项目中移除 axios
pnpm remove axios --filter package-a

一些问题

使用 pnpm install --shamefully-hoist

如果依赖一直有问题,可以使用pnpm install --shamefully-hoist创建一个扁平node_modules目录结构, 类似于 npm 或 yarn

解决幽灵依赖时,安装默认的包导致报错

先使用 npm 安装,生成package-lock.json, 安装缺少的包时,使用 lock 里面的版本

即使删除了 node_modules 和 lock 文件,安装时,特定的包还是报错

比如我们在升级时,一个包把最新的版本删除了。导致安装时一直失败。可以尝试使用pnpm store prune来删除