即插即用
什么是 Yarn 即插即用?
Yarn 即插即用(通常称为 Yarn PnP)是 Yarn 现代版本中的默认安装策略。它可以换成更传统的方法(包括 node_modules
安装或基于符号链接的 pnpm 风格方法),但我们建议在创建新项目时使用它,因为它有许多改进。
它是如何工作的?
如果您查看项目中的文件,您可能会注意到没有 node_modules
文件夹。这很奇怪!我们经常在 Discord 上被问到该文件夹在哪里,因为人们认为 yarn install
在静默中失败了。
事实上,这是预期的!Yarn PnP 的工作方式是,它告诉 Yarn 生成一个 Node.js 加载器文件,以代替典型的 node_modules
文件夹。此加载器文件名为 .pnp.cjs
,其中包含有关项目依赖项树的所有信息,告知工具包在磁盘上的位置,并让他们知道如何解析 require 和 import 调用。
有什么优势?
Yarn PnP 解决各种问题。可以通过更智能的 node_modules
布局算法来解决其中一些问题(例如,基于符号链接的安装策略 pnpm-style 尝试执行的操作),但 PnP 是唯一可以解决所有这些问题的策略
最小的安装占用空间
Yarn PnP 安装通常执行一项操作:生成 Node.js 加载器文件 (.pnp.cjs
)。在其他包管理器中,大部分时间都花在执行 I/O 操作以将文件从一个位置复制到另一个位置,无论是像 npm 这样的磁盘,还是像 pnpm 这样的符号链接/硬链接。
跨磁盘共享安装
与前一点相关,Yarn PnP 允许在磁盘上的所有项目中重复使用相同的包工件。与 pnpm 不同,后者使用内容可寻址存储,其中每个包中的每个文件都需要硬链接到其最终目标,PnP 加载器通过其缓存路径直接引用包,从而消除了大量复杂性。
完美且正确的提升
典型的 node_modules
安装尝试通过提升包来优化生成的 node_modules
大小,但代价是幽灵依赖项的风险更高。不幸的是,即使是这些优化也有限度!一些依赖项模式会阻止安全提升,从而导致包重复和多次实例化。
幽灵依赖项保护
由于 Yarn 保留所有包及其依赖项的列表,因此它可以防止访问解析期间未考虑的依赖项,让你能够在这些问题深入代码库并危及应用程序在部署时的稳定性之前快速识别并修复这些问题。
有时会将此作为采用 Yarn PnP 的一项挑战。这意味着当其他包管理器似乎可以开箱即用时,可能会报告错误 - 也就是说,在添加、升级或删除不相关的依赖项时,可能会开始出现奇怪的故障。
虽然它确实增加了一些摩擦,但它是 Yarn 成为非常稳定的包管理器的关键部分。今天工作的应用程序将来不会突然中断,并且在你的 PR 合并后,你的同事不会面临看似随机的问题。
语义错误
你可能从未注意到,但是当 Node.js import 或 require 调用无效时,你只会收到一个通用错误作为回报,它并没有真正告诉你问题是什么或如何解决它
Uncaught Error: Cannot find module 'not-found'
Yarn PnP 不仅会准确告诉你是什么问题,还会告诉你涉及哪些包。例如,根据情况可能会发出以下两个错误消息
Error: Your application tried to access not-found, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound.
Required package: not-found
Required by: /path/to/my-project/
Error: awesome-plugin tried to access awesome-core (a peer dependency) but it isn't provided by its ancestors; this makes the require call ambiguous and unsound.
Required package: awesome-plugin
Required by: awesome-core
Ancestor breaking the chain: awesome-template
语义错误在很大程度上让你了解并解决由你的依赖项引起的问题。
使用起来是否困难?
在创建新项目时
如果你从头开始创建一个项目,那么你的项目本身应该几乎“开箱即用”。你可能需要不时使用 packageExtensions
来修复偶尔出现的幽灵依赖项,但这仍然不常见,而且这个过程也很简单。生态系统中的大多数工具都经过设计和测试,可以在 Yarn PnP 环境中很好地工作,因此很少出现问题。
一个值得注意的例外是 React Native / Expo,它需要使用典型的 node_modules
安装。
实际上,您将面临的主要问题将围绕 IDE 集成。所有 IDE 都对 Yarn PnP 有一定程度的支持,但通常您应该按照本指南中的某个过程进行操作,以确保正确解析您的所有导入内容。
在迁移现有项目时
在以前由 Yarn Classic 安装的项目中运行 yarn install
将导致 Yarn PnP 自动禁用,以使迁移更加顺畅。您仍将受益于现代版本中实现的增强稳定性和其他功能,并且可以决定是否在以后花费时间迁移到 PnP。
由于以下几个原因,现有项目可能更难迁移到 Yarn PnP
- 您已经开始拥有很多依赖项,因此可能列出幽灵依赖项的包数量将成比例地增加
- 它们可能被锁定在各自包的旧版本上,因此包含幽灵依赖项的可能性更高
- 您的脚本可能会无意中依赖于某些实现细节或幽灵依赖项,有时甚至在您没有意识到这一点的情况下。
这些都不是障碍,但它们意味着将现有项目迁移到 Yarn PnP 可能需要几天时间。但是,我们提供了工具来简化此过程中的某些部分,并且查看下面的 footguns 将帮助您更快地识别导致某些内容中断的方式,因此并非不可能。
请记住,迁移到 Yarn PnP 是可选的:您可以随时通过在项目的 .yarnrc.yml
文件中设置 nodeLinker: node-modules
设置来恢复到 node_modules
安装。
Footguns
对等依赖项
对等依赖项很强大,但很难实现 - 对于非 PnP 项目来说更是如此,这些项目必须在文件系统层次结构允许的范围内工作。
另一方面,Yarn PnP 没有此限制,并且将准确表示依赖项树中每个项目的对等依赖项 - 甚至工作区。如果工作区具有对等依赖项,并且此依赖项由其祖先的不同版本满足,那么该工作区将被实例化两次,每个唯一的“依赖项集”一次。
这是正确的行为,但如果你的项目大量使用对等依赖项而不确保它们始终由完全相同的版本满足,则可能会导致实例化工作区的数量意外激增。
共享二进制文件
Yarn 阻止了项目依赖的包中的幽灵依赖项,也阻止了自身代码中的幽灵依赖项 - 这是为了减少包在开发机器上工作但在发布后中断的可能性。
然而,当涉及到 bin 时,它会产生副作用。如果你的项目根目录中列出了 typescript
,则 tsc
二进制文件将在根包中可用,但仅在根项目中可用。换句话说,任何在脚本中使用 tsc
二进制文件的 workspace 都需要在其依赖项中声明它。
为了避免此类问题,一个好的建议是拥有一个“工具”工作区来包含你的基础架构工具和脚本,并让所有其他工作区依赖它。
常见问题
与 npm / pnpm 的兼容性
Yarn PnP 被设计为使用与其他包管理器完全相同的“公共接口”,而差异仅限于已经实现的细节。如果一个项目适用于 Yarn PnP,那么它应该适用于所有地方!
但有一个警告:反之并不总是成立。由于其他包管理器不会/不能强制正确列出依赖项,因此它们更容易意外地向其使用者发送幽灵依赖项。通过这种方式,使用 Yarn PnP 可以被视为对生态系统健康的一种良好实践!🙂
如何修复幽灵依赖项?
可以使用 packageExtensions
设置来解决幽灵依赖项,该设置允许你向依赖项树中的任何包添加新依赖项。例如,如果你遇到类似于 @babel/core 尝试访问 @babel/types,但它未在其依赖项中声明
的错误,你可以通过将以下内容添加到你的 .yarnrc.yml
文件中轻松修复它
packageExtensions:
"@babel/core@*":
dependencies:
"@babel/types": "*"
有时扩展 peerDependencies
字段而不是 dependencies
字段更有意义,这需要逐案解决。
为了避免你不得不添加太多 packageExtensions
条目,Yarn 团队维护了一个 生态系统中已知幽灵依赖项列表,我们自动修复这些依赖项。此列表由 Yarn 和 pnpm 使用,我们非常乐意合并那里的贡献。