6
头图

ErKeLost.png

1. 2022年了,扩展你代码库的最好方式 🚀🚀

1.1 什么是monorepo ?

Monorepo是一种项目管理方式,在Monorepo之前,代码仓库管理方式是 MultiRepo,即每个项目都对应着一个单独的代码仓库每个项目进行分散管理
这就会导致许多弊端,例如可能每个项目的基建以及工具库都是差不多的,基础代码的重复复用问题等等...

Monorepo就是把多个项目放在一个仓库里面, 关于monorepo的文章已经很多了,并且目前可以搭建Monorepo的工具也很多,例如

工具简述
Bit用于组件驱动开发的工具链
Turborepo用于 JavaScript 和 TypeScript 代码库的高性能构建系统。
Rush一个可扩展的 web 单仓库管理器。
Nx具有一流的 monorepo 支持和强大集成的下一代构建系统。
Lerna用于管理包含多个软件包的项目

我们今天为大家带来高性能的构建 Monorepo工具,Turborepo

2. TurboRepo

2.1 什么是Turborepo

TurboRepo 是构建Javascript,Typescript的monorepo高性能构建系统,Turborepo抽象出所有烦人的配置、脚本和工具,减少项目配置的复杂性,可以让我们专注于业务的开发

我们通常在构建monorepo 时需要搭配适当的工具来扩展 monorepo,Turborepo利用先进的构建技术和思想来加速开发,构建了无需配置复杂的工作,只需要我们通过脚本和工具,即可快速搭建您的Monorepo TurboRepo 支持使用Yarn Npm Pnpm,
我们本次使用Pnpm 来带大家领略 Tuborepo的魅力!

2.2 TurboRepo的优势

2.2.1 多任务并行处理

Turbo支持多个任务的并行运行,我们在对多个子包,编译打包的过程中,turbo会同时进行多个任务的处理

