TypeScript和Turborepo构建Monorepo实战

更新日期: 2026-03-13 阅读: 22 标签: TypeScript

一个仓库。多个应用。零混乱。这就是高级工程师真正使用的架构。

你有一个 Next.js 应用、一个通用组件库、一个 REST api,以及一个 design token 包——全部分别位于不同的仓库中。因此,每次对共享按钮组件的修改都意味着 4 个 PR、3 条崩溃的 CI 流水线,以及一个非常愤怒的团队。其实有更好的方式。

Monorepo 并不新鲜。十多年来,Google 和 Meta 都在单一仓库中运行其整个产品代码库。对我们其他人来说,这似乎是新的——其中大多数技术已经存在一段时间,但工具链终于跟上了。结合 Turborepo 中的 TypeScript project references 和 pnpm workspaces,你可以获得真正可用于生产级别的 monorepo:快速构建、共享包、增量缓存,以及不会让你想去种草的 DX。

这是我第一次搭建时希望拥有的一份简短指南。让我们从零开始构建。

指标效果
CI 时间下降85%(使用 Turborepo 远程缓存后)
依赖管理单一版本,多应用共享
包管理pnpm 原生 monorepo 支持——workspace 协议

为什么选择 Turborepo 而不是 Nx 或 Lerna?

答案实际上取决于团队规模。

Nx 非常强大,但带有大量主观约束——它是一个完整的框架,而不仅仅是构建编排工具。Lerna 主要是一个包发布工具,是仓库中各个包之间的协调者。

Turborepo 处于中间位置:一个专注于任务运行和缓存的构建系统,同时在其他方面不过多干涉。

  • 本地 + 远程缓存与增量构建。Turborepo 通过对所有输入(源文件、环境变量、依赖等)进行哈希计算来判断是否需要重建,只有当某个包使用的输入发生修改时才会重新构建。在已预热的 CI 缓存中,这非常高效。

  • 基于依赖的任务编排。你声明哪个任务依赖哪个。turbo build 会在构建你的 Next.js 应用之前先构建 @repo/ui 包——在可能的情况下并行执行,自动完成,顺序正确。

  • 零锁定。Turborepo 只是构建在包管理器 workspace 之上的一层薄封装。移除它,一切仍然可以正常运行——你的 package.json scripts 仍会执行,只是没有缓存。这是一个良好的约束。


起作用的文件夹结构

在编写任何配置之前,先确定结构。社区形成的一种约定——也是 Turborepo 官方 starter 所采用的方式——是将 apps(可部署的内容)与 packages(内部库)分离:

my-monorepo/
├── apps/
│   ├── web/          ← Next.js 15 前端
│   ├── docs/         ← Next.js 文档站点
│   └── api/          ← Node/Express 或 tRPC API
├── packages/
│   ├── ui/           ← 共享 react 组件
│   ├── tsconfig/     ← 基础 TypeScript 配置
│   ├── eslint-config/← 共享 ESLint 规则
│   └── utils/        ← 共享工具函数、校验器
├── turbo.json
├── pnpm-workspace.yaml
└── package.json

重要规则:apps/ 中的内容不得被另一个 app 引用。如果两个应用都需要某段代码,那么它应该放在 packages/ 中。


连接配置:turbo.json + TypeScript References

下面是一个真实的 turbo.json,展示了具有正确依赖顺序的标准流水线——build、lint、test 和 type-check:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    },
    "typecheck": {
      "dependsOn": ["^build"]
    },
    "lint": {},
    "dev": {
      "cache": false,
      "persistent": true
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    }
  }
}

需要注意的关键是 ^build 语法:插入符号表示“首先在该任务所依赖的所有包中运行 build”。因此,当你在 Next.js 中运行 turbo build 时,由于我们使用的是 monorepo,当命令在 js 应用的根目录执行时,它会自动先构建 @repo/ui 和 @repo/utils。无需手动排序,也不会陷入循环依赖地狱。

现在,将其与可复用包中的 TypeScript project references 结合:

{
  "extends": "@repo/tsconfig/react-library.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src",
    "composite": true,
    "declaration": true,
    "declarationMap": true
  },
  "include": ["./src"],
  "exclude": ["node_modules", "dist"]
}
💡 Pro Tip
为每个公共包启用 declaration 和 declarationMap。这可以为消费应用提供准确的类型信息,并在无需运行完整构建的情况下,实现 VS Code 跨包跳转到定义。


正确共享组件库的方式

在 monorepo 中常见的错误是在第一天就过度设计共享 UI 包。先发布一个非常简单的起点,然后逐步演进。下面是一个带有实际 TypeScript 导出的共享组件:

import type { ButtonhtmlAttributes, ReactNode } from 'react';

type Variant = 'primary' | 'ghost' | 'destructive';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: Variant;
  isLoading?: boolean;
  children: ReactNode;
}

