为什么需要任务编排?
在 monorepo(多 package)架构的项目中,package 和 package 之间肯定会存在依赖关系,因此在运行一个 package 的任务执行时,可能需要依赖其他 package 先执行对应的任务,比如下面这个例子:

myReactApp 项目依赖于其他库,因此,在运行 myReactApp 的 build 任务时,需要首先执行其依赖的 package 的 build 任务,以便 myReactApp 可以使用这些 package 的构建产物。虽然开发者可以自己管理这些关系并设置自定义脚本以按正确的顺序构建所有项目(例如,首先构建shared-ui、utils,然后构建 product1、product2,最后构建myReactApp),但是这种方法是不可扩展的,并且随着项目的迭代,需要不断维护
。
为了解决这一问题,Nx 提供了智能的任务编排功能,而任务编排的核心功能依赖于项目关系图,即 Nx 能够分析并展示 workspace 中所有库之间的依赖关系图,开发者只需要执行 npx nx graph
命令就能查看整个工作区的依赖关系图。

配置任务依赖
Nx 允许开发者以"规则"的形式定义任务依赖关系,并在运行任务时遵循这些规则。任务的依赖关系可以全局定义也可以局部定义,全局定义任务依赖关系可以在根目录的 nx.json 文件中定义:
jsx
{
...
"targetDefaults": {
"build": {
"dependsOn": ["^build", "prebuild"]
},
"test": {
"dependsOn": ["build"]
}
}
}
除此之外,开发者还可以在每个 package 下的 project.json 或者 package.json 文件中单独为每个 package 定义任务依赖关系:
jsx
// package.json
{
"name": "myreactapp",
"dependencies": {},
"devDependencies": {},
...
"nx": {
"targets": {
"build": {
"dependsOn": ["^build", "prebuild"]
},
"test": {
"dependsOn": ["build"]
}
}
}
}
// proejct.json
{
"name": "myreactapp",
...
"targets": {
"prebuild": {
"command": "echo Prebuild"
},
"build": {
"command": "echo Build",
"dependsOn": ["^build", "prebuild"]
}
}
}
在运行 nx build myreactapp
时,上述配置将告诉 Nx:
- 在 myreactapp 中运行 build 命令。
- 在运行 build 命令之前先运行 prebuild 命令,因为在配置中定义了 build 命令和 prebuild 命令之间的依赖关系("dependsOn": ["prebuild"])。
- 在运行 build 命令之前先运行所有依赖的 package 对应的 build 命令,因为在配置中定义了 build 命令和所有依赖的 package 对应的 build 命令之间的依赖关系("dependsOn": ["^build"])。
并行执行任务
Nx 使用了 Node.js 提供的 Child Process API 来生成子进程,并在子进程中执行任务,以此来达到并行执行任务的目的,默认值是 3。有了任务并行机制,执行 nx build myreactapp 的构建流程可能如下:

如果想调整并发量,可以在执行 build 命令时传入 parallel 变量:
jsx
npx nx build myreactapp --parallel=5
同时 Nx 也允许开发者全局配置并发量,可以在根目录的 nx.json 文件中配置:
jsx
// nx.json
{
"parallel": 5
}
常见陷阱
循环依赖
在 monorepo(多 package)结构下,如果开发者将代码划分成定义明确的内聚单元,那即使是一个小型组织最终也会拥有数十或数百个package。如果他们都能自由地相互依赖,最终就会出现混乱,工作空间将变得难以管理,常见的问题就是循环依赖。
例如有四个 package,libA 依赖 libB,libB 依赖 libC,libC 依赖 libD,最后 libD 又依赖 libA,最终形成的依赖关系图如下:

Nx 为了帮助开发者解决这一问题,提出了enforce module boundaries 的方案,即通过声明的方式对项目如何相互依赖施加约束,可查看官方详细介绍。在项目中配置 enforce module boundaries 主要有两步:
-
为 package 增加标签。
Nx 引入了标签功能,开发者可以根据 package 的功能类型为 package 打上相应的标签,例如 scope:module 表示功能模块相关的 package,scope:utils 表示一个常用的工具函数,scope:components 表示业务性的通用组件,scope:shared-ui 表示基础的ui组件等。
下面分别对这四个 package 打上标签,以 libA 为例,我如果想给他添加一个 scope:module 标签,可直接在 project.json 或者 package.json 中配置:
jsx// package.json { // ... more project configuration here "nx": { "tags": ["scope:module"] } } // project.json { // ... more project configuration here "tags": ["scope:module"] }
libB 打上scope:components 标签,libC 和 libD 打上 scope:utils 标签。
-
全局添加eslint配置
在项目的根目录,Nx 会默认生成一个 eslint 配置文件,里面有一条规则项就表示模块边界,开发者可以在 depConstraints 配置中配置标签的依赖关系,以上面的标签为例:
jsxrules: { "@nx/enforce-module-boundaries": [ "error", { enforceBuildableLibDependency: true, allow: [ "^.*/eslint(\\.base)?\\.config\\.[cm]?[jt]s$" ], depConstraints: [ { sourceTag: "scope:module", onlyDependOnLibsWithTags: [ "scope:components", "scope:utils" ] }, { sourceTag: "scope:components", onlyDependOnLibsWithTags: [ "scope:utils" ] } ] } ] }
如果此时在 libB 中引用 libA,eslint 会出现报错提示:
小结
本篇文章介绍了 Nx 在 monorepo (多package)项目中的任务编排机制,阐述了其如何通过依赖关系图自动分析和执行任务顺序,避免开发者手动维护复杂的构建流程。同时最后也指出了在monorepo 中常见的循环依赖问题,为了解决这一问题 Nx 提供了 enforce module boundaries 方案,通过为 package 添加标签及配置 ESLint 规则,有效规范依赖关系,防止架构混乱,从而保障大型多包项目的可维护性与扩展性。