node_modules 问题

过去的安装方式很简单:运行yarn installYarn 时会生成一个node_modules目录,然后 Node 可以使用其内置的Node Resolution Algorithmopen in new window来使用该目录。在这种情况下,Node 不必首先知道“包”是什么:它只根据文件进行推理。“这个文件在这里存在吗?不:好的,让我们看看父文件node_modules。它在这里存在吗?仍然不存在:好的……”,它继续运行,直到找到正确的文件。由于以下几个原因,这个过程非常低效:

  • 这些node_modules目录通常包含大量文件。生成它们可以弥补运行所需时间的 70% 以上yarn install。即使有预先存在的安装也不会拯救你,因为包管理器仍然必须区分node_modules应该包含的内容。
  • 因为node_modules生成是一个 I/O 繁重的操作,包管理器除了做一个简单的文件复制之外没有太多的余地来优化它——即使它可以在可能的情况下使用硬链接或写时复制,它也会在进行一堆系统调用来操作磁盘之前,仍然需要区分文件系统的当前状态。
  • 因为 Node 没有包的概念,它也不知道文件是否应该被访问。完全有可能您编写的代码有一天在开发中有效,但后来在生产中中断,因为您忘记在package.json.
  • 即使在运行时,Node 解析也必须进行大量调用statreaddir以确定从何处加载每个所需文件。这是非常浪费的,也是启动 Node 应用程序花费如此多时间的部分原因。
  • 最后,node_modules文件夹的设计是不切实际的,因为它不允许包管理器正确地对包进行重复数据删除。尽管可以使用一些算法来优化树布局(提升open in new window),但我们仍然无法优化某些特定模式——不仅导致磁盘使用率高于所需,而且一些包在内存中被多次实例化.

修复 node_modules

Yarn 已经知道关于你的依赖树的所有信息——它甚至会为你将它安装在磁盘上。那么,为什么要由 Node 来查找你的包在哪里呢?相反,包管理器的工作应该是通知解释器包在磁盘上的位置,并管理包之间的任何依赖关系,甚至包的版本。这就是创建即插即用的原因。

在这种安装模式下(默认从 Yarn 2.0 开始),Yarn 生成单个文件而不是包含各种包副本.pnp.cjs的通常文件夹。node_modules.pnp.cjs文件包含各种映射:一个将包名称和版本链接到它们在磁盘上的位置,另一个将包名称和版本链接到它们的依赖项列表。有了这些查找表,Yarn 可以立即告诉 Node 在哪里可以找到它需要访问的任何包,只要它们是依赖关系树的一部分,并且只要这个文件被加载到您的环境中(下一节将详细介绍) )。

这种方法有多种好处:

  • 安装现在几乎是即时的。Yarn 只需要生成一个文本文件(而不是可能的数万个)。主要瓶颈是项目中依赖项的数量而不是磁盘性能。
  • 由于减少了 I/O 操作,安装更加稳定和可靠。尤其是在 Windows 上(批量写入和删除文件可能会触发与 Windows Defender 和类似工具的各种意外交互),I/O 繁重的node_modules操作更容易失败。
  • 完美优化依赖树(又名完美提升)和可预测的包实例化。
  • 生成的.pnp.cjs文件可以作为零安装open in new window工作的一部分提交到您的存储库,从而无需首先运行yarn install
  • 更快的应用程序启动!节点解析不必像以前那样迭代文件系统层次结构(很快就不必这样做了!)。

初始化即插即用

Yarn 会生成一个.pnp.cjs需要安装的文件,以便 Node 知道在哪里可以找到相关的包。这种注册通常是透明的:node通过您的条目之一执行的任何直接或间接命令scripts都会自动将该.pnp.cjs文件注册为运行时依赖项。对于绝大多数用例,以下内容将按照您的预期工作:

{
  "scripts": {
    "start": "node ./server.js",
    "test": "jest"
  }
}
1
2
3
4
5
6

对于一些剩余的边缘情况,可能需要一个小的设置:

  • 如果您需要运行任意 Node 脚本,请使用yarn nodeopen in new window解释器,而不是node. 这足以将.pnp.cjs文件注册为运行时依赖项。
yarn node ./server.js
1
  • 如果您在自动执行 Node 脚本的系统上操作(例如在 Google Cloud Platform 上(--此处需要参考--)),只需在 init 脚本顶部需要 PnP 文件并调用其setup函数即可。
