Turbo repo

新建项目

全局安装

bash
1pnpm install turbo --global

创建 turbo 项目

bash
1pnpm dlx create-turbo@latest

这时候会提示输入项目名以及选择哪个包管理器。

image-20231214104352266

当创建会有如下的文件目录:

image-20231214104804543

理解 packages/ui

首先打开/packages/ui/package.json,我们能够注意到它的 name 属性:

json
1"name": "@repo/ui"

然后打开/apps/web/package.json中,它的 name 属性是这样的:

json
1"name": "web"

然后查看它的依赖项:

json
1  "dependencies": {
2    "@repo/ui": "workspace:*",
3  }

这就意味着我们的 web app 依赖了本地的ui包。

如果我们查看/apps/docs/package.json,会发现它的依赖项也是如此。

这说明 web 和 docs 都依赖了同一个包:来自本地的 @repo/ui包。

这种跨应用程序的共享代码模式在 monorepo 中非常常见,这意味着多个应用程序可以共享一个设计系统。

理解 imports 和 exports

查看/apps/docs/app/page.tsx文件。

typescript
1import { Button } from '@repo/ui/button'

docs 和 web 都使用了 Next.js 构建。他们都直接从@repo/ui中引入了 Button 组件。那么这个 Button 是从哪里来的呢?

打开packages/ui/package.json查看一下:

typescript
1  "exports": {
2    "./button": "./src/button.tsx",
3    "./card": "./src/card.tsx",
4    "./code": "./src/code.tsx"
5  },

你会发现都是从 packages/ui/src 中导出的。

尝试一下在/packages/ui/src/button.tsx继续导出一个ButtonProps类型

typescript
1export interface ButtonProps {
2  children: ReactNode
3  className?: string
4  appName: string
5}

会发现也能够被page导入进来:

typescript
1import { Button, ButtonProps } from '@repo/ui/button'

理解 typescript-config

现在我们还有两个工作区目录需要查看:typescript-configeslint-config

其中 typescript-config 的 name 属性是这样的:

json
1"name": "@repo/typescript-config"

它有四个文件:

bash
1packages/typescript-config/
2├── base.json
3├── nextjs.json
4├── package.json
5└── react-library.json

packages/ui依赖了它:

json
1{
2  "devDependencies": {
3    "@repo/typescript-config": "workspace:*"
4  }
5}

packages/ui/tsconfig.json里,把 typescript-configreact-library.json文件的配置extends过来了。

json
1extends": "@repo/typescript-config/react-library.json"

这种模式允许 monoreo 在其所有工作区中共享一个 tsconfig.json,从而减少代码重复。

理解 eslint-config

它的 package.json是这样的:

json
1  "name": "@repo/eslint-config",
2  "files": [
3    "library.js",
4    "next.js",
5    "react-internal.js"
6  ]

这是针对不同的 package,使用不同的 eslint-config 的意思。

我们到apps/web/package.json能够看到引入了它:

"devDependencies": {
	"@repo/eslint-config": "workspace:*"
}

再到apps/web/.eslintrc.js查看一下:

javascript
1module.exports = {
2  extends: ['@repo/eslint-config/next.js'],
3}

这里引入了它的 next.js 的配置。

说明在 apps/web 这个项目中,eslintrc.js采用的是eslint-config/next.js里的配置。

小结

在工作区之间理解互相的依赖是非常重要的。现在的依赖关系是这样的:

  • web - 依赖了 ui、typescript-config 和 eslint-config
  • docs - 依赖了 ui、typescript-config 和 eslint-config
  • ui - 依赖了 typescript-config 和 eslint-config
  • typescript-config - 没有依赖,只有拆分成了不同用途的 config
  • eslint-config - 没有依赖,只是拆分成了不同用途的 config

理解 turbo.json

现在我们已经理解了仓库的依赖关系了,那 turbo 还提供了什么功能呢?

Turborepo 会使运行任务更简单、更高效。

turbo.json 是这样的:

json
1{
2  "$schema": "https://turbo.build/schema.json",
3  "globalDependencies": ["**/.env.*local"],
4  "pipeline": {
5    "build": {
6      "dependsOn": ["^build"],
7      "outputs": [".next/**", "!.next/cache/**"]
8    },
9    "lint": {
10      "dependsOn": ["^lint"]
11    },
12    "dev": {
13      "cache": false,
14      "persistent": true
15    }
16  }
17}

