从 PNPM Workspace 到分布式 CI:用 Nx 将 Tasker 单仓构建与测试提速实战指南
【免费下载链接】nxThe Monorepo Platform that amplifies both developers and AI agents. Nx optimizes your builds, scales your CI, and fixes failed PRs automatically. Ship in half the time.项目地址: https://gitcode.com/GitHub_Trending/nx/nx
本课程基于 Nx 官方仓库中的 pnpm-nx-next 课程整理而成,以 Tasker(一个基于 Next.js、以 PNPM workspace 组织、通过 Prisma 访问本地数据库并拆分出数据访问包与 UI 组件包的任务管理应用)为实战样例,逐步讲解如何把现有 PNPM 单仓改造为高性能的 Nx 工作区:引入 Nx、配置本地缓存与任务管道、接入 Nx Cloud 远程缓存、用 Nx Agents 做分布式任务执行,最终把 Playwright e2e 测试从 20 分钟压缩到 9 分钟。读完本文,你将掌握一套可直接复制的“PNPM 单仓 + Nx + Nx Cloud + 分布式 CI”落地方案。
课程全景:六个增量改造步骤
整个课程围绕 Tasker 应用展开,采用“小步快跑、每步可验证”的增量策略,把单仓优化拆解为六个阶段:
- 添加 Nx:在不破坏现有 PNPM workspace 结构的前提下引入 Nx 作为任务编排层;
- 配置并调优本地缓存:让 Nx 正确捕获并恢复构建产物(重点是 Next.js 的
.next目录); - 定义任务管道(Task Pipeline):声明任务间的依赖关系,确保任务按正确顺序执行(如先 build 再 start);
- 用远程缓存优化 CI:通过 Nx Cloud 让 CI 与本地共享构建结果,避免重复劳动;
- 调整 CI 配置以启用任务分发:引入 Nx Agents,跨多台机器并行执行任务;
- 拆分并行化 Playwright e2e 测试:将整体 20 分钟的 e2e 压到 9 分钟。
这六步对应课程中的 00-overview 与 13-outro 之间的 12 节课,下文逐一展开。
第一步:用nx init把 Nx 引入现有 PNPM 工作区
Tasker 原本是一个标准的 PNPM workspace 单仓:根目录的pnpm-workspace.yaml声明包范围,各包通过package.json的scripts维护构建、测试等命令。引入 Nx 有两条路径:
- 最小化手工方式:只在
package.json中加入nx依赖,然后手工创建 nx.json 配置文件; - 推荐方式:直接运行初始化命令:
nx initnx init会分析仓库现状,向你提出若干问题(比如选择要启用的插件、确认哪些脚本要纳入任务编排),然后自动生成nx.json与必要的项目配置,同时完整保留现有 PNPM workspace 结构——这是本课程反复强调的前提:Nx 不是要替代包管理器,而是叠加在 PNPM 之上的任务编排与缓存层。
作为佐证,Nx 官方仓库自身的 nx.json 就同时声明了nxCloudId、nxCloudUrl、namedInputs、targetDefaults与plugins等完整配置,说明这正是生产级单仓的标准形态。关于迁移细节可参考课程给出的扩展阅读:Adopting Nx 与 Import an Existing Project into an Nx Workspace。
第二步:用 Nx 统一运行任务,替代裸pnpm --filter
PNPM 单仓里常见的运行方式是--filter加包名:
pnpm --filter @tasker/web build引入 Nx 后,同样的任务可以这样写:
pnpm nx build @tasker/web两种写法的差异在于:pnpm --filter只会机械地执行目标包的脚本;而pnx nx build会先构建项目图、识别依赖、判断缓存,再做最少必要计算(仅重新执行受影响的、缓存未命中的任务)。对于单个任务与多个任务,Nx 的语法是一致的:
# 运行单个任务 pnpm nx build @tasker/web # 同时运行多个任务(可混合包名与任务名) pnpm nx run-many -t build testNx 本身是一个任务运行器(task runner):它不重新发明构建逻辑,而是接管“何时运行、按什么顺序运行、是否可复用上次结果”这类编排职责。这正是后续缓存与分布式执行的基础,相关能力可参见课程引用的 Run Tasks with Nx。
第三步:配置本地缓存,正确处理.next目录
Nx 默认会自动捕获常见的产物目录(如dist、build),并在任务重跑时从本地缓存直接恢复。但.next目录不在默认列表内——而它是 Next.js 构建的核心产物,没有它next start根本无法启动。
因此本课的核心是在targetDefaults中为 build 目标显式声明缓存输出。nx.json中与之对应的关键结构是targetDefaults与namedInputs的配合,例如 Nx 官方仓库在 nx.json 中对build目标所做的声明:
{ "targetDefaults": { "build": { "dependsOn": ["^build"], "inputs": ["production", "^production"], "cache": true } } }cache: true开启该目标的缓存能力;inputs声明缓存键的输入范围(哪些文件变化会导致缓存失效);outputs声明产物位置,Next.js 项目通常需要显式加上"{projectRoot}/.next"(以及"!{projectRoot}/.next/cache"之类的排除项),确保next build的产物能被正确捕获与恢复。
调优缓存的核心心智模型是“输入哈希决定命中与否,输出目录决定恢复什么”。只有.next被正确声明为输出,开发者本地和 CI 上才能享受“改一行代码 → 只重跑这一个任务 → 其余全部秒级恢复”的体验。详见 Cache Task Results。
第四步:用 Task Pipeline 保证任务执行顺序
所有 Next.js 项目几乎都有这样一对脚本:
{ "scripts": { "build": "next build", "start": "next start" } }next start只有在项目根目录存在.next目录时才能工作,而这个目录由next build生成。也就是说,start对build存在隐式依赖。这是任务管道(Task Pipeline)最典型的应用场景。
本课的目标是:每次运行start时,Nx 自动先执行build(或从缓存中恢复其结果)。在nx.json的targetDefaults中声明依赖即可:
{ "targetDefaults": { "start": { "dependsOn": ["build"] } } }dependsOn支持两种前缀语法:
build:依赖同一项目的build任务;^build:依赖所有依赖项目的build任务(先构建上游库)。
当 Nx 执行start时,它会自动扩展出完整的任务图:先跑build,成功(或缓存命中)后再跑start。这种“由管道声明驱动、按图执行”的机制,避免了在脚本里手工拼&&的脆弱写法,也让缓存得以作用于图中的每一个节点。官方仓库 nx.json 中大量使用"dependsOn": ["^build", ...]正是这一能力的生产实践。概念与实操可参考 Defining a Task Pipeline。
第五步:用 implicitDependencies 补齐项目图
Nx 的核心能力之一,是在后台构建项目图(project graph),并据此决定任务如何编排、哪些任务受某个改动影响。可视化项目图的方式:
pnpm nx graph提示:也可以安装Nx Console(VSCode / IntelliJ 扩展),它能在编辑器内直接可视化项目图、运行任务、查看缓存状态,极大提升单仓开发体验,详见 editor setup。
项目图中绝大多数关系能被 Nx自动发现:要么来自package.json的依赖声明,要么来自 JS/TS 的 import 语句。但有一类依赖无法被静态分析发现,比如 Tasker 中的 Playwright e2e 项目:它在代码层面不 import 任何 Next.js 应用的模块,却在运行时强依赖——Playwright 必须先把 Next 应用 serve 起来才能跑测试。
这种“运行时依赖”需要手工声明,方式是给 e2e 项目的project.json添加implicitDependencies属性:
{ "implicitDependencies": ["@tasker/web"] }声明之后,项目图才会把 e2e 与 web 应用连起来,从而获得两个关键收益:
nx affected能正确识别“web 应用变了 → e2e 也要重跑”;- 任务分发时,e2e 任务会被正确地排在应用启动之后。
implicitDependencies的完整字段说明见 project configuration。
第六步:接入 Nx Cloud——本地与 CI 的桥梁
“Smart Monorepos”由 Nx 驱动,而“Fast Builds”则由 Nx Cloud 带来。Nx Cloud 是 Nx 的云配套服务,把本地缓存的能力延伸进 CI 流水线,让大型单仓在 CI 上也能保持高效。本课的操作链路是:
- 把 Tasker 单仓推送到 GitHub;
- 在 Nx Cloud 上创建一个 workspace;
- 将其与 GitHub 仓库关联(链接后 Nx Cloud 能感知 PR、提交等事件)。
完成后,本地与 CI 就都能访问 Nx Cloud 提供的远程缓存与分布式任务执行能力。Nx 官方仓库在 nx.json 中同样声明了nxCloudId与nxCloudUrl,即该配置的真实形态。接入步骤可参考 Connect to Nx Cloud。
用 Nx 命令重构 GitHub Actions
Tasker 原本的 CI 脚本基于pnpm --filter,本课将它替换为 Nx 命令以提升效率。用生成器直接脚手架出一份新的 CI 配置:
pnpm nx g ci-workflow该生成器会按最佳实践生成 GitHub Actions workflow,其中包含nx affected(只跑 PR 受影响的任务)等关键环节,详见 Run Only Tasks Affected by a PR。
同时课程特别提醒一个缓存细节:CI 配置本身也应该参与缓存键计算。如果改了.github/workflows/ci.yml但缓存不失效,就会得到“配置已变、结果未重算”的陈旧输出。这正是nx.json中namedInputs的用武之地。看 Nx 官方仓库 nx.json 的实际做法:
{ "namedInputs": { "sharedGlobals": [ "{workspaceRoot}/babel.config.json", "{workspaceRoot}/.nx/workflows/agents.yaml", "{workspaceRoot}/.github/workflows/ci.yml" ], "default": ["{projectRoot}/**/*", "sharedGlobals"] } }namedInputs允许把一组文件 glob 或环境变量命名成一个可复用的“输入组”。这里把sharedGlobals(含 CI workflow 文件)并入default与production,意味着任何 CI 配置变更都会自动使相关任务的缓存失效,从根本上避免脏缓存问题。
为远程缓存配置访问令牌
Nx Cloud 内置强大的远程缓存能力,因此访问控制至关重要。本课在 Nx Cloud 的 workspace 配置中创建一个访问令牌(access token),把它以 Secret 形式注入 GitHub Actions,从而赋予 CI 对远程缓存的读写权限。令牌的粒度管理详见 Nx CLI and CI Access Tokens。
用nx login让开发者机器也接入远程缓存
远程缓存不仅服务 CI,也可以服务开发者的本地机器。Nx Cloud 使用Personal Access Token(PAT)提供细粒度控制,按需二选一:
- 只读访问:开发者只能拉取远程缓存结果,为团队节省重复构建时间;
- 读写访问:开发者既能拉取,也能贡献(写入)缓存,让本地构建成果反哺整个团队。
开发者在本机执行如下命令完成认证:
pnpm nx login认证后本地任务会自动读写 Nx Cloud 远程缓存。PAT 的权限配置方法见 Nx Cloud and Personal Access Tokens。
调试缓存未命中
缓存命中率直接决定收益大小,所以弄清“为什么 miss”与“为什么 hit”同样重要。Nx Cloud 提供了对比界面,可以把两次 run 并排比较,精确定位是哪一份输入变化导致了缓存未命中。
常见的未命中原因包括:无关文件被纳入输入范围、namedInputs配置过宽、环境变量或全局配置参与哈希等。排查方法论见 Troubleshoot cache misses。
第七步:用 Nx Agents 把任务分发到多台机器
远程缓存很强,但存在一个边界场景:当核心包频繁变动时,缓存会大面积失效,此时即使有远程缓存,任务仍然要真实执行。单台机器顺序跑仍然很慢。
Nx Cloud 内置的Nx Agents特性正是为此设计:它自动把任务图中的任务分发到多台机器上并行执行,且无需手工拆分任务。本课启用 Nx Agents 只需在 CI 配置中增加一行:
nx start-ci-run --distribute-on="5 linux-medium-js"这条命令的含义是:启动一次 CI run,并通过--distribute-on指定分发规模——5表示 5 个 agent,linux-medium-js是预置的 agent 规格(Linux 中等配置、面向 JS/TS 工作负载)。Nx 会动态地把任务图调度到这 5 个 agent 上,跑完后再由主 job 汇总产物。
从源码结构看,该命令对应 Nx 仓库中的 start-ci-run 处理器,其实现会先检查工作区是否已连接 Nx Cloud(isNxCloudUsed),未连接则给出提示并直接返回;已连接则将命令转发给 Nx Cloud 执行——这印证了start-ci-run与 Nx Cloud 的强绑定关系。完整文档见 Distribute Task Execution。
第八步:并行化 Playwright e2e,20 分钟 → 9 分钟
e2e 测试在 CI 上往往是最痛苦的环节:每个 PR 都想跑,但又不想等 30 分钟。Tasker 的 Playwright 测试在 CI 上耗时约 20 分钟,本课的目标是把它显著压下来。
解法是利用 Nx Playwright 插件自动拆分 e2e 任务:把整体 e2e 运行拆成“每个测试文件(或每条测试)一个独立任务”,再让这些原子任务经由 Nx Agents 分散到多台机器上并行执行。
这一行为在 Nx Playwright 插件的源码中有直接体现:在 插件入口 中,插件会为每个 spec 文件生成形如e2e-ci--<相对路径>的原子化 CI 目标(ciTargetName默认e2e-ci),并将它们聚合进e2e-ci目标组、统一关闭并行度交由 Nx 调度;同时还会生成--merge-reports合并目标,把分散机器上的测试报告合并回来。Nx 官方仓库的 nx.json 中也有对应的 Playwright 插件配置(targetName与ciTargetName选项),以及e2e-ci--**/**这类原子目标模式,可作为真实生产的参照。
组合效果非常直观:任务级并行 + 机器级分发 + 报告合并,让 20 分钟的整体 e2e 缩短到约 9 分钟,且每个 PR 依然能拿到完整、可读的测试报告。原理细节见 Automatically Split E2E Tasks。
总结:一套可复制的单仓提速路径
回顾整条改造链路,本质是给单仓叠加三层能力:
- 任务编排层(Nx):
nx init无侵入接入 → 统一pnpm nx <target> <project>语法 →nx graph可视化项目图 →implicitDependencies补齐静态分析盲区; - 缓存层(本地 + 远程):
targetDefaults声明cache、inputs、outputs以正确捕获.next等产物 →namedInputs把 CI 配置纳入缓存键 → Nx Cloud 远程缓存用 access token / PAT 控制读写 → 用对比界面调试缓存未命中; - 分布式执行层(Nx Agents):一行
nx start-ci-run --distribute-on开启跨机分发 → Playwright 插件自动原子化拆分 e2e → 20 分钟压到 9 分钟。
每一步都是增量、可回滚、可验证的。对任何已经跑在 PNPM workspace 上的 Next.js 单仓而言,这套“从 PNPM Workspaces 到分布式 CI”的方案都可以直接照搬落地。
【免费下载链接】nxThe Monorepo Platform that amplifies both developers and AI agents. Nx optimizes your builds, scales your CI, and fixes failed PRs automatically. Ship in half the time.项目地址: https://gitcode.com/GitHub_Trending/nx/nx
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考