各位朋友,大家好!我是你们今天的 Monorepo 专家(暂时)。今天咱们来聊聊 JS 世界里的两个当红炸子鸡:Nx 和 Turborepo。它们都是 Monorepo 工作流的利器,尤其在构建缓存优化方面,简直能让你的 CI/CD 速度起飞。咱们今天就来扒一扒它们的皮,看看它们到底是如何做到让开发效率蹭蹭往上涨的。
Monorepo:为啥大家都爱它?
先说说 Monorepo。顾名思义,就是一个代码仓库里放着多个项目。 传统的多仓库(Polyrepo)模式,每个项目一个仓库,看似清晰,但项目多了,管理起来就麻烦了:
特性 | Monorepo | Polyrepo |
---|---|---|
代码共享 | 方便,可以直接 import 其他项目的代码 | 困难,需要发布 npm 包或者使用 git submodule 等方式 |
依赖管理 | 统一管理,避免版本冲突 | 复杂,容易出现版本冲突 |
代码复用 | 容易,可以直接复制粘贴(虽然不推荐,但确实方便) | 困难,需要抽取公共组件,发布 npm 包 |
重构 | 方便,可以一次性修改多个项目 | 困难,需要修改多个仓库 |
构建/部署 | 可以一次性构建/部署多个项目,或者只构建/部署受影响的项目 | 需要分别构建/部署每个项目 |
可见性 | 所有代码都在一个地方,方便代码审查 | 代码分散在多个地方,不方便代码审查 |
总而言之,Monorepo 能带来更好的代码复用性、依赖管理和重构效率。当然,Monorepo 也不是完美的,它也会带来一些挑战,比如仓库体积变大、构建时间变长等。 这时候,就需要 Nx 和 Turborepo 这样的工具来帮忙了。
Nx:全能选手,不止于构建缓存
Nx 是一个强大的、可扩展的构建系统,它不仅仅是一个构建缓存工具,更是一个提供了项目结构、代码生成、测试、部署等一系列功能的完整解决方案。
-
核心概念:计算缓存
Nx 的核心在于“计算缓存”。它会记住每个任务(例如构建、测试、lint)的输入和输出,如果输入没有变化,就会直接从缓存中恢复输出,而不需要重新执行任务。 想象一下,你修改了一个不影响某个项目的代码,那么 Nx 就会跳过该项目的构建过程,直接使用上次构建的缓存结果,大大节省了时间。
-
Nx 的工作原理
- 依赖图分析: Nx 会分析你的项目结构,构建一个依赖图,了解哪些项目依赖于哪些项目。
- 任务定义: 你需要在
nx.json
文件中定义任务,例如build
、test
、lint
等。 - 缓存命中: 当你执行一个任务时,Nx 会检查该任务的输入(例如代码、配置文件、依赖项)是否发生了变化。 如果没有变化,Nx 会直接从缓存中恢复输出。
- 分布式缓存: Nx 支持分布式缓存,可以将缓存存储在云存储服务(例如 AWS S3、Google Cloud Storage)中,以便在多个机器之间共享缓存。
-
Nx 的配置:
nx.json
nx.json
是 Nx 的核心配置文件,它定义了项目的结构、任务、缓存策略等。 举个例子:{ "npmScope": "my-org", "affected": { "defaultBase": "main" }, "implicitDependencies": { "package.json": { "dependencies": "*", "devDependencies": "*" }, ".eslintrc.json": "*" }, "tasksRunnerOptions": { "default": { "runner": "nx-cloud", "options": { "cacheableOperations": ["build", "test", "lint", "e2e"], "accessToken": "YOUR_NX_CLOUD_TOKEN" } } }, "targetDefaults": { "build": { "dependsOn": ["^build"], "inputs": ["production", "^production"] } }, "namedInputs": { "default": ["{projectRoot}/**/*", "sharedGlobals"], "production": [ "default", "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", "!{projectRoot}/tsconfig.spec.json", "!{projectRoot}/jest.config.[jt]s", "!{projectRoot}/.eslintrc.json" ], "sharedGlobals": [] }, "generators": { "@nx/react:application": { "style": "styled-components", "linter": "eslint" }, "@nx/react:library": { "style": "styled-components", "linter": "eslint" }, "@nx/next:application": { "style": "styled-components", "linter": "eslint" }, "@nx/web:application": { "bundler": "webpack", "linter": "eslint" }, "@nx/node:application": { "linter": "eslint" } } }
npmScope
: 你的 npm 组织名称。affected
: 配置如何确定受影响的项目。implicitDependencies
: 定义隐式依赖关系,例如package.json
的修改会影响所有项目。tasksRunnerOptions
: 配置任务运行器,可以使用 Nx Cloud 或者本地缓存。targetDefaults
: 配置任务的默认选项,例如build
任务依赖于其依赖项目的build
任务。namedInputs
: 定义命名的输入,例如production
输入排除了测试文件。generators
: 配置代码生成器的默认选项。
-
使用 Nx CLI
Nx 提供了一个强大的 CLI 工具,可以用来创建项目、运行任务、分析依赖关系等。
nx build <project>
: 构建指定的项目。nx test <project>
: 测试指定的项目。nx lint <project>
: Lint 指定的项目。nx affected:build
: 构建受影响的项目。nx graph
: 可视化项目的依赖关系图。nx migrate latest
: 更新 Nx 版本。
-
Nx 代码示例 (React 项目)
假设我们有一个 React Monorepo,包含两个项目:
app1
和lib1
。lib1
是一个 React 组件库,app1
使用了lib1
中的组件。-
创建 Monorepo:
npx create-nx-workspace my-monorepo --preset=react --style=styled-components cd my-monorepo
-
创建
lib1
:nx generate @nx/react:library lib1
-
创建
app1
:nx generate @nx/react:application app1
-
在
app1
中使用lib1
:在
app1/src/app/app.tsx
中:import React from 'react'; import { MyComponent } from '@my-org/lib1'; // 假设 npmScope 是 my-org import styled from 'styled-components'; const StyledApp = styled.div` text-align: center; `; const StyledContent = styled.div` background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; `; const StyledLink = styled.a` color: #61dafb; `; function App() { return ( <StyledApp> <StyledContent> <MyComponent /> {/* 使用 lib1 中的组件 */} <p> Edit <code>src/app/app.tsx</code> and save to reload. </p> <StyledLink href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </StyledLink> </StyledContent> </StyledApp> ); } export default App;
-
运行
app1
:nx serve app1
-
修改
lib1
:修改
lib1/src/lib/lib1.tsx
中的组件,然后再次运行nx serve app1
。 你会发现,只有lib1
和app1
会被重新构建,其他项目会被跳过。 -
构建受影响的项目:
nx affected:build --all
这个命令会构建所有受上次提交影响的项目。
-
-
Nx Cloud:分布式缓存的终极方案
Nx Cloud 是一个可选的、但强烈推荐的 Nx 扩展。 它可以提供分布式缓存、可视化构建信息、协作功能等。 使用 Nx Cloud 可以显著提高构建速度,尤其是在 CI/CD 环境中。
配置 Nx Cloud 非常简单,只需要在
nx.json
中配置tasksRunnerOptions
即可。{ "tasksRunnerOptions": { "default": { "runner": "nx-cloud", "options": { "cacheableOperations": ["build", "test", "lint", "e2e"], "accessToken": "YOUR_NX_CLOUD_TOKEN" } } } }
Turborepo:专注于速度的构建工具
Turborepo 是 Vercel 团队开发的、专注于速度的构建工具。 它的核心在于“管道化”和“缓存”。
-
核心概念:管道化和缓存
- 管道化: Turborepo 会将你的任务分解成多个步骤,并尽可能并行执行这些步骤。 类似于 CI/CD 中的 Pipeline。
- 缓存: Turborepo 会缓存每个任务的输出,并在下次运行时检查输入是否发生了变化。 如果没有变化,Turborepo 会直接从缓存中恢复输出。
-
Turborepo 的工作原理
- 任务定义: 你需要在
turbo.json
文件中定义任务,例如build
、test
、lint
等。 - 依赖分析: Turborepo 会分析你的项目结构,了解哪些项目依赖于哪些项目。
- 任务调度: Turborepo 会根据依赖关系和任务定义,调度任务的执行顺序。
- 缓存命中: 当你执行一个任务时,Turborepo 会检查该任务的输入是否发生了变化。 如果没有变化,Turborepo 会直接从缓存中恢复输出。
- 远程缓存: Turborepo 支持远程缓存,可以将缓存存储在云存储服务(例如 Vercel Remote Cache)中,以便在多个机器之间共享缓存。
- 任务定义: 你需要在
-
Turborepo 的配置:
turbo.json
turbo.json
是 Turborepo 的核心配置文件,它定义了任务、依赖关系、缓存策略等。 举个例子:{ "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { "dependsOn": ["^build"], "outputs": [".next/**", "dist/**", "lib/**"] }, "lint": {}, "dev": { "cache": false, "persistent": ["./.next"] } } }
$schema
: JSON Schema 的 URL,用于验证配置文件的格式。pipeline
: 定义任务的管道。build
: 构建任务。dependsOn
: 依赖于其他任务,^build
表示依赖于所有依赖项目的build
任务。outputs
: 构建任务的输出目录,Turborepo 会缓存这些目录。
lint
: Lint 任务。dev
: 开发任务。cache
: 是否缓存该任务,false
表示不缓存。persistent
: 持久化目录,即使任务没有缓存,这些目录也会被保留。
-
使用 Turborepo CLI
Turborepo 提供了一个简单的 CLI 工具,可以用来运行任务。
turbo run build
: 运行所有项目的build
任务。turbo run test
: 运行所有项目的test
任务。turbo run lint
: 运行所有项目的lint
任务。turbo run dev
: 运行所有项目的dev
任务。turbo prune --scope=<package_name>
: 创建一个只包含指定项目及其依赖项的子仓库。turbo link
: 将本地 Turborepo 链接到全局环境。
-
Turborepo 代码示例 (Next.js 项目)
假设我们有一个 Next.js Monorepo,包含两个项目:
web
和ui
。ui
是一个 React 组件库,web
使用了ui
中的组件。-
创建 Monorepo:
pnpm create turbo my-turbo-repo cd my-turbo-repo
-
创建
ui
库:在
packages
目录下创建一个ui
目录,并在其中创建一个react
组件。packages/ui/src/index.tsx
:import React from 'react'; export const Button = ({ children }: { children: React.ReactNode }) => { return <button>{children}</button>; };
packages/ui/package.json
:{ "name": "@my-turbo-repo/ui", "version": "0.0.0", "private": true, "main": "./src/index.tsx", "types": "./src/index.tsx", "devDependencies": { "react": "^18.0.0", "typescript": "^4.5.3" } }
-
创建
web
应用:在
apps
目录下创建一个web
目录,并使用 Next.js 初始化一个项目。cd apps npx create-next-app web cd ..
-
在
web
中使用ui
:在
web/pages/index.tsx
中:import React from 'react'; import { Button } from '@my-turbo-repo/ui'; const Home = () => { return ( <div> <h1>Welcome to Next.js!</h1> <Button>Click me</Button> </div> ); }; export default Home;
-
配置
turbo.json
:{ "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { "dependsOn": ["^build"], "outputs": [".next/**", "dist/**", "lib/**"] }, "lint": {}, "dev": { "cache": false, "persistent": ["./.next"] } } }
-
运行
dev
:turbo run dev
-
修改
ui
:修改
packages/ui/src/index.tsx
中的组件,然后刷新浏览器。 你会发现,只有ui
和web
会被重新构建,其他项目会被跳过。
-
-
Vercel Remote Cache:加速你的 CI/CD
Turborepo 提供了 Vercel Remote Cache,可以将缓存存储在 Vercel 的云存储服务中。 这可以显著提高 CI/CD 的速度,尤其是在多个机器之间共享缓存时。
配置 Vercel Remote Cache 非常简单,只需要在 Vercel 中启用 Turborepo 集成即可。
Nx vs Turborepo:选择哪个?
Nx 和 Turborepo 都是优秀的 Monorepo 构建工具,但它们的设计理念和适用场景有所不同。
特性 | Nx | Turborepo |
---|---|---|
定位 | 全能型构建系统,提供项目结构、代码生成、测试、部署等一系列功能 | 专注于速度的构建工具,提供管道化和缓存功能 |
学习曲线 | 较陡峭,需要学习 Nx 的概念和配置 | 较平缓,配置简单,易于上手 |
灵活性 | 非常灵活,可以自定义任务和插件 | 相对灵活,但不如 Nx |
生态系统 | 庞大,支持多种前端和后端框架 | 相对较小,主要支持 JavaScript 和 TypeScript 项目 |
社区支持 | 活跃,有大量的文档和示例 | 活跃,但不如 Nx |
适用场景 | 大型 Monorepo,需要复杂的功能和高度的定制化 | 中小型 Monorepo,追求速度和简单易用 |
远程缓存 | 支持 Nx Cloud,提供分布式缓存、可视化构建信息、协作功能等 | 支持 Vercel Remote Cache,提供远程缓存功能 |
总结
Nx 和 Turborepo 都是 Monorepo 工作流的强大工具,它们可以帮助你提高开发效率、缩短构建时间。 选择哪个工具取决于你的项目规模、需求和团队的偏好。 如果你需要一个全能型、高度可定制的构建系统,Nx 是一个不错的选择。 如果你追求速度和简单易用,Turborepo 可能会更适合你。
无论你选择哪个工具,都要记住构建缓存优化的核心思想: 尽可能地缓存任务的输出,并在下次运行时尽可能地重用缓存。 这才是提高构建速度的关键所在。
好了,今天的讲座就到这里。希望大家有所收获,也希望大家都能在 Monorepo 的世界里玩得开心! 谢谢大家!