在传统的 monorepo 任务运行器中,就像lerna或者yarn自己的内置workspaces run命令一样,每个项目的script生命周期脚本都以拓扑方式运行(这是“依赖优先”顺序的数学术语)或单独并行运行。根据 monorepo 的依赖关系图,CPU 内核可能处于空闲状态——这样就会浪费宝贵的时间和资源。[](https://turborepo.org/docs/gl...)

什么是拓扑 ?

拓扑 Topological Order
是一种排序 拓扑排序是依赖优先的术语, 如果 A 依赖于 B,B 依赖于 C,则拓扑顺序为 C、B、A。

比如一个较大的工程往往被划分成许多子工程,我们把这些子工程称作活动(activity)。在整个工程中,有些子工程(活动)必须在其它有关子工程完成之后才能开始,也就是说,一个子工程的开始是以它的所有前序子工程的结束为先决条件的

为了可以了解turbo多么强大,下图比较了turbovslerna任务执行时间线:

image.png

Turbo它能够有效地安排任务类似于瀑布可以同时异步执行多个任务,而lerna一次只能执行一项任务 所以Turbo的 性能不言而喻。

2.2.2 更快的增量构建

如果我们的项目过大,构建多个子包会造成时间和性能的浪费,turborepo中的缓存机制 可以帮助我们记住构建内容 并且跳过已经计算过的内容,优化打包效率。

2.2.3 云缓存

Turbo通过其远程缓存功能可以帮助多人远程构建云缓存实现了更快的构建。

2.2.4 任务管道

用配置文件定义任务之间的关系,然后让Turborepo优化构建内容和时间。

2.2.5 基于约定的配置

通过约定降低复杂性,只需几行JSON 即可配置整个项目依赖,执行脚本的顺序结构。

2.2.6 浏览器中的配置文件

生成构建配置文件并将其导入Chrome或Edge以了解哪些任务花费的时间最长。

3 Turbo 核心概念

3.1 管道 (Pipeline)

Turborepo为开发人员提供了一种以常规方式显式指定任务关系的方法。

  1. 新来的开发人员可以查看Turborepopipeline并了解任务之间的关系。
  2. turbo可以使用这个显式声明来执行基于多核处理器的丰富可用性的优化和计划执行。

turbo在定义monorepo的任务依赖关系图,我们需要在根目录定义turbo.json 执行各种调度,输出,缓存依赖, 打包等功能

turbo.json位于turborepo项目根目录接下来实战的部分会带领大家从头创建一个项目

pipeline中的每一个key都指向我们在package.json中定义的script脚本执行命令,并且在pipeline中的每一个key 都是可以被turbo run所执行 执行pipeline的脚本的名称。您可以使用其下方的键以及与缓存相关的一些其他选项来指定其依赖项。

在我们执行turbo run ***命令的时候 turbo 会根据我们定义的 Pipelines
里对命令的各种配置去对我们的每个package中的package.json 中 对应的script执行脚本进行有序的执行和缓存输出的文件。

// turbo.json
{
  "$schema": "https://turborepo.org/schema.json",
  "pipeline": {
    "build": {
      // A package's `build` script depends on that package's
      // dependencies' and devDependencies'
      // `build` tasks  being completed first
      // (the `^` symbol signifies `upstream`).
      "dependsOn": ["^build"],
      // note: output globs are relative to each package's `package.json`
      // (and not the monorepo root)
      "outputs": [".next/**"]
    },
    "test": {
      // A package's `test` script depends on that package's
      // own `build` script being completed first.
      "dependsOn": ["build"],
      "outputs": [],
      // A package's `test` script should only be rerun when
      // either a `.tsx` or `.ts` file has changed in `src` or `test` folders.
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"]
    },
    "lint": {
      // A package's `lint` script has no dependencies and
      // can be run whenever. It also has no filesystem outputs.
      "outputs": []
    },
    "deploy": {
      // A package's `deploy` script depends on the `build`,
      // `test`, and `lint` scripts of the same package
      // being completed. It also has no filesystem outputs.
      "dependsOn": ["build", "test", "lint"],
      "outputs": []
    }
  }
}

<br/>接下来我们来解析每一个对象中的key到底是用来做什么的帮助我们更好的理解pipeline

3.1.1 DependsOn (依赖于)

比如当前我们有三个子包package两个工具package一个playground用于测试其余两个package

image.png

  "pipeline": {
    "build": {
      "dependsOn": ["^build"]
    }
  }

3.1.2 常规依赖

如果一个任务的执行,只依赖自己包其他的任务,那么可以把依赖的任务放在dependsOn数组里

{
    "turbo": {
        "pipeline": {
            "deploy": {
                "dependsOn": ["build", "test", "lint"]           
            } 
        }    
    }
}

3.1.3 拓扑依赖

可以通过^符号来显式声明该任务具有拓扑依赖性,需要依赖的包执行完相应的任务后才能开始执行自己的任务

{
    "turbo": {
        "pipeline": {
            "build": {
                "dependsOn": ["^build"],           
            }
        }    
    }
}

因为playground依赖于@relaxed/utils和@relaxed/hook,所以我们当前playground子包的build存在依赖关系,根据build的dependsOn配置,会先执行依赖项的build命令,也就是@relaxed/utils和@relaxed/hook的build命令,依赖项执行完后才会执行playground的build命令。
如果我们不添加"dependsOn": ["^build"]数组中的‘^’那么就代表我们当前只需要执行我们自己的build命令

dependsOn 表示当前命令所依赖的命令,^ 表示 dependencies 和 devDependencies 的所有依赖都执行完 build,才执行 build

3.1.4 空依赖

如果一个任务的dependsOn为[] 或者不声明这个属性,那么表明这个任务可以在任意时间被执行

3.1.5 Output

outputs 表示命令执行输出的文件缓存目录

默认值为["dist/**", "build/**"]

我们还可以通过传递一个空数组用来告诉turbo任务是一个副作用,这样我们不会输入任何文件

"pipeline": {
  "build": {
    // "Cache all files emitted to package's dist/** or .next
    // directories by a `build` task"
    "outputs": ["dist/**", ".next/**"],
    "dependsOn": ["^build"]
  },
 }

3.1.6 Cache

cache 表示是否缓存,通常我们执行 dev 命令的时候会结合 watch 模式,所以我们一般在项目启动模式下不需要开启 turbo 缓存机制

{
  "$schema": "https://turborepo.org/schema.json",
  "pipeline": {
    "dev": {
      "cache": false
    }
  }
}

3.1.7 Input

默认为[]。告诉turbo在确定特定任务的包是否已更改时要考虑的文件集。将其设置为文件输入地址将导致仅当与这些真正子包中需要配置输入匹配的文件发生更改时才重新运行任务。例如,如果您想跳过运行测试,除非源文件发生更改,这会很有帮助。

指定[]意味着任务在任何文件发生更改时重新运行。

{
  "$schema": "https://turborepo.org/schema.json",
  "pipeline": {
   "test": {
      // A package's `test` task should only be rerun when
      // either a `.tsx` or `.ts` file has changed.
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
    }
  }
}

3.1.8 OutputMode

outputMode代表输出的模式类型是字符串

type
full
new-only
hash-only
none

full 也是默认值代表 显示任务的整个输出

hash-only仅显示计算的任务哈希

image.png

new-only显示缓存未命中的完整输出和缓存命中的计算哈希值。意思就是返回带有hash的日志并且当如果有未命中的子包缓存或者打包错误导致缓存未命中再次打包时会输出上一次缓存未命中的的子包完整任务输出日志

none 使用“none”隐藏任务输出。意思就是不会在控制台中打印我们拓扑顺序以及打包输入的日志,但是依然会正确执行 build 命令

{
  "$schema": "https://turborepo.org/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputMode": "new-only"
    },
    "test": {
      "outputs": [],
      "dependsOn": ["build"],
    },
  }
}

