Turborepo logo

仓库结构

turbo 构建于 Workspaces 之上,它是 JavaScript 生态系统中包管理器的一项功能,允许你将多个包分组到一个仓库中。

遵循这些约定非常重要,因为它允许你

  • 为你的仓库的所有工具依赖这些约定
  • 快速、渐进地将 Turborepo 引入现有仓库

在本指南中,我们将引导你设置多包工作区 (monorepo),以便为 turbo 奠定基础。

开始使用

手动设置工作区的结构可能很繁琐。如果你是 monorepo 新手,我们建议使用 create-turbo 立即开始使用有效的工作区结构。

终端
npx create-turbo@latest

然后你可以查看仓库是否具有本指南中描述的特征。

工作区剖析

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

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

package.json
package-lock.json
turbo.json
package.json

最低要求

在 monorepo 中指定包

声明包的目录

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

./package.json
{
  "workspaces": [
    "apps/*",
    "packages/*"
  ]
}
npm workspace 文档

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

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

如果你想按目录对包进行分组,你可以使用 glob 模式,例如 packages/*packages/group/*,并且不要创建 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": "npm@10.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"
  }
}

请注意,此示例为了简单起见,使用了即时包模式。它直接导出 TypeScript,但你也可以选择使用编译包模式。

此示例中的 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 还具有其他强大的功能,例如 条件导出。总的来说,我们建议尽可能使用 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 指南
  • 你要尽可能避免跨包边界访问文件。如果你发现自己编写 ../ 以从一个包转到另一个包,你可能有一个机会重新思考你的方法,即在需要的地方安装该包并将其导入到你的代码中。

下一步

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