require('./.pnp.cjs').setup();
1

作为一个快速提示,yarn node通常所做的只是将NODE_OPTIONS环境变量设置为使用--requireopen in new window来自 Node 的选项,与.pnp.cjs文件的路径相关联。如果您愿意,您可以自己轻松地应用此操作:

node -r ./.pnp.cjs ./server.js
NODE_OPTIONS="--require $(pwd)/.pnp.cjs" node ./server.js
1
2

即插即用loose模式

由于提升启发式不是标准化和可预测的,因此在严格模式下运行的 PnP 将阻止包需要未明确列出的依赖项;即使其他依赖项也依赖它。这可能会导致某些软件包出现问题。

为了解决这个问题,Yarn 提供了一种“松散”模式,这将导致 PnP 链接器与提升器协同工作node-modules- 我们将首先生成在典型安装中将被提升到顶层的软件包列表node_modules,然后记住这个列表,我们称之为“后备池”。

请注意,因为松散模式直接调用提升器,它遵循与链接器open in new windownode-modules使用的真正算法完全相同的实现!node-modulesopen in new window

在运行时,如果依赖项的任何版本最终在回退池中,仍然允许需要未列出的依赖项的包访问它们(可以使用pnpFallbackModeopen in new window调整哪些包完全被允许依赖回退池)。

请注意,备用池的内容是不确定的。如果依赖关系树包含同一个包的多个版本,则无法确定将哪个版本提升到顶层。因此,访问回退池的包仍然会生成警告(通过process.emitWarningopen in new window API)。

此模式提供了strictPnP 链接器和node_modules链接器之间的折衷方案。

为了启用loose模式,请确保该nodeLinkeropen in new window选项设置为pnp(默认)并将以下内容添加到您的本地.yarnrc.ymlopen in new window文件中:

pnpMode: loose
1

有关该pnpMode选项的更多信息。open in new window

警告

因为我们在解决错误时发出警告(而不是抛出错误),所以应用程序无法捕获它们。这意味着require如果缺少依赖项,尝试在 try/catch 块内尝试可选对等依赖项的常见模式将在运行时打印警告,即使它不应该。唯一的运行时含义是这样的警告可能会导致混淆,但可以放心地忽略它。

因此,从 2.1 版开始,即插即用loose模式将不再是默认模式(正如我们最初计划的那样)。它将继续作为替代方案得到支持,希望能够轻松过渡到默认和推荐的工作流程:即插即用strict模式。

备择方案

在 Plug'n'Play 被批准为主要安装策略之前的几年里,其他项目提出了节点解析算法的替代实现——通常是为了规避require.resolveAPI 的缺点。示例包括 Webpack ( enhanced-resolve)、Babel ( resolve)、Jest ( jest-resolve) 和 Metro ( metro-resolver)。这些替代方案应被视为与 Plug'n'Play 的适当集成所取代。

兼容性表

下面的兼容性表让您了解与社区中各种工具的集成状态。请注意,此处仅列出了 CLI 工具,因为前端库(例如react, vue, lodash, ...)不会重新实现节点解析,因此不需要任何特殊逻辑来利用 Plug'n'Play:

建议在此表中添加open in new window

原生支持

许多常见的前端工具现在原生支持即插即用!