我们在这里看到的是,我们已经向 turbo 注册了三个任务:lint、dev 和 build。在 turbo.json 中注册的每个任务都可以使用turbo-run<task>(简称turbo<task>)运行。

尝试执行lint程序:

bash
1turbo run lint

image-20231214130126416

在终端我们可以看到很多信息:

  • 很多 lint 都被执行了:@repo/ui:lintweb:lintdocs/lint等等。
  • 有一个 lint 失败了。
  • total 任务 3 个,其中 2 个成功了,一个失败了。
  • 三个任务中有两个被缓存了。
  • 总共耗时 3.813 秒

失败的任务是因为我们之前引入了一个 ButtonProps 但是没有使用:

image-20231214130306464

删掉它之后重新跑一下:

image-20231214130410240

全部成功且缓存!

每次运行的脚本都来自每个工作区的 package.json。每个工作区都可以选择指定自己的 lint 脚本:

apps/web/package.json

json
1{
2  "scripts": {
3    "lint": "next lint"
4  }
5}

apps/docs/package.json

json
1{
2  "scripts": {
3    "lint": "next lint"
4  }
5}

packages/ui/package.json

json
1{
2  "scripts": {
3    "lint": "eslint \"**/*.ts*\""
4  }
5}

当我们运行 turbo lint 时,Turborepo 会查看每个工作区中的每个 lint 脚本并运行它。

使用缓存

如果我们重复执行 lint脚本,会发现除了第一次慢一点,后面的速度都很快。

比如第二次执行 lint 时耗时 329ms

这是因为 Turbo-repo 意识到我们的代码并没有发生改变。

它保存了上次运行的日志,因此每次只是重新将日志展示出来。

随便修改一个文件的内容:

apps/web/app/page.tsx

diff
1 -     <Button appName="web" className={styles.button}>
2 -       Click me!
3 -     </Button>
4
5 +     <Button appName="web" className={styles.button}>
6 +       Hello Turbo repo
7 +     </Button>

再执行一下:

image-20231214131427411

@repo/ui:lintdocs/lint都有标识:cache hit

web:lint标识为:cache miss,executing

这说明没有修改过的代码会命中缓存,修改过代码的地方则会重新执行脚本

使用 Turborepo 构建

现在尝试build脚本:

bash
1turbo run build

您将看到与我们运行 lint 脚本时类似的输出。

并且在 docs 和 web 下都出现了.next 文件夹。

image-20231214135745566

出现.next文件夹表示 turbo 已经完成了 task 并且它会将这些输出都保存在缓存里。

pipeline 上我们声明了 outputs意味着当 turbo 完成任务时,它会将您指定的输出保存在其缓存中。

Turborepo 缓存了我们以前构建的结果。当我们再次运行 build 命令时,它从缓存中恢复了整个.next/**文件夹。

使用 Turborepo 开发

现在尝试这个命令:

bash
1turbo run dev

image-20231214140223739

  • turbo 已经帮助我们执行了 docs 和 web 的开发环境了。

  • 两个开发脚本同时运行,在端口 3000 和 3001 上启动 Next.js 应用程序。

  • docs:devweb:dev :cache bypass,force executing 表示绕开了缓存,强制执行了脚本。

dev 命令的配置是这样的:

json
1{
2  "pipeline": {
3    "dev": {
4      "cache": false,
5      "persistent": true
6    }
7  }
8}

在 dev 中,我们指定了"cache": false。这意味着我们告诉 Turborepo 不要缓存 dev 脚本的结果。dev 运行一个持久的开发服务器并且不产生任何输出,所以没有什么可以缓存的。

另外,我们设定了"persistent": true,以让Turbo知道这是一个长期运行的开发服务器,这样 Turbo 可以确保没有其他任务依赖它。

仅运行一个 dev

默认情况下,turbo dev将同时在所有工作区上运行 dev。但有时,我们可能只想选择一个workspace

为了处理这个问题,我们可以在命令中添加一个--filter 标志。

bash
1turbo run dev --filter=web

现在只有 web workspace 的 dev 脚本会被运行了。