任务依赖项
当您表达任务之间的关系时,Turborepo 才能发挥最大的威力。我们将这些关系称为“依赖项”,但它们与您从 package.json
文件中安装的包依赖项不同。虽然 Turborepo 确实理解您的工作区,但它不会自动在它们的任务之间建立任何关系,除非您在 turbo.json
中通过 dependsOn
配置表达它们。
让我们逐步了解如何使任务依赖于其他任务的一些常见模式。
来自同一工作区
可能有一些任务需要在其他任务之前运行。例如,build
可能需要在 deploy
之前运行。
如果两个任务都在同一个工作区中,您可以像这样指定关系
{
"$schema": "https://turbo.rust-lang.net.cn/schema.json",
"pipeline": {
"build": {},
"deploy": {
// A workspace's `deploy` task depends on the `build` task of the same workspace.
"dependsOn": ["build"]
}
}
}
这意味着,每当运行 turbo deploy
时,build
也会在同一个工作区中运行。
来自依赖工作区
Monorepo 中的常见模式是声明工作区的 build
任务应该只在它依赖的所有工作区的 build
任务完成后运行。
这可能令人困惑,因为它指的是工作区依赖项和任务依赖项,它们是不同的概念。工作区依赖项是 dependencies
和 devDependencies
在 package.json
中,而任务依赖项是 dependsOn
键在 turbo.json
中。
^
符号(称为“插入符号”)明确声明该任务依赖于它所依赖的工作区中的任务。
{
"$schema": "https://turbo.rust-lang.net.cn/schema.json",
"pipeline": {
"build": {
// A workspace's `build` command depends on its dependencies'
// and devDependencies' `build` commands being completed first
"dependsOn": ["^build"],
}
}
}
使用上面的配置,如果应用程序从另一个工作区安装了包,则该包的 build
脚本将始终在应用程序的 build
脚本之前运行。
来自任意工作区
有时,您可能希望工作区任务依赖于另一个工作区任务。这对于从 lerna
或 rush
迁移的仓库特别有用,在这些仓库中,任务默认情况下在不同的阶段运行。有时,这些配置会做出无法在简单的 pipeline
配置中表达的假设,如上所示。或者,您可能只是想在 CI/CD 中使用 turbo
时表达应用程序或微服务之间的任务序列。
对于这些情况,您可以在 pipeline
配置中使用 <workspace>#<task>
语法来表达这些关系。下面的示例描述了 frontend
应用程序的 deploy
脚本,它依赖于 backend
的 deploy
和 health-check
脚本,以及 ui
工作区的 test
脚本。
{
"$schema": "https://turbo.rust-lang.net.cn/schema.json",
"pipeline": {
// Explicit workspace-task to workspace-task dependency
"frontend#deploy": {
"dependsOn": ["ui#test", "backend#deploy", "backend#health-check"]
}
}
}
对于 frontend#deploy
的这种显式配置似乎与 test
和 deploy
任务配置冲突,但事实并非如此。由于 test
和 deploy
不依赖于其他工作区(例如 ^<task>
),它们可以在其工作区的 build
和 test
脚本完成后随时执行。
注意
- 虽然这种
<workspace>#<task>
语法是一个有用的应急措施,但我们通常建议将其用于部署编排任务(如运行状况检查),而不是构建时依赖项,以便 Turborepo 可以更有效地优化这些任务。 - 工作区任务不会继承缓存配置。您必须在此时重新声明
outputs
。 <workspace>
必须与工作区package.json
中的name
键匹配,否则该任务将被忽略。
无依赖项
空依赖项列表(dependsOn
未定义或为 []
)意味着在该任务之前不需要运行任何内容!毕竟,它没有依赖项。
{
"$schema": "https://turbo.rust-lang.net.cn/schema.json",
"pipeline": {
// A workspace's `lint` command has no dependencies and can be run any time.
"lint": {}
}
}
任务之外的依赖项
假设您有一个通用的 ui
包,您在两个应用程序 docs
和 web
中使用它。
apps/
docs/package.json # Depends on ui
web/package.json # Depends on ui
packages/
ui/package.json # No workspace dependencies
turbo.json
package.json
您已经在工作区中编写了一些 TypeScript 代码,现在是时候运行 tsc
来检查您的类型了。这里有两个要求
- 所有类型检查并行运行以保持速度:因为您的类型检查结果彼此不依赖,所以您可以并行运行它们。
- 依赖项更改应导致缓存未命中:如果
ui
包发生更改,则docs
或web
中的类型检查任务应该知道缓存未命中。
为了实现这一点,您将在图中创建一个假的递归任务并依赖于它
{
"$schema": "https://turbo.rust-lang.net.cn/schema.json",
"pipeline": {
"topo": {
"dependsOn": ["^topo"]
},
"typecheck": {
"dependsOn": ["topo"]
}
}
}
由于 topo
任务不存在于您的脚本中,因此 Turborepo 将“立即”完成该任务,然后查看任何依赖于该工作区的任务。因此,您的任务将并行执行,同时仍然了解它们在任务图中与其他工作区的关联关系。
这里的名称 topo
不是一个特殊名称。它是“拓扑”的缩写,因此有助于表明它存在的原因,但您可以随意命名此任务。
为什么这样做有效?
我们可以通过查看几乎满足我们要求的管道来更深入地了解为什么这样做有效。
您可以通过从任务定义中省略 dependsOn
来实现任务的并行性,如下所示
{
"$schema": "https://turbo.rust-lang.net.cn/schema.json",
"pipeline": {
"typecheck": {} // Uh oh, not quite!
}
}
您的 typecheck
任务将成功并行运行 - 但它们不会了解其工作区依赖项!
我们可以使用以下步骤来演示这一点
- 运行
turbo typecheck
- 更改
ui
包中的一些源代码 - 运行
turbo typecheck --filter=web
如果您这样做,您将在步骤 3 中命中缓存 - 但您不应该这样做!您可能在 web
工作区中创建了一个类型错误,该错误来自 ui
包代码中的更改。步骤 3 中的缓存命中将是不正确的,会隐藏您的类型错误。
为了解决这个问题,您可以选择直接依赖于您的拓扑依赖项图,就像您对 build
任务所做的那样
{
"$schema": "https://turbo.rust-lang.net.cn/schema.json",
"pipeline": {
"typecheck": {
"dependsOn": ["^typecheck"] // Uh oh, not quite!
}
}
}
现在您有了正确的缓存行为:当 ui
代码发生更改时,web
将错过缓存。这很好 - 但我们刚刚失去了使我们的管道执行速度如此快的并行性。ui
工作区中的 typecheck
任务现在必须完成,然后 web
中的任务才能开始。
如果我们能够依赖于 ui
中“立即完成”的任务,从而使依赖工作区中的 typecheck
命令尽早开始呢?
这就是“假”topo
任务的用武之地
{
"$schema": "https://turbo.rust-lang.net.cn/schema.json",
"pipeline": {
"topo": {
"dependsOn": ["^topo"]
},
"typecheck": {
"dependsOn": ["topo"]
}
}
}
在这个管道中,我们声明了一个名为 topo
的“合成”任务。由于我们在任何 package.json
文件中都没有 topo
脚本,turbo typecheck
管道将直接运行所有并行的 typecheck
脚本,满足我们的第一个要求。
但是这个 topo
任务还会从 web
到 ui
,以及从 docs
到 ui
创建一个“合成”工作区任务依赖关系。这意味着,当你在 ui
中更改代码时,你也会在 web
和 docs
的工作区中获得缓存失效,满足第二个要求。
管道声明 typecheck
依赖于 topo
任务,而 topo
依赖于 ^topo
。用英语来说,这意味着 同一个 工作区的 topo
任务必须在所有 typecheck
任务之前运行,而所有 包依赖项 的 topo
任务必须在 topo
任务本身之前运行。
你可能会问,为什么 typecheck
不直接依赖于 ^topo
?因为我们希望我们的工作区通过合成任务 递归地 连接包依赖项。如果 typecheck
依赖于 ^topo
,turbo
将在第一层依赖项之后停止向图中添加内容。