3.2 过滤包 (Filtering Packages)

turbo中第二个核心概念filtering packages允许您将任务仅定位到你想要去操作的包。Turborepo支持一个类似pnpm--filter过滤命令但是他不同于传统pnpm --filter 命令 我们需要在 --filter后面添加一个等号确定需要对过滤的包进行单独的指令,允许您选择将充当“入口点”的包进入 monorepo 的包/任务图中,您可以通过包名称、包目录、包是否包含依赖项/依赖项以及 git 历史记录的更改来过滤您的项目。
语法 pnpm build --filter=@relaxed/utils

image.png
过滤器语法还有很多场景这里就不一一展示具体可见turbo 过滤包语法

3.3 缓存 (Caching)

Turborepo 检查文件内容变动时,会根据内容生成Hash来对比
turbo它可以缓存发出的文件和以前运行的命令的日志。它这样做是为了跳过已经完成的工作,从而节省大量时间。
例如我们执行三个子包的build命令我们只需要执行turbo run build

turbo在每次打包的过程中会在每个build 的子包中生成 log 文件在下一次打包的时候
缓存构建内容,并跳过已经计算过的内容,判断是否需要重新构建

在我们在输入build命令的时候首先进入到pipeline中查看是否关闭缓存,然后通过当前子包中的turbo的log文件来hash 对比当前代码是否有改变 如果没有发生改变那么就 跳过构建
image.png

image.png

强制覆盖缓存

相反,如果要强制turbo重新执行之前缓存的任务,请添加--force标志:

#在所有包和应用中运行`Build`N脚本
#忽略缓存命中。
turbo run build --force

3.4 远程缓存 (Remote Caching)

Turborepo速度的一个主要关键🔑 是它既懒惰又高效——它做的工作量尽可能少,并且它试图从不重做以前已经完成的工作。 这是官网对turbo 远程缓存的一个总结
正常情况下 我们使用turbo 的时候在构建过程中只能将我们的任务缓存在本地系统上turbo支持了一种多人开发共享缓存的模式

开发人员团队和/或持续集成 (CI) 系统使用远程缓存来共享构建输出。如果您的构建是可重现的,那么一台机器的输出可以安全地在另一台机器上重复使用,这可以显着加快构建速度。

如果要将本地 turborepo 链接到远程缓存,请首先使用 Vercel 帐户对 Turborepo CLI 进行身份验证:

npx turbo login

接下来,您可以通过运行以下命令将您的 turborepo 链接到远程缓存:

npx turbo link

启用后,对当前缓存的包或应用程序进行一些更改,并使用turbo run. 您的缓存工件现在将存储在本地您的远程缓存中。

要验证,请使用以下命令删除本地Turborepo 缓存:

rm -rf ./node_modules/.cache/turbo

