跳转到内容

为什么选择 Nx?

因为没有模型的架构,最后往往只剩文件夹结构

Section titled “因为没有模型的架构,最后往往只剩文件夹结构”

“为什么要用 Nx?”

这个问题听起来很简单,得到的答案通常也很简单。

因为 Monorepo。
因为 Angular、Nest、React 和 Library 可以放在一起。
因为 Build 会更快。
因为 App 和 Lib 更容易管理。
因为现在的现代 Tooling 都这么做。

这些都不算错。

但都太浅了。

Nx 的价值并不来自“所有东西都放在一个 Repository 里”。当一个 Workspace 需要一个明确模型时,Nx 才真正开始有价值。

项目的模型。
依赖关系的模型。
Task 的模型。
Input 和 Output 的模型。
受变化影响范围的模型。
CI 的模型。
架构规则的模型。
以及越来越重要的一类模型:给 Agent 使用,让它们不必在文件树里盲猜。

或者更短一点:

Nx 把文件夹变成一个工作模型。

Nx 把文件夹变成一个工作模型。

最常见的反应是:

“我们用 Nx,因为我们想要 Monorepo。”

这大概相当于说:

“我们做架构,因为我们需要文件夹。”

Monorepo 首先只是一个组织方式:多个东西放在同一个 Repository 中。

这本身还不是架构。

你可以在 Monorepo 里拥有非常清晰的边界。也可以在里面建出一座版本管理得极其优秀的垃圾场:sharedcommonutils 到处都是,Import 随意穿透,依赖隐式增长,CI 每次修改都把整个机房重新点亮一遍。

Repository 不是最有意思的部分。

真正有意思的问题是:

你的 Workspace 知道自己内部发生了什么吗?

它知道哪个 App 依赖哪个 Library 吗?
它知道哪些测试与当前修改相关吗?
它知道一次修改会影响哪个 Deployment 吗?
它知道哪条 Boundary 被违反了吗?
它知道哪些 Task 依赖哪些 Input 吗?
它知道会产出哪些 Artifact 吗?

如果答案是否定的,你也许拥有一个 Monorepo。

但还没有一个架构模型。

Nx 并不只是:

  • 一个 Monorepo Tool;
  • 一个 Build Wrapper;
  • 一个“文件更多的 Angular CLI”;
  • 一组 Generator;
  • 一个把所有东西扔进同一个 Repo 的理由;
  • 一个神奇的 CI 加速器;
  • 一个架构决策的替代品。

Nx 更像是一个让 Workspace 显式化的工具。

它可以建模 Project、Dependency、Target、Task、Input、Output、Generator、Rule、Cache 和修改影响范围。

Nx 不只是描述文件。Nx 描述工作、依赖和变化的后果。

听起来有点干。

实际上并不是。

因为架构正是在这里开始变得可操作。

不是 Wiki 里的图。
不是 PowerPoint 里的方框。
不是“原则上应该这样”。

而是一个可以执行的模型。

Nx 最重要的部分不是 Monorepo。

而是 Graph。

更准确地说,是 Project Graph、Task Graph、Target、Input、Output、Tag 和 Rule 组合起来形成的模型。

Nx 可以建模这些问题:

  • 什么是一个 Project?
  • 它依赖什么?
  • 它能执行哪些 Task?
  • 哪些 Input 会影响这些 Task?
  • 会产生哪些 Output?
  • 哪些 Project 会被一次修改影响?
  • Project 之间有哪些规则?
  • 哪些 Tag 描述 Role、Layer 或 Domain?
  • 哪些 Target 应该标准化?
  • CI 和 Deployment 会产出哪些 Artifact?

这才是真正的元模型。

不是因为“元模型”这个词显得学术。

而是因为它不再把 Workspace 看成文件集合,而是看成工作单元和依赖关系组成的系统。

此时,一个 Project 不只是 appslibs 下面的一个目录。

它是工作模型里的一个节点。

它有任务。
有依赖。
有边界。
有允许和禁止的关系。
它可能因为修改而受到影响。
它可以被 Build、Test、Lint、Publish 或 Deploy。