项目名笔记
从 13+ 开始
通天塔resolve1.9 开始
创建反应应用程序从 2.0+ 开始
剑龙从 2.0.0-beta.14 开始
ESLint共享配置的一些兼容性问题(可使用@rushstack/eslint-patch 修复open in new window
盖茨比支持版本 ≥2.15.0、≥3.7.0
吞咽支持 4.0+ 版本
沙哑从 4.0.0-1+ 开始
笑话从 24.1+ 开始
Next.js从 9.1.2+ 开始
包裹从 2.0.0-nightly.212+ 开始
Preact CLI从 3.1.0+ 开始
更漂亮从 1.17+ 开始
卷起resolve1.9+开始
故事书从 6.0+ 开始
打字稿通过plugin-compatopen in new window(默认启用)
TypeScript-ESLint从 2.12+ 开始
VSCode-Stylelint从 1.1+ 开始
网络风暴从 2019.3+开始;请参阅编辑器 SDKopen in new window
网页包从 5+ 开始(插件open in new window可用于 4.x)

通过插件支持

项目名笔记
ESBuild通过@yarnpkg/esbuild-plugin-pnpopen in new window
VSCode-ESLint关注编辑器 SDKopen in new window
VSCode关注编辑器 SDKopen in new window
Webpack 4.xVia pnp-webpack-pluginopen in new window(5 岁以上原生)

不相容

以下工具不能用于纯即插即用安装(即使在松散模式下)。

**重要提示:**即使某个工具与 Plug'n'Play 不兼容,您仍然可以启用该node-modules插件open in new window。只需按照说明操作open in new window,您就可以在一分钟内准备好 🙂

项目名笔记
流动关注yarnpkg/berry#634open in new window
反应原生关注react-native-community/cli#27open in new window
普鲁米关注pulumi/pulumi#3586open in new window
VSCode 扩展管理器 (vsce)使用启用插件的vsce-yarn-patchopen in new window分支node-modules在合并 microsoft/vscode-vsce#493open in new window之前需要 fork ,因为vsce当前使用已删除的yarn list命令
雨果雨果管道期待一个node-modules目录。启用node-modules插件
诏书按照rescript-lang/rescript-compiler#3276open in new window

此列表根据我们从 v2 开始发布的最新版本保持更新。如果您发现自己的项目中有问题,请先尝试升级 Yarn 和有问题的包,然后随时提出问题。也许是公关?😊

经常问的问题

为什么不使用导入地图?

Yarn Plug'n'Play 提供语义错误(解释为什么一个包不能从另一个包访问的确切原因)和一个合理的 JS APIopen in new window来解决require.resolve. 这些是导入地图无法自行解决的功能。这在这个线程open in new window中有更详细的回答。

我们今天陷入这种混乱的一个主要原因是,最初的node_modules设计试图将包抽象出来,以便提供一个可以在没有任何包概念的情况下工作的通用系统。这成为一个挑战,促使许多实施者提出自己的解释。导入地图也存在同样的缺陷。

包存储在 Zip 档案中:我如何访问他们的文件?

使用 PnP 时,包被直接存储在 Zip 存档中并从缓存中访问。PnP 运行时 ( .pnp.cjs) 会自动修补 Node 的fs模块,以添加对访问 Zip 存档中文件的支持。这样,您不必做任何特别的事情:

const { readFileSync } = require(`fs`)

// Looks similar to `/path/to/.yarn/cache/lodash-npm-4.17.11-1c592398b2-8b49646c65.zip/node_modules/lodash/ceil.js`
const lodashCeilPath = require.resolve(`lodash/ceil`)

console.log(readFileSync(lodashCeilPath))
1
2
3
4
5
6

后备模式

回到 PnP 第一次实现的时候,兼容性还没有现在那么好。为了帮助过渡,我们设计了一种回退机制:如果一个包试图访问一个未列出的依赖项,如果顶级包将其列为一个依赖项,它仍然可以解决它。我们允许这样做是因为没有分辨率歧义,因为任何项目中都有一个顶级包。不幸的是,这可能会导致令人困惑的行为,具体取决于您的项目设置方式。当这种情况发生时,即插即用总是正确的,它在不在工作区时工作的唯一原因是由于一些额外的松懈。

此行为只是一个补丁,最终将被删除以消除任何混乱。pnpFallbackModeopen in new window您现在可以通过设置来为此做准备none,这将完全禁用回退机制。install

前言

从 npm 到 yarn,再到之后的 pnpm, ni 等安装工具

不断进步, 不断升级, 升级了哪些,又存在哪些问题

问题

  • 嵌套安装

    • 路径过长
    • 同一依赖, 多次安装
  • 扁平安装

    • 仅一个版本根据包的安装顺序被提升,且升级版本后,会存在新的问题(依赖提升的不确定性)
  • npm 分身

    • hoist 机制
  • 幽灵依赖

    • 项目未安装某个依赖,但因为安装的某个依赖中使用了该依赖, 导致可以项目中使用未安装的依赖, 但在项目升级后, 若去掉该依赖, 就会报错

    结构

node_modules
└─ foo
   ├─ index.js
   ├─ package.json
   └─ node_modules
      └─ bar
         ├─ index.js
         └─ package.json

1
2
3
4
5
6
7
8
9

npm

npm i
1

yarn

yarn
1

pnpm

pnpm i
1

ni

ni
1

Graph of the alotta-files results

img

上次更新:
(adsbygoogle = window.adsbygoogle || []).push({});