export function Button({
  variant = 'primary',
  isLoading = false,
  children,
  className,
  ...rest
}: ButtonProps) {
  return (
    <button
      >={variant}
      disabled={isLoading || rest.disabled}
      className={['btn', className].filter(Boolean).join(' ')}
      {...rest}
    >
      {isLoading ? 'Loading…' : children}
    </button>
  );
}

在你的 Next.js 应用中,它像任何 npm 包一样被消费——从 TypeScript 的角度来看,它就是一个包:

import { Button } from '@repo/ui';

export default function HomePage() {
  return (
    <main>
      <Button variant="primary" onClick={() => console.log('clicked')}>
        Get Started
      </Button>
    </main>
  );
}

Turborepo 支持渐进式采用。这意味着你不必重构整个仓库才能获得缓存收益——只需在一个流水线任务上启用,然后逐步扩展。


远程缓存:改变游戏规则(CI 流水线亦然)

最后一个组成部分——也是让团队真正关心的部分——是 CI 上的远程缓存。没有它,每次流水线运行都会从零开始重新编译所有内容。Turbo 的远程缓存(Vercel 或自托管)允许 CI 跳过自上次执行以来输入未发生变化的任务。

- name: Install dependencies
  run: pnpm install --frozen-lockfile

- name: Build, Lint, Test (with remote cache)
  run: pnpm turbo build lint test typecheck
  env:
    TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
    TURBO_TEAM:  ${{ secrets.TURBO_TEAM }}

两个环境变量。这就是远程缓存设置的全部内容。第一次运行会完整构建。之后,如果某个包未发生变化,则为缓存命中——不到一秒。


停止交付复杂性,开始交付功能

Monorepo 中最难的部分并不是工具本身——而是让团队信任它。当开发者第一次运行 pnpm turbo build,看到它因为没有变化而跳过 11 个包,看到整个过程在 4 秒而不是 4 分钟内完成——那一刻,他们就不会再问“我们为什么要这样做”。

从小开始。共享一个 tsconfig 包,一个 UI 组件包,一个 Turborepo 流水线。根据需要添加 packages。架构会随着你演进,而你的 git log 终于会成为一个由真正协作构建产品的人所书写的连贯故事。

原文地址:https://medium.com/@mernstackdevbykevin/monorepos-with-typescript-turborepo-setup-best-practices-aa94296a8dc3

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/13411

相关推荐

Nerv_一款类 React 前端框架,基于虚拟 DOM 技术的 JavaScript(TypeScript) 库

Nerv_是一款由京东凹凸实验室打造的类 React 前端框架,基于虚拟 DOM 技术的 JavaScript(TypeScript) 库。它基于React标准,提供了与 React 16 一致的使用方式与 API。

使用TypeScript两年后-值得吗?

差不多两年前,我在一个创业团队中开始了一个全新的项目。用到的全都是类似Microservices,docker,react,redux这些时髦的东西。我在前端技术方面积累了一些类似的经验

TypeScript最佳实践:是否使用noImplicitAny

我应该使用noImplicitAny TypeScript编译器标志吗?noImplicitAny编译器选项所做的,基本上是将TypeScript从可选类型语言转换为强制类型检验语言。这使得TypeScript离JavaScript的超集稍微远了一些,因为简单的:

为什么要学习Typescript 语言呢?Typescript 开发环境安装

TypeScript是一种由微软开发的自由和开源的编程语言。它是JavaScript的一个超集,TypeScript是JavaScript类型的超集,它可以编译成纯JavaScript。TypeScript可以在任何浏览器、任何计算机和任何操作系统上运行,并且是开源的。

5分钟了解TypeScript

有两种方式安装TypeScript,如何创建第一个TypeScript文件,在TypeScript中,可以使用interface来描述一个对象有firstName和lastName两个属性,TypeScript支持JavaScript的新功能,其中很重要的一个功能就是基于类的面向对象编程

Typescript中以变量方式传递类

最近尝试用TypeScript写一个工具库,需要实现这样一个场景:声明一个抽象类Parent,声明一组子类ChildA、ChildB继承这个Parent,实现它的抽象方法

TypeScript_TS系列之高级类型

交叉类型:将多个类型合并为一个类型、联合类型:表示取值可以为多种类型中的一种、混合类型:一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性、类型断言:可以用来手动指定一个值的类型

用TypeScript弥补Elm和JavaScript之间的差距

近些日子,我使用了新语言编程,从JavaScript,切确地说是Elm,转成TypeScript。在本文中,我将继续深挖一些我非常喜欢的TypeScript特性。

TypeScript_命名空间(namespace)

什么时候要用命名空间?如果你发现自己写的功能(函数/类/接口等...)越来越多, 你想对他们进行分组管理就可以用命名空间, 下面先用类,举例:发现namespace下还有export, export在这里用来表示哪些功能是可以外部访问的:

TypeScript功能:const断言

我发现官方的 TypeScript 文档非常有用,但是总觉得有点过于学术化并且枯燥无味。每当我发现一个新功能时,我想要知道这个功能究竟能够解决什么问题而不是长篇大论

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!