构建仓库结构
turbo 建立在 Workspaces 之上,这是 JavaScript 生态系统中包管理器的一个功能,允许您在一个仓库中组织多个包。
遵循这些约定很重要,因为它允许您:
- 为您仓库的所有工具依赖这些约定
- 快速、增量地将 Turborepo 采用到现有仓库中
在本指南中,我们将逐步设置一个多包工作区(monorepo),以便为 turbo 奠定基础。
开始使用
手动设置工作区结构可能很繁琐。如果您是 monorepo 的新手,我们建议使用 create-turbo 开始,立即获得有效的工作区结构。
pnpm dlx create-turbo@latest
yarn dlx create-turbo@latest
npx create-turbo@latest
bunx create-turbo@latest
然后您可以查看仓库中本指南描述的特征。
工作区解剖
在 JavaScript 中,工作区可以是单个包或包的集合。在这些指南中,我们将专注于多包工作区,通常称为"monorepo"。
下面突出显示了 create-turbo 使其成为有效工作区的结构元素。
package.json ✓
pnpm-lock.yaml ✓
pnpm-workspace.yaml ✓
turbo.json
apps/
docs/
package.json ✓
web/
package.json ✓
packages/
ui/
package.json ✓
package.json ✓
yarn.lock ✓
turbo.json
apps/
docs/
package.json ✓
web/
package.json ✓
packages/
ui/
package.json ✓
package.json ✓
package-lock.json ✓
turbo.json
apps/
docs/
package.json ✓
web/
package.json ✓
packages/
ui/
package.json ✓
package.json ✓
bun.lock ✓
turbo.json
apps/
docs/
package.json ✓
web/
package.json ✓
packages/
ui/
package.json ✓
最低要求
在 monorepo 中指定包
1. 声明包的目录
首先,您的包管理器需要描述包的位置。我们建议从将包分为 apps/(用于应用程序和服务)和 packages/(用于其他所有内容,如库和工具)开始。
packages:
- "apps/*"
- "packages/*"
{
"workspaces": [
"apps/*",
"packages/*"
]
}
{
"workspaces": [
"apps/*",
"packages/*"
]
}
{
"workspaces": [
"apps/*",
"packages/*"
]
}
参考文档:
使用此配置,apps 或 packages 目录中带有 package.json 的每个目录都将被视为一个包。
Turborepo 不支持嵌套包,如 apps/** 或 packages/**,因为 JavaScript 生态系统中包管理器之间存在模糊行为。使用将包放在 apps/a 和另一个包放在 apps/a/b 的结构将导致错误。
如果您想按目录对包进行分组,可以使用 packages/* 和 packages/group/* 等 glob 模式,并且不要创建 packages/group/package.json 文件。
2. 每个包中的 package.json
在包的目录中,必须有一个 package.json 才能让您的包管理器和 turbo 发现该包。包的 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"
}
{
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "latest"
},
"packageManager": "yarn@1.22.19",
"workspaces": ["apps/*", "packages/*"]
}
{
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "latest"
},
"packageManager": "npm@10.0.0",
"workspaces": ["apps/*", "packages/*"]
}
{
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "latest"
},
"packageManager": "bun@1.2.0",
"workspaces": ["apps/*", "packages/*"]
}
根目录 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 字段:
{
"exports": {
".": "./src/constants.ts",
"./add": "./src/add.ts",
"./subtract": "./src/subtract.ts"
}
}
请注意,此示例为了简单起见使用了即时包模式。它直接导出 TypeScript,但您可能选择使用编译包模式。
此示例中的 exports 字段需要现代版本的 Node.js 和 TypeScript。
这将允许您从 @repo/math 包导入 add 和 subtract 函数,如下所示:
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 的 compilerOptions#paths 选项,它实现了类似的目标。从 TypeScript 5.4 开始,TypeScript 可以从 imports 推断子路径,使其成为更好的选择,因为您将使用 Node.js 约定。
源代码
当然,您会希望在包中有一些源代码。包通常使用 src 目录来存储其源代码并编译到 dist 目录(也应该位于包内),尽管这不是必需的。
常见陷阱
- 如果您使用 TypeScript,您可能不需要在工作区根目录中有
tsconfig.json。包应该独立指定自己的配置,通常基于工作区中单独包的共享tsconfig.json构建。 - 您希望尽可能避免跨包边界访问文件。如果您为自己编写
../从一个包到另一个包,您可能有机会通过在需要的地方安装包并将其导入到代码中来重新思考您的方法。
下一步
配置好工作区后,您现在可以使用包管理器将依赖项安装到包中。