这在架构上很有价值,因为结构不只是被文档化。

它变成了可执行规则。

Nx 不只是描述文件,而是描述工作、依赖和变化后果。

架构图有一个问题。

它通常描述的是系统原本“想成为”的样子。

代码描述的则是它后来真正变成的样子。

Nx Graph 很不客气地接近第二种真相。

Wiki 里的架构图告诉你系统原本打算怎样组织。Nx Graph 告诉你 Workspace 实际上如何耦合。

这可能很好看。

也可能很疼。

一个很小的 UI Library 突然依赖了半个 Feature 区域。
一个 Domain Lib 偷偷 Import 了 Infrastructure。
每个 App 都依赖同一个 shared 大箱子。
一个号称“完全隔离”的 Feature,在 Graph 上看起来却像煮熟的意大利面。

这时候 Nx 不是问题。

Nx 只是把镜子举了起来。

Graph 不太会照顾情绪。

也正因为这样,它很有价值。

Wiki 展示架构原本想怎么做,Graph 展示代码实际如何耦合。

当你希望架构不仅可以讨论,而且可以被检查时,Nx 才真正强大。

例如:

  • 一个 Domain 可以 Import 哪些其他 Domain?
  • UI Library 可以直接访问 Data-Access Library 吗?
  • Feature 可以直接使用 Infrastructure Code 吗?
  • App 可以深入 Import 其他 Library 的内部路径吗?
  • 是否存在清晰 Public API?
  • 纵向 Slice 真的按纵向切分了吗?
  • 技术 Utility 真的只是 Utility,还是已经隐藏了 Domain Logic?
  • 一次修改会牵动哪些 Test、Build 或 Deployment?

从这里开始,Nx 就不只是一个 Build Tool。

当然,它可以 Build、Test、Lint 和 Cache。

但更有意思的是:

Nx 可以帮助你把架构规则变成机器可以读取和检查的规则。

不完美。

也不完整。

更不替代思考。

但它可以成为一个非常有效的反馈系统。

Nx 可以组织大型 Workspace。

不是因为它会神奇地产生“正确的文件夹”,而是因为它能够把 Project 作为有意义的单元显式展示出来。

Nx 可以把依赖关系变得可见。

听起来很普通,直到你第一次发现自己所谓“结构清晰”的 Workspace,其实是靠三个 shared Library 和五个直接 Import 勉强缝在一起。

Nx 可以提供 Project Graph 和 Task Graph。

Project Graph 告诉你谁依赖谁。Task Graph 告诉你哪些工作必须按什么顺序执行。

这是两件不同的事情。

一个 Library 在业务上可能看起来很普通,但对 CI 来说可能非常昂贵。一个 Task 看起来独立,却可能触发大量后续 Task。Nx 能把这些关系变得更具体。

Nx 可以计算 Affected Project。

这对 CI 很重要。

但它也是架构反馈。

后面还会再谈。

Nx 可以只对受影响区域运行 Build、Test 和 Lint。

在大型 Workspace 里,这不是奢侈品。它迟早会成为“CI 仍然是开发流程的一部分”和“Pull Request 在 CI 里慢慢变老”之间的分界线。

Nx 可以把昂贵检查限制在真正受到影响的范围。

不是每个 Commit 都需要 Build 全部。
不是一个 Style 修改就应该触碰每个 Deployment。
一个隔离 Library 的变化也不应该让整个 Workspace 陷入恐慌。

Nx 可以一致地生成代码。

Generator 不只是便利工具。好的 Generator 会传递架构决策。坏的 Generator 只是更快地复制坏边界。

Nx 可以通过 Tag 和 Boundary Rule 让架构规则变得可检查。

当 Tag 不只是装饰性标签,而真正表达含义时尤其有价值:

  • Domain;
  • Layer;
  • Type;
  • Runtime;
  • Ownership;
  • Deployment Context。

Nx 可以帮助组织 Migration。

对于长生命周期 Workspace,这非常重要。架构不只是初始结构是否漂亮,而是多年以后还能不能演进。

Nx 可以标准化重复 Target。

