组织代码库

turbo 构建在 Workspaces 之上,这是 JavaScript 生态系统中包管理器的一项功能,允许您在一个存储库中 agrupaciones 多个包。

遵循这些约定很重要,因为它们允许您

  • 将这些约定应用于您存储库的所有工具
  • 快速、增量地将 Turborepo 引入现有存储库

在本指南中,我们将介绍如何设置一个多包工作区(monorepo),以便为 turbo 打下基础。

入门

手动设置工作区结构可能会很繁琐。如果您是 monorepo 的新手,我们建议 使用 create-turbo 快速入门,它能立即提供有效的工​​作区结构。

终端
pnpm dlx create-turbo@latest

然后,您可以查看存储库以了解本指南中所述的特征。

工作区结构

在 JavaScript 中,工作区可以是 单个包 或多个包的集合。在本指南中,我们将重点介绍 多包工作区,通常称为“monorepo”。

下面,突出显示了 create-turbo 中使其成为有效工作区的结构元素。

package.json
pnpm-lock.yaml
pnpm-workspace.yaml
turbo.json
package.json

最低要求

在 monorepo 中指定包

声明包的目录

首先,您的包管理器需要描述包的位置。我们建议从将包拆分为 apps/(用于应用程序和服务)和 packages/(用于其他所有内容,例如库和工具)开始。

pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
pnpm 工作区文档

使用此配置,appspackages 目录中**带有 package.json** 的每个目录都将被视为一个包。

Turborepo 不支持嵌套包,例如 apps/**packages/**,因为这在 JavaScript 生态系统的包管理器之间存在歧义。使用会将一个包放在 apps/a,另一个放在 apps/a/b 的结构将导致错误。

如果您想按目录对包进行分组,可以使用类似 packages/*packages/group/* 的 glob 模式,**并且不要**创建 packages/group/package.json 文件。

每个包中的 package.json

在包的目录中,必须有一个 package.json 文件才能使包被您的包管理器和 turbo 发现。包的 package.json 要求 如下。

package.json

package.json 是您工作区的基石。下面是一个常见的示例,展示了您会在根 package.json 中找到的内容:

./package.json
{
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint"
  },
  "devDependencies": {
    "turbo": "latest"
  },
  "packageManager": "pnpm@9.0.0"
}

turbo.json

turbo.json 用于配置 turbo 的行为。要了解有关如何配置任务的更多信息,请访问 配置任务 页面。

包管理器锁文件

锁文件对于包管理器和 turbo 的可重现行为至关重要。此外,Turborepo 使用锁文件来理解您工作区内 内部包 之间的依赖关系。

如果您在运行 turbo 时没有锁文件,可能会遇到不可预测的行为。

包结构

通常最好将设计包视为工作区内的独立单元。从高层来看,每个包几乎就像自己的小型“项目”,拥有自己的 package.json、工具配置和源代码。这种想法有一些限制 — 但这是一个很好的起点。

此外,包具有特定的入口点,您工作区中的其他包可以使用这些入口点来访问该包,这些入口点由 exports 指定。

包的 package.json

name

name 字段 用于标识包。它在您的工作区中应该是唯一的。

最佳实践是为您的 内部包 使用命名空间前缀,以避免与 npm 注册表上的其他包发生冲突。例如,如果您的组织名为 acme,您可以将您的包命名为 @acme/package-name

我们在文档和示例中使用 @repo,因为它是在 npm 注册表中未使用且无法声明的命名空间。您可以选择保留它或使用自己的前缀。

scripts

scripts 字段用于定义可以在包的上下文中运行的脚本。Turborepo 将使用这些脚本的名称来识别在包中要运行(如果有)的脚本。我们在 运行任务 页面上详细介绍了这些脚本。

exports

exports 字段 用于指定其他包想要使用该包的入口点。当您想在另一个包中使用一个包中的代码时,您将从该入口点导入。

例如,如果您有一个 @repo/math 包,您可能有如下的 exports 字段:

./packages/math/package.json
{
  "exports": {
    ".": "./src/constants.ts",
    "./add": "./src/add.ts",
    "./subtract": "./src/subtract.ts"
  }
}

请注意,此示例为了简单起见使用了 Just-in-Time Package 模式。它直接导出 TypeScript,但您可以选择使用 Compiled Package 模式。

此示例中的 exports 字段需要现代版本的 Node.js 和 TypeScript。

这将允许您像这样从 @repo/math 包导入 addsubtract 函数:

./apps/my-app/src/index.ts
import { GRAVITATIONAL_CONSTANT, SPEED_OF_LIGHT } from '@repo/math';
import { add } from '@repo/math/add';
import { subtract } from '@repo/math/subtract';

这样使用 exports 会带来三个主要好处:

  • 避免 barrel 文件:Barrel 文件是重新导出同一包中其他文件的文件,为整个包创建一个入口点。虽然它们可能看起来很方便,但它们对编译器和打包器来说很难处理,并且很快就会导致性能问题。
  • 更强大的功能:与 main 字段 相比,exports 还具有其他强大的功能,例如 Conditional Exports。总的来说,我们建议尽可能使用 exports 而不是 main,因为它是一种更现代的选择。
  • IDE 自动完成:通过使用 exports 指定包的入口点,您可以确保您的代码编辑器可以为包的导出提供自动完成功能。

imports(可选)

imports 字段 提供了一种创建指向包内其他模块的子路径的方法。您可以将这些视为“快捷方式”,以编写更简单的导入路径,这些路径更能抵抗移动文件的重构。要了解如何操作,请访问 TypeScript 页面

您可能更熟悉 TypeScript 的 compilerOptions#paths 选项,该选项实现了类似的目标。从 TypeScript 5.4 开始,TypeScript 可以从 imports 推断子路径,这使其成为更好的选择,因为您将使用 Node.js 约定。有关更多信息,请访问 我们的 TypeScript 指南

源代码

当然,您会在包中包含一些源代码。包通常使用 src 目录来存储其源代码,并编译到 dist 目录(该目录也应位于包内),但这并非强制要求。

常见陷阱

  • 如果您使用 TypeScript,您很可能不需要在工作区根目录中设置 tsconfig.json。包应独立指定其自己的配置,通常基于工作区中另一个包提供的共享 tsconfig.json。有关更多信息,请访问 TypeScript 指南
  • 您应该尽可能避免访问跨包边界的文件。如果您发现自己编写了 ../ 来从一个包进入另一个包,那么您很可能可以通过安装所需包并将其导入到您的代码中来重新考虑您的方法。

后续步骤

配置好工作区后,您现在可以使用包管理器 为您的包安装依赖项