node 模块现状

在我们执行 npm install 时,npm 会做出如下操作:

  1. 向 registry 查询获取模块的地址
  2. 根据 package.json 中的配置确定需要安装的模块版本
  3. 下载对应的压缩包,存放在 ~/.npm 目录
  4. 解压压缩包到当前项目的 node_modules 目录

而在 Node.js 中,我们提供给 require 方法的参数如果不是一个路径,也不是 node 的核心模块, node 将试图去当前目录的 node_modules 文件夹里搜索。如果当前目录的 node_modules 里没有找到, node 会继续试图在父目录的 node_modules 里搜索,这样递归下去直到根目录。

这些过程都需要进行大量的文件 I/O 操作,这无疑是非常低效的。为了解决这些问题,Facebook 提出了 Plug’n’Play(PnP) 方案。

PnP 原理

在 Yarn 中,当我们开启 PnP 后,Yarn 会生成一个 .png.js 文件来描述项目的依赖信息和所需模块的查找路径。同时,项目目录下不再需要一个 node_modules 目录,取而代之的是一个全局的缓存目录,项目所需依赖都可以从这个目录中获取。

使用方法

可以在项目目录下执行 yarn --pnp ,或直接在 package.json 中修改如下字段开启 PnP:

1
2
3
4
5
{
"installConfig": {
"pnp": true
}
}

如果你使用 CRA 来创建项目,也可以直接在命令中加入 --use-pnp

PnP 速度

按照官方的说法, Generating it makes up for more than 70% of the time needed to run yarn install with a hot cache.

在我的一个实际项目中,使用 npm i、yarn 和 PnP 安装依赖完成时间分别为 26.48s、19.71s 和 11.25s,提升极为可观。

扩展

pnpm

pnpm 使用了 symlink 来记录模块路径,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-> - a symlink (or junction on Windows)

node_modules
├─ foo -> .registry.npmjs.org/foo/1.0.0/node_modules/foo
└─ .registry.npmjs.org
├─ foo/1.0.0/node_modules
| ├─ bar -> ../../bar/2.0.0/node_modules/bar
| └─ foo
| ├─ index.js
| └─ package.json
└─ bar/2.0.0/node_modules
└─ bar
├─ index.js
└─ package.json

pnpm 在维护了模块层级的同时大幅度提升了安装速度,但是由于它的实现,无法保证和 npm 行为一致。

tink

tink 是 npm 官方提出的一种类似 PnP 的解决方案,和 .pnp.js 文件类似, tink 会在项目中生成一个 .package-map.json 文件用来记录各安装包内文件的 hash 值。目前 tink 依然处于测试阶段,在 npm 8 中我们将能尝试这一特性。