如果每个 Project 都用不同名字表示 buildtestlintdocker:buildservee2edeploy,那已经没有 Tooling Model 了。

只有民间传说。

Nx 可以把 Fullstack Workspace 连接进一个工作模型。

Frontend、API、Service、Domain Lib、UI Lib、E2E Test、Docker Target 和 Deployment Step 可以一起被显式描述。

这对于 SCS、微前端或纵向切分的 Fullstack Workspace 尤其有价值。

Host、Remote、API、Service、Domain Lib、UI Lib、Test 和 Deployment 不再只是随机目录里的孤立 Artifact。

它们是一个已建模工作空间中的组成部分。

此外,Nx 还可以为 Agent 和其他 Tool 提供机器可读的上下文。

这一点会比很多人现在愿意承认的更重要。

Nx 是一个放大器,而不是架构工作的替代品。

现在说不太舒服的部分。

Nx 不能替你发明业务 Boundary。

如果没人知道 Domain 到哪里结束,Nx 也不会凭空变出答案。

Nx 不能修复糟糕架构。

它可以让问题可见,让规则可检查,让影响范围更清楚。但它不会替你决定某段代码应该是 App、Feature Lib、Domain Lib、Data-Access Lib 还是 Utility。

Nx 不能制造 Ownership。

如果没有人真正负责一个 Library,它不会因为在 Graph 上显示得很漂亮就自动变好。

Nx 不能替你理清团队结构。

如果五个团队同时修改相同 Library、扭曲相同 API、添加相同 Workaround,那么 Workspace 不是根因。

它只是案发现场。

Nx 不能替代缺失的测试。

Affected Test 只有在真正存在有价值测试时才有意义。

一个很快结束、但什么都没验证的测试,不是质量优势。

只是很快。

Nx 也不能把糟糕的 Generator 变成好架构。

Generator 如果产生坏边界,只会把坏边界规模化。

至少很一致。

但仍然是一致地错。

Nx 也无法替代纪律。

你可以定义 Boundary Rule,然后到处绕开。
可以维护 Tag,也可以让它慢慢腐烂。
可以认真使用 Public API,也可以到处 Import 深层内部路径。

Nx 更救不了一个叫 shared 的垃圾场。

这个工作只能自己做。

Nx 不是架构工作的替代品。它是让架构工作更可见、更可检查的工具。

说得刻薄一点:

Nx 不会解决坏架构。

它只是让坏架构更快暴露。

很多团队一开始把 affected 看成 CI 性能功能。

很合理。

Workspace 一大,Build 会变慢。Test 会变贵。Lint 需要时间。Docker Build 开始堆积。E2E Test 形成瓶颈。Deployment 变得谨慎,因为没人再确定一次修改到底影响什么。

这时,nx affected 听起来像救命工具。

只 Build 受影响 Project。
只运行受影响 Test。
只执行相关 Lint。
只 Build 相关 Docker Image。
只触发真正需要的 Deployment。

确实很强。

但它不只是性能优化。

Affected 是架构反馈。

Nx 可以计算哪些 Project 会受到一次修改的影响,然后有针对性地执行:

  • Build;
  • Test;
  • Lint;
  • E2E Test;
  • Docker Build;
  • Deployment;
  • Format Check;
  • Quality Gate;
  • Release Step。

这改变了 CI 的问题。

很多团队发现 CI 太慢之后,第一反应是加更多技术:

更多 Runner。
更多并行。
更多 Cache。
更多 Dashboard。
更多 YAML 杂技。
更多“反正晚上跑”。

Nx 把问题反过来问:

不是:

“我们能把什么都跑一遍吗?”

而是:

“这次修改真正影响了什么?”

这是完全不同的思维方式。

不是因为能够运行,就把所有东西都运行一遍。

不要因为能运行全部,就每次都运行全部。

Affected 只有在 Graph 描述真实世界时才有价值

Section titled “Affected 只有在 Graph 描述真实世界时才有价值”

Affected Strategy 很强。

但前提是 Graph 真的知道边界在哪里。

这正是关键。

如果所有东西都与所有东西相连,最后所有东西当然都会 Affected。

这不是 Nx 的问题。

