构建仓库结构

turbo 构建在 工作空间 之上,这是 JavaScript 生态系统中包管理器的一个特性,允许你在一个仓库中组织多个包。

遵循这些约定很重要,因为它可以让你:

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

在本指南中,我们将介绍如何设置多包工作空间(单仓库),为使用 turbo 打下基础。

入门

手动设置工作空间结构可能会很繁琐。如果你是单仓库新手,我们建议使用 create-turbo 开始,它可以立即创建一个有效的工作空间结构。

Terminal
npx create-turbo@latest

然后你可以查看仓库,了解本指南中描述的特征。

工作空间剖析

在 JavaScript 中,工作空间可以是单个包或多个包的集合。在这些指南中,我们将重点关注多包工作空间,通常称为"单仓库"。

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

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

最低要求

在单仓库中指定包

声明包的目录

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

./package.json
{
  "workspaces": [
    "apps/*",
    "packages/*"
  ]
}
npm 工作空间文档

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

由于 JavaScript 生态系统中包管理器的行为不明确,Turborepo 不支持像 apps/**packages/** 这样的嵌套包。使用将一个包放在 apps/a 而另一个包放在 apps/a/b 的结构会导致错误。

如果你想按目录对包进行分组,可以使用像 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 提供了三个主要好处:

  • 避免桶文件:桶文件是重新导出同一包中其他文件的文件,为整个包创建一个入口点。虽然它们看起来很方便,但它们对编译器和打包工具来说很难处理,并且很快就会导致性能问题。
  • 更强大的功能:与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 指南
  • 你应该尽可能避免跨包边界访问文件。如果你发现自己在写 ../ 来从一个包访问另一个包,你可能有机会重新思考你的方法,通过在需要的地方安装包并将其导入到你的代码中。

下一步

配置好工作空间后,你现在可以使用包管理器在包中安装依赖