由 V. Sun 于 2018 年 2 月 15 日发布
虽然 yarn workspaces 非常棒,但社区的其他人尚未完全赶上单一仓库提升计划。引入 nohoist 是 yarn 原生支持的一种易于使用的机制,它可以让工作区协同原本不兼容的库。
我们希望此功能能缓解单一仓库开发人员的痛苦,并在效率(尽可能提升)和实用性(取消那些不适合工作区的库的封锁)之间取得平衡。
问题是什么?
首先,让我们快速了解提升在独立项目中的工作原理
为了减少冗余,大多数包管理器都采用某种提升计划来尽可能地提取和扁平化所有依赖模块,以将其存储在一个集中位置中。在独立项目中,可以将依赖树简化如下
通过提升,我们能够消除重复的“A@1.0”和“B@1.0”,同时保留版本变化(B@2.0)并维护相同的根 package-1/node_modules
。大多数模块爬虫/加载器/捆绑器都可以通过从项目根目录中遍历“node_modules”树来非常高效地定位模块。
然后是单一仓库项目,该项目引入了一个新的层次结构,这种结构不一定通过“node_modules” 链接。在这样的项目中,模块可能会分散在多个位置
yarn workspaces 可以通过将模块提升到其父项目的 node_modules:monorepo/node_modules
来跨子项目/包共享模块。考虑到这些包很大可能彼此依赖(拥有单一仓库的主要原因),即更高的冗余度,这种优化变得更加突出。
找不到模块!
虽然看来我们可以从项目根目录的 node_modules 访问所有模块,但我们通常在其本地项目中构建每个包,而模块在自己的 node_modules 中可能不可见。此外,并非所有爬虫都会遍历 符号链接。
因此,工作区开发人员在从子项目进行构建时常常会遇到“未找到模块”相关错误
- 无法从项目根目录“monorepo”找到模块“B@2.0”(无法按照符号链接进行操作)
- 无法从“package-1”找到模块“A@1.0”(不知道“monorepo”中上面模块树)
为了让此单一仓库项目从任何位置可靠地找到任何模块,它需要遍历每个 node_modules 树:“monorepo/node_modules”和“monorepo/packages/package-1/node_modules”。
为什么无法修复?
事实上,库所有者有很多方法来解决这些问题,例如多根、自定义模块映射、聪明的遍历计划等,但是
- 并非所有第三方库都有资源来适应单一仓库环境
- 最弱环节问题:javascript 很棒,这要归功于大量的第三方库。但是,这也意味着复杂的工具链仅仅和最弱环节一样强大。工具链深处的一个未适应的包可能会让整个工具变得毫无用处。
- 自举问题:例如,react-native 通过
rn-cli.config.js
提供了一种配置多根的方法。但是,它无法在自举过程中提供帮助,例如react-native init
或create-react-native-app
,它们在创建/安装应用之前无法访问任何此类工具。
当一项解决方案在单机项目中适用,但在单一仓库环境中却失效时,是令人沮丧的。理想的解决方案在于解决底层库,但现实往往不那么完美,我们都知道项目不能因此而久等……
什么是“nohoist”?
有没有一种简单但通用的机制,可以让这些不兼容的库在单一仓库环境中无缝运行?
事实证明,确实有,并且很方便,称为“nohoist”,该机制还展示在其他的单一仓库工具中,如 lerna。
“nohoist”使工作区可以消费与其提升方案尚未兼容的第三方库。其原理是禁用选中模块,使其不被提升到项目根目录。相反,它们与在独立的非工作区项目中一样,被放置在实际的(子)项目中。
由于许多第三方库已在独立项目中使用,在工作区内模拟此类环境的能力,应该可以解决许多已知的兼容性问题。
谨慎告示
虽然 nohoist 很有用,但也存在一些缺点。最显而易见的一点是,nohoist 模块可能在多个位置重复出现,从而抵消提升带来的好处。因此,我们建议在项目中将 nohoist 范围保持得尽可能小且明确。
在什么时候可以获得?
计划随 1.4.2 一起发布
如何使用?
使用 nohoist 非常简单。它由 package.json 中定义的 nohoist 规则驱动。从 1.4.2 开始,yarn 将采用新的工作区配置格式,以包含(可选)nohoist 设置
// flow type definition:
export type WorkspacesConfig = {
packages?: Array<string>,
nohoist?: Array<string>,
};
例如
- 在一个没有 nohoist 的私有项目根目录下
"workspaces": { "packages": ["packages/*"], "nohoist": ["**/react-native", "**/react-native/**"] }
- 在一个没有 nohoist 的私有项目根目录下
"workspaces": { "packages": ["packages/*"], }
- 在一个有 nohoist 的私有项目子目录下
"workspaces": { "nohoist": ["react-native", "react-native/**"] }
注意:对于不需要 nohoist 的项目,旧工作区格式将继续得到支持。
nohoist 规则只是一组 glob 模式,用于匹配其依赖关系树中的模块路径。模块路径是依赖关系树的虚拟路径,不是实际的文件路径,因此无需在 nohoist 的模式中指定“node_modules”或“packages”。
说明
让我们以一个简化后的伪示例来说明如何使用 nohoist 来阻止 react-native 在我们的单一仓库项目“monorepo”中被提升。在“monorepo”下有 3 个包:A、B 和 C
在 yarn install
之前的文件系统
项目根目录“monorepo”中的 package.json 文件
// monorepo's package.json
...
"name": "monorepo",
"private": true,
"workspaces": {
"packages": ["packages/*"],
"nohoist": ["**/react-native", "**/react-native/**"]
}
...
让我们详细看看配置
范围:私有
nohoist 仅适用于私有包,因为工作区仅适用于私有包。
匹配的 glob 模式
在内部,yarn 会根据模块的“原始”(提升前)包依赖关系为每个模块构建一个虚拟模块路径。如果该路径匹配提供的 nohoist 模式,则该模块将改为提升到最接近的子项目/包。
模块路径
A
- monorepo/A
- monorepo/A/react-native
- monorepo/A/react-native/metro
- monorepo/A/Y
B
- monorepo/B
- monorepo/B/X
- monorepo/B/X/react-native
- monorepo/B/X/react-native/metro
C
- monorepo/C
- monorepo/C/Y
nohoist 模式
“**/react-native”:这告诉 yarn 不管它在何处,都不能提升 react-native 包本身。(浅层)
- globstar “**”的使用匹配了 react-native 之前 0 到 n 个元素,这意味着它将匹配任何出现在该路径上的 react-native 出现。
- 该模式以“react-native”结尾,意味着 react-native 的依赖关系(如“react-native/metro”)不会与该模式匹配,因此称为“浅层”。
“**/react-native/**”:这告诉 yarn 不要提升任何 react-native 的依赖库及其依赖库。(深层)
- 与上面提到的前缀全局星号不同,模式以“**”结尾,后缀全局星号匹配react-native之后的1到n个元素,这意味着仅react-native的依赖项将匹配此模式,但react-native本身不会匹配此模式。
- 不只是react-native的直接依赖项匹配此模式,它们的依赖项等也将匹配,因此出现了“深层”一词。
将这些2个模式(浅层+深层)组合起来,它们指示yarn不要提升react-native及其所有依赖项。
让我们尝试一些其他模式
- 如果我们只想将react-native nohoist应用于package A,该怎么办?
"nohoist": ["A/react-native", "A/react-native/**"]
- 如果package A在构建react-native应用程序时也需要包括package C,该怎么办?
"nohoist": ["A/react-native", "A/react-native/**", "A/C"]
将在package A的node_modules下创建一个package C的符号链接。
提升后的文件结构
在yarn install
之后,文件结构将如下所示
模块X和Y已提升到根目录,因为“monorepo/A/Y”、“monorepo/B/X”和“monorepo/C/Y”不匹配任何nohoist模式。请注意,即使“monorepo/B/X/react-native”匹配nohoist模式,“monorepo/B/X”也不匹配。因此,react-native模块将保存在package“B”中,而它们的原始父项“X”将提升到根目录。
react-native和metro都分别保存在package A和B下,因为它们匹配react-native nohoist模式。请注意,即使B不直接依赖react-native,它们仍提升到“B”,就像在独立项目中一样。
如何关闭nohoist?
nohoist默认开启。如果yarn在私有package.json中看到nohoist配置,它将使用该配置。
要关闭nohoist,只需从package.json中删除nohoist配置或通过.yarnrc或yarn config set workspaces-nohoist-experimental false
设置标志workspaces-nohoist-experimental false
。
工作实例
现在你对nohoist的工作原理有一些基本了解,是时候使用真实的东西来进行操作了…
以下是我们开发nohoist时使用的测试项目。它们现在可在yarn-nohoist-examples中获取。
- 在yarn工作空间中创建react-native: => 确认我们完全可以像在独立环境中一样遵循react-native指南
- 创建一个更贴近真实情况的单体库项目,其中包括react和react-native: => 确保nohoist适用于此命令和更高级别的用例。
这些是工作实例,即,你应该可以按照其中的说明进行克隆并运行。如果不是,请告知我们。
调查
如果事情没有按预期的那样发生,该怎么办?惊讶于本地node_modules中有多少个模块或根本没有模块?Yarn有一个功能强大的“why”命令,该命令可以报告其提升理由,以便你可以调查并满足你的好奇心…
这可能最好通过一个实际的示例来解释
结论
nohoist是新的,很可能需要几轮的完善。如果有些事情看起来不对,请告诉我们。它已经让我们的工作空间变得更简单,希望它对你也有同样的作用。
我们还希望号召库的所有者调整你们的库以适应单体库环境,所以也许我们可以有一天废除nohoist,并且所有可共享模块都可以提升到它们应有的位置…
引用
- nohoist最初的提议:RFC #86
- nohoist PR: #4979
- 工作空间简介:Yarn中的工作空间