这是架构在 Graph 里清了清嗓子。

一次很小的修改突然影响半个 Workspace,并不是 Nx 发来的坏消息。

它只是如实告诉你:这里存在耦合。

你当然可以继续想办法把 CI 做快。

更多 Cache。
更多并行。
更多排除规则。
更多特殊处理。

都可以。

但某个时候,你优化的已经不是 CI。

你只是在麻醉架构反馈。

结构清晰的 Workspace 会呈现完全不同的效果:

隔离的 Domain Lib 修改,只影响少数 Consumer。
UI Component 的修改会影响相关 App,但不会随机影响后端 Service。
一个 SCS 的修改会影响它的 Remote、API、Service、Test 和 Deployment,但不会自动影响整个 Workspace。
一个核心基础 Library 的变化可以影响很多地方——但所有人都应该明确知道它是核心基础设施。

所以 Affected 不只是一个 Trick。

它还是测谎仪。

如果 Graph 总是告诉你“全部受影响”,也许并不是 Graph 太悲观。

而是架构真的耦合得这么诚实。

如果所有东西都彼此连接,最终所有东西都会 Affected。

CI 经常被当作技术上的后续工作。

几个 Job。
几个 Stage。
一些 npm Script。
某处 Docker。
某处 Test。
某处 Deployment。
慢了就再加一台 Runner。

一段时间内,这当然能工作。

但在大型 Workspace 中,CI 自己会成为架构模型的 Consumer。

CI 必须知道:

  • 有哪些 Project?
  • 哪些 Target 与当前修改有关?
  • 哪些 Project 可以 Deployment?
  • 哪些 Test 属于哪类变化?
  • 哪些 Docker Image 必须 Build?
  • 哪些 Service 需要 Deploy?
  • 会产生哪些 Artifact?
  • 哪些 Stack 属于同一个上下文?
  • 哪类修改只是局部变化,哪类会影响系统?

如果 CI 不能从 Workspace Model 得到这些信息,就会出现各种旁路知识。

YAML 知道 Nx 不知道的事。
Shell Script 知道 Graph 不知道的事。
Deployment Matrix 知道任何架构规则里都没有记录的事。
最后没人知道,到底哪一份知识才是真的。

Nx 可以帮助改善这一点,因为 Target、Affected Analysis 和 Project Metadata 能够形成共同语言。

但前提是 CI 真的使用 Graph。

如果 CI 只是把 Nx 当作一个更复杂的 npm-script Starter,价值就很有限。

Log 里虽然写着 nx

但模型并没有真正参与工作。

Customizing 很诱人。

而且有时非常有价值。

自定义 Generator 可以帮助创建一致的 SCS 结构。标准化 Target 可以确保每个可部署 Project 都按相同方式 Build。Tag 可以表达 Domain、Layer、Type 或 Runtime Context。Deployment Convention 也可以进入 Tooling,而不是躺在十篇 Confluence 页面里慢慢发霉。

这是好的 Customizing。

因为它在表达架构规则。

危险发生在 Customizing 开始替代或隐藏 Tooling Convention 的时候。

每个 Project 都拥有自己的 Target 名称。
Generator 开始制造各种 Sonderfall。
Shell Script 绕过 Graph。
CI 只把 Nx 当成薄 Wrapper。
Migration Path 越来越困难。
新开发者无法理解系统。
Agent 就更难理解。
知识不断搬进自定义 Script。
Graph 还显示结构,却越来越不能解释真实行为。

最后,你其实是在自建 Build System。

只是 Logo 还是 Nx。

Customizing 在表达架构规则时很有价值;当它开始替代 Tooling Convention 时,就变得危险。

越是把 Nx 改造成另一个工具,共同模型的价值就越低。

而这个共同模型,才是 Nx 真正有意思的原因。

不是因为它能把任意 Shell Script 换个名字执行。

过度改造 Nx,最后得到的是自己的 Build System,只是仍然挂着 Nx 的 Logo。

Anti-Pattern:sharedcommonutils 和其他停车场

Section titled “Anti-Pattern:shared、common、utils 和其他停车场”