然后再次运行相同的构建。如果一切正常,turbo则不应在本地执行任务,而是从远程缓存下载日志和工件并将它们重放给您。

4. 命令行的使用

4.1 选项语法

options选项语法可以通过 turbo 使用不同的方式来进行传递

  • 传递值的选项
  --<option>=<value>
  // like
  pnpm build --filter=vue-devui
  
  pnpm build --filter=@relaxed/hook

4.2 全局参数 (常用命令)

4.2.1 --continue

默认为false.该标志告诉turbo是否在出现错误(即任务的非零退出代码)时继续执行。默认情况下,指定--parallel标志将自动设置--continue为,true除非明确设置为false。当--continueis时trueturbo将以执行期间遇到的最高退出代码值退出。

turbo run build --continue

4.2.2 --parallel

默认false。脚本程序并行运行命令并忽略依赖关系图。这对于使用实时重新加载进行开发很有用。例如我们启动vite项目的时候我们就需要忽略其他可能出现的dependsOn依赖关系

  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", "build/**"],
      "outputMode": "new-only"
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      //
      "dependsOn": ["^build"]
    }
  }
}

由于我们在pipeline中设置了dependson的build命令依赖优先的问题所以我们可以指定--parallel并行执行并且阻止默认依赖 build 指令

turbo run lint --parallel --no-cache
turbo run dev --parallel --no-cache

4.2.3 --filter

指定包/应用程序、目录和 git 提交的组合作为执行的入口点。

可以组合多个过滤器来选择不同的目标集。此外,过滤器还可以排除目标。匹配任何过滤器且未明确排除的目标将在最终入口点选择中。

有关--filter标志和过滤的更多详细信息,请参阅我们文档中的专用页面

turbo run build --filter=my-pkg
turbo run test --filter=...^@scope/my-lib
turbo run build --filter=./apps/* --filter=!./apps/admin

4.2.4 --force

忽略现有的缓存工件并强制重新执行所有任务(覆盖重叠的工件)

turbo run build --force

4.2.5 --no-cache

默认false。不要缓存任务的结果。next dev这对于诸如or之类的监视命令很有用react-scripts start

turbo run build --no-cache
turbo run dev --parallel --no-cache

4.2.6 --only

默认false。将执行限制为指定package中的指定的任务。这与默认情况下如何lernapnpm运行任务的方式非常相似。如果我们指定了 在依赖前需要执行build命令 但是如果我们设置 --only 将默认排除 build命令

鉴于此管道turbo.json

{
  "$schema": "https://turborepo.org/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": [
        "^build"
      ]
    },
    "test": {
      "dependsOn": [
        "^build"
      ]
    }
  }
}
turbo run test --only

将只会执行每个包中的test任务。它不会build

本文介绍几种常用的 turbo 命令, 当然 turbo 提供了许多在命令行中使用的指令, 具体可以查看

命令行参考

5. Demo实战✨✨

快速开始

我们可以通过现有的monorepo 改造 也可以 直接创建turbo 项目,

turbo 的另一大特色就是改造您现有的monorepo也很简单, 只需要安装 turbo 依赖 根目录添加 turbo.json 一切就尽在掌握了

我们本次体验一下 使用 turbo 命令行创建项目, demo仅展示turbo和monorepo的结合使用方式,具体构建方式,以及完整项目 可以查看github模板

我们在命令行输入

npx create-turbo@latest

创建我们的工程名

>>> TURBOREPO

>>> Welcome to Turborepo! Let's get you set up with a new codebase.

? Where would you like to create your turborepo? (./my-turborepo)

我们推荐使用Pnpm 构建

? Which package manager do you want to use?
  npm
> pnpm
  yarn

turbo 会自动根据我们选择的包管理器为我们创建相对应的项目
然后我们进入项目

image.png

为了更好的体验我们使用vite更直观的展示

我们在根目录命令行输入npm init vue@3创建一个名为playground的vue项目

image.png

然后在pnpm-workspace.yaml中添加一个playground字段代表把playground包添加到pnpm monorepo的管理

packages:
  - 'packages/*'
  - 'playground'

紧接着在 packages 里面新建 @relaxed/hook, @relaxed/utils, @relaxed/tsconfig 文件夹前两个作为测试工具库 剩下作为我们 tsconfig 共享配置库

  • @relaxed/utils

根目录新建 index.ts

export function Accumulation(...value: any[]) {
  return value.reduce((t, v) => t + v, 0)
}

export function Multiplication(...value: any[]) {
  return value.reduce((t, v) => t * v, 1)
}

然后我们在package.json为了测试我们使用tsc打包新增build命令添加tsc

  • @relaxed/hook同理

根目录新建 index.ts

import { ref } from 'vue'
export default function useBoolean(initValue = false) {
  const bool = ref(initValue)

  function setBool(value: boolean) {
    bool.value = value
  }
  function setTrue() {
    setBool(true)
  }
  function setFalse() {
    setBool(false)
  }
  function toggle() {
    setBool(!bool.value)
  }
  return {
    bool,
    setBool,
    setTrue,
    setFalse,
    toggle
  } 
}
  • @relaxed/tsconfig

在tsconfig目录下新建base.json

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Default",
  "compilerOptions": {
    "composite": false,
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "inlineSources": false,
    "isolatedModules": true,
    "moduleResolution": "node",
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "preserveWatchOutput": true,
    "skipLibCheck": true,
    "strict": true
  },
  "exclude": ["node_modules"]
}

package.json中

{
  "name": "@relaxed/tsconfig",
  "version": "0.0.0",
  "private": true,
  "files": [
    "base.json"
  ]
}

首先我们需要把tsconfig共享模块添加到两个工具库中

可以直接在@relaxed/hook @relaxed/utils的package.json中添加

  "devDependencies": {
    "@relaxed/tsconfig": "workspace:*",
    "vue": "^3.2.37"
  }

或者

pnpm add @relaxed/tsconfig --filter=@relaxed/hook

pnpm add @relaxed/tsconfig --filter=@relaxed/utils

然后我们如何在playground中使用呢?

第一种方法:在 playground中的package中 新增 两个工具子包的依赖项
  "dependencies": {
    "@relaxed/hook": "workspace:1.0.0",
    "@relaxed/utils": "workspace:*"
  },

然后我们执行
pnpm install

第二种方法: 我们直接通过命令安装
pnpm add @relaxed/utils  @relaxed/hook --filter=playground

*当前工作目录下的代表最新版本
然后我们执行 pnpm dev turbo先在pipeline 中寻找 dev指令 然后 根据你是否配置了管道中的执行任务,最后执行子包中的dev命令,就代表执行playground中的dev命令

<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
import HelloWorld from './components/HelloWorld.vue'
import { Accumulation, Multiplication } from '@relaxed/utils'
import useBoolean from '@relaxed/hook'
const { bool, setBool, setTrue, setFalse, toggle } = useBoolean(false)
</script>

<template>
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
    <div style="margin: 10px 0">
      <d-tag type="warning">
        {{ bool }}
      </d-tag>
    </div>
    <d-button type="tertiary" @click="setFalse"> {{ bool }} </d-button>
    <d-button type="primary" @click="setTrue"> {{ bool }} </d-button>
    <d-button type="info" @click="toggle"> Toogle </d-button>
  </div>
  <HelloWorld msg="Vite + Vue" />
</template>


image.png

然后到了构建环节,我们执行 pnpm build 这时候 我们需要在 turbo.json 中 pipeline 中的 build 设置

"dependsOn": ["^build"],

只有加了 ^ 这样 我们才能 先去 执行 依赖 的 三个子包的build 命令 由于 我们playground 中 依赖了 其它的子包 所以我们必须 加上 ^ 否则turbo 会阻止build 因为我们存在子包的互相依赖,
所以 Turborepo 可以有序的帮助我们进行管理项目之间的逻辑

image.png

4. 总结

Turborepo 可以帮助我们更好的管理Monorepo项目, 凭借自身优秀的任务调度管理和增量构建缓存等等, 都可以帮助我们在未来解决monorepo目前存在的一些问题,进而提高我们的开发效率,以及提升整个项目在构建等方面的性能。

5. 参考


DevUI团队
714 声望810 粉丝

DevUI,致力于打造业界领先的企业级UI组件库。[链接]