JS `Nx` / `Turborepo`:Monorepo 工作流与构建缓存优化

各位朋友,大家好!我是你们今天的 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 的工作原理

    1. 依赖图分析: Nx 会分析你的项目结构,构建一个依赖图,了解哪些项目依赖于哪些项目。
    2. 任务定义: 你需要在 nx.json 文件中定义任务,例如 buildtestlint 等。
    3. 缓存命中: 当你执行一个任务时,Nx 会检查该任务的输入(例如代码、配置文件、依赖项)是否发生了变化。 如果没有变化,Nx 会直接从缓存中恢复输出。
    4. 分布式缓存: 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,包含两个项目:app1lib1lib1 是一个 React 组件库,app1 使用了 lib1 中的组件。

    1. 创建 Monorepo:

      npx create-nx-workspace my-monorepo --preset=react --style=styled-components
      cd my-monorepo
    2. 创建 lib1:

      nx generate @nx/react:library lib1
    3. 创建 app1:

      nx generate @nx/react:application app1
    4. 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;
    5. 运行 app1:

      nx serve app1
    6. 修改 lib1:

      修改 lib1/src/lib/lib1.tsx 中的组件,然后再次运行 nx serve app1。 你会发现,只有 lib1app1 会被重新构建,其他项目会被跳过。

    7. 构建受影响的项目:

      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 的工作原理

    1. 任务定义: 你需要在 turbo.json 文件中定义任务,例如 buildtestlint 等。
    2. 依赖分析: Turborepo 会分析你的项目结构,了解哪些项目依赖于哪些项目。
    3. 任务调度: Turborepo 会根据依赖关系和任务定义,调度任务的执行顺序。
    4. 缓存命中: 当你执行一个任务时,Turborepo 会检查该任务的输入是否发生了变化。 如果没有变化,Turborepo 会直接从缓存中恢复输出。
    5. 远程缓存: 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,包含两个项目:webuiui 是一个 React 组件库,web 使用了 ui 中的组件。

    1. 创建 Monorepo:

      pnpm create turbo my-turbo-repo
      cd my-turbo-repo
    2. 创建 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"
        }
      }
    3. 创建 web 应用:

      apps 目录下创建一个 web 目录,并使用 Next.js 初始化一个项目。

      cd apps
      npx create-next-app web
      cd ..
    4. 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;
    5. 配置 turbo.json:

      {
        "$schema": "https://turbo.build/schema.json",
        "pipeline": {
          "build": {
            "dependsOn": ["^build"],
            "outputs": [".next/**", "dist/**", "lib/**"]
          },
          "lint": {},
          "dev": {
            "cache": false,
            "persistent": ["./.next"]
          }
        }
      }
    6. 运行 dev:

      turbo run dev
    7. 修改 ui:

      修改 packages/ui/src/index.tsx 中的组件,然后刷新浏览器。 你会发现,只有 uiweb 会被重新构建,其他项目会被跳过。

  • 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 的世界里玩得开心! 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注