每个大型 Workspace 迟早都会出现一些“暂时不知道放哪”的地方。

通常叫:

  • shared
  • common
  • utils
  • core
  • base
  • misc
  • helpers

当然,不是所有 Utility Library 都有问题。

但很多这种目录根本不是架构概念。

它们只是那些没人愿意做决定的东西的停车场。

shared 往往不是架构概念,而是没人愿意做归属决策时留下的停车场。

问题不只是名字。

问题是它没有明确含义。

当所有东西都是 Shared,就没有任何东西真正受到保护。

Feature 随意 Import Helper。
App 深入访问 Library 内部路径。
Domain Code 进入技术 Utility。
UI Logic 渗入 Data-Access。
Test 依赖随机实现细节。
最后没人知道谁真正拥有这些东西。

典型 Nx 陷阱包括:

  • 所有东西都进 shared
  • 所有东西都进 common
  • 所有东西都进 utils
  • 技术 Library 没有清晰业务切分;
  • Feature Lib 什么都 Import;
  • App 深入 Import 其他 Lib 的内部路径;
  • 没有清晰 Public API;
  • Tag 存在但没人维护;
  • Boundary Rule 定义了却不断被绕过;
  • 有大量没有业务理由的微型 Library;
  • Library 太少,所有内容又重新集中;
  • Generator 生成代码,却没有生成架构;
  • CI 调用 Nx,却不用 Affected Logic;
  • 团队期待 Affected 帮忙,但所有依赖本来就无处不在;
  • 每个团队建立自己的 Convention;
  • Nx 被当成架构决策的替代品;
  • 直到 CI 变慢才有人第一次打开 Graph。

最麻烦的地方在于:

Nx 会把这些问题展示出来。

但它不会自动阻止它们。

Workspace 仍然需要真正的决策。

有哪些业务区域?
哪些技术 Layer 有意义?
哪些 Library 可以稳定共享?
哪些东西应该明确禁止共享?
允许哪些 Public API?
禁止哪些 Import?
哪些 Generator 真正在传递架构?
哪些 Target 必须标准化?

没有这些决策,Nx 就不会成为架构工具。

它只是在更高效地管理更大的混乱。

当 Workspace 需要的不只是文件夹和 npm Script 时,Nx 开始变得有价值。

例如:

  • 多个 App;
  • 多个 Library;
  • 多个 Team;
  • Frontend + Backend + API + Service 在一个 Workspace;
  • 微前端;
  • Module Federation;
  • 自包含系统(SCS);
  • 纵向 Slice;
  • 重复出现的架构规则;
  • 高 CI 成本;
  • 大量 Pull Request;
  • 需要 Affected Build 和 Affected Test;
  • 标准化生成;
  • 清晰 Boundary;
  • 长期产品开发;
  • 需要明确上下文的 Agentic Workflow;
  • 修改必须能够精确限定影响范围的 Workspace。

特别是在 SCS、微前端或 Fullstack Workspace 里,Nx 可以非常强大。

一个业务区域不只是一个 Backend API。

它可能包括:

  • Remote;
  • API;
  • Service;
  • Domain Lib;
  • UI Lib;
  • Data-Access;
  • Test;
  • E2E;
  • Docker Target;
  • Deployment Step。

这些都可以在 Nx 中成为相互关联、但有明确边界的工作空间。

不是因为一切都应该随意混在一起。

而是因为它们之间的关系是显式的。

这是非常重要的区别。

一个好的 Nx Workspace 不会说:

“所有东西都可以认识所有东西。”

它会说:

“我们知道谁可以认识谁。”

Nx 并不是永远正确的答案。

有时简单结构就够了。

这完全没有问题。

Nx 对这些项目来说可能就是 Overkill:

  • 一个很小的单 App;
  • 一个 Team;
  • 几乎没有 Shared Code;
  • CI 很简单;
  • Prototype;
  • 一次性项目;
  • 很简单的 Repository;
  • Angular CLI、Vite 或 npm Script 已经完全够用;
  • 团队根本不准备维护一个模型。

最后一点很重要。

如果没有人认真维护 Graph,Nx 就只是更复杂地运行 npm Script。

团队付出了复杂度成本,却没有得到它真正的收益。

Nx 需要维护。

不必过度。

不必宗教化。

也不需要 Tool Fetish。

但 Tag、Boundary、Target、Generator 和 Project Structure 必须被认真对待。

否则 Graph 最后只会变成架构会议里偶尔打开的一张彩色图片,然后大家继续直接 Import 一切。

不是每个项目都需要 Nx,但大型 Workspace 需要一个模型。

接下来这部分,在未来几年会明显变得更重要。

对人来说,Nx Graph 是一张架构图。

对 Agent 来说,它是导航系统。

Coding Agent 需要上下文。

而不只是文件。

它们必须知道:

  • 有哪些 Project?
  • 谁依赖谁?
  • 哪些 Test 与当前工作相关?
  • 有哪些 Command?
  • 哪些 Boundary 不能违反?
  • 哪个 Library 可以 Import 到哪里?
  • 哪个 API Service 属于哪个 Frontend?
  • 一次修改会影响 DB、Backend、API、UI 和哪些 Test?
  • 修改后必须运行哪些 Target?
  • 哪些 Project 根本不允许被当前任务修改?
  • Build、Test 和 Deployment 会产出哪些 Artifact?

人类可以花很多时间慢慢理解这些信息。

读目录。
搜索 Import。
问同事。
翻 CI。
找老 Ticket。
解释命名。
最后逐渐形成对系统的直觉。

Agent 不会自动拥有这种直觉。

它需要模型。

如果它只看到文件树,它就在猜。

而会猜的 Agent,往往能够极其高效地制造混乱。

Nx 可以在这里提供帮助,因为它让 Workspace 变得机器可读。

Agent 不必只靠猜:

“相关测试到底在哪里?”

它可以通过 Target 和 Affected Analysis 更接近答案。

它也不必只靠猜:

“哪些 Project 属于同一个变化范围?”

Graph 可以提供起点。

它也不必随意 Import:

“应该没问题吧。”

Boundary Rule 可以给它明确边界。

但这里仍然有同样的前提:

只有 Workspace 本身维护得好,Nx 才有帮助。

如果系统里全是 Custom Script、Sonderfall、随意 Import 和腐烂的 Tag,那么 Agent 也只是更快理解混乱。

Agent 不只放大生产力。

也会放大无序。

代码越多由 Agent 修改,机器可读的架构模型就越重要。

不是因为 Agent 可以替代架构。

而是因为如果没有模型,它们会在没有架构地图的情况下工作。

这大概和给一个实习生 Root 权限,再配上极强自信一样令人安心。

对人来说 Graph 是架构图,对 Agent 来说它是导航系统。

所以真正更好的问题并不是:

“我们应该用 Nx 吗?”

而是:

“我们的 Workspace 需要一个可执行模型吗?”

如果答案是否定的,Nx 很可能没有必要。

如果答案是肯定的,Nx 就开始变得很有意思。

但这时不要只停在“Monorepo”。

这时讨论的是架构。

有意识地切分 Project。
定义清晰 Public API。
不要把 Tag 当装饰。
认真使用 Boundary Rule。
标准化 Target。
不要只把 Affected 当成加速手段,也把它当成架构反馈。
Generator 要小、稳定、真正有意义。
不要把 shared 变成垃圾场。
把 CI 当成架构模型的使用者。
也把 Agent 当成未来的架构模型使用者。
只在 Customizing 真正表达规则时定制,不要为了定制而摧毁 Convention。

Nx 不是魔法棒。

它是放大器。

它会放大好的结构。

也会让坏结构变得更响亮。

Nx 最重要的部分不是 Monorepo。

而是 Graph。

Nx 的价值不在于所有东西都放在同一个 Repo。它的价值在于 Workspace 需要一个模型:给人、给 CI,也给 Agent。

再短一点:

当 Graph 能描述架构时,Nx 才真正强大。如果所有东西都与所有东西相连,那 Graph 只是诚实地描述了问题。

Nx 的价值不在于所有东西都放进一个 Repo,而在于 Workspace 拥有一个可执行模型。