TypeScript Monorepo 最佳实践

当我们跨多个代码仓库管理多个项目之间的依赖关系时,既耗时又容易出错。
monorepo 是一种处理上述问题的代码管理架构概念,它将多个项目的所有隔离代码库整合到一个大型存储库中,而不是单独管理它们。当与合适的工具一起使用时,Monorepos 很有优势。因此许多组织采用了在单个存储库中维护多个项目的策略。
Google、Meta 和 Microsoft 等大公司通常在单个 monorepo 中管理组织内多个项目的代码库。这种方法使他们能够在项目之间共享依赖项、库、组件、实用程序、文档等。在项目之间共享代码可确保代码库的一致性和可预测性。然而,依赖管理才是 monorepo 真正的优势所在。如果有人对共享库进行重大的更新,所有受影响的库将会立刻收到这个更新。
尽管 monorepo 有自己的一系列挑战,但它也通过正确的工具解决了这些挑战。其中一个工具是 Typescript,在这篇文章中,我们将研究管理 Typescript monorepo 的最佳实践。

使用 TypeScript Project References

TypeScript Project References 的主要目标始终是帮助解决大型 TypeScript 项目(如 monorepo)中编译时间长的问题。它们可以将一个巨大的项目划分为几个较小的模块,这些模块都可以独立构建。此外,它还可以创建更模块化的代码。使用这种方法,可以大大缩短构建时间,可以在逻辑上分离组件,并且可以以更有条理和逻辑的方式重新组织你的代码。
这是 TypeScript Project References 的文档。

管理依赖于其他包的包

在处理依赖于 Typescript monorepo 中另一个包的多个包时,你必须明确让 Typescript 知道这种依赖关系。例如, @projectName /package-A 依赖于 @projectName /package-B。我们需要添加以下配置,让 Typescrip 知道这个依赖。
首先,你必须在 package-b 的 tsconfig 中添加它。

// packages/package-b/tsconfig.json
{
    "extends": "../../tsconfig.json",
    "compilerOptions": {
      "outDir": "./dist",
      "composite": true
    },
    "include": ["./src"]
  }
}

下一步是在 package-a 中引用包。

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "dist",
    }
  },
  "include": ["**/*.ts", "**/*.tsx"],
  "exclude": ["dist/*"],
  "references": [{ "path": "../package-b/tsconfig.json" }]
}

 

设置工作区

工作空间是在 yarn、NPM 和其他工具中发明的一个概念,可用于以有组织且一致的文件夹结构的形式为 monorepo 存储库中的包和应用程序提供自己的工作空间。monorepos 等大型项目可以从用于管理包和依赖项的工作区中受益。
在 Yarn 工作区中,可以创建项目,例如:

packages/  
  localPackageA/  
    package.json   
    ...  
  localPackageB/   
    package.json   
    ...

使用 Yarn Workspaces,跨工作区安装包变得更快、更轻。此外,它可以防止跨工作区的包重复,还可以在相互依赖的目录之间创建链接,确保所有目录在 monorepo 中都是一致的。
你可以在 yarn 工作区、npm 工作区或 pnpm 工作区之间进行选择。

使用绝对路径导入模块

导入模块时最好使用绝对路径而不是长的相对路径。这对于代码的清晰很重要,因为随着代码库变得越来越大,会有更深的嵌套文件夹和文件,使用它们的相对路径导入它们是在代码库中弄乱的最快方法之一,因为这会使代码混乱且难以阅读。
我们看一下下面的例子。

import { FormType } from '../../../../types/form';
import { DateType }   from '../../../../types/date';

在这种导入的写法中,任何一个开发人员都会发现很难确切知道这些模块是从哪个文件夹中被导入的。
所以最好使用绝对路径。
我们再看一下比上面更好的一个例子:

import { FormType } from 'utils/types/form';
import { DateType }   from 'utils/types/date';

从上面的代码中,导入现在是清晰、可读和可预测的,因为开发人员知道要导入的模块来自哪个文件夹。
使用绝对导入方法在某些会比较冗余,因为它必须写全路径,而不是使用点和斜线。
但是从长远来看,随着代码库创建更多文件夹和文件而变得更大时,它会很好地发挥作用。
有一些工具可以帮助你实现这个功能。只需要在编译代码时将为你的模块添加解析器的工具就可以了。其中一个工具是 Babel Plugin Module Resolver,您可以阅读它的文档:Babel Plugin Module Resolver。

Prettier 和 ESLint

具有多个项目的大型代码库,每天有多个人在很长的时间内工作,因此编码风格往往不一致。
在开发过程中捕获常见错误被认为是开发人员的核心工作,而开发人员比较容易犯下愚蠢的错误有:错误的文件导入、未使用的变量、错误的变量命名、访问为定义的变量、重复的定义函数……
如果使用 Eslint,这些问题都可以避免。
在 monorepo 中使用 Prettier 和 ESLint 配合效果很好。
使用 Prettier,你可以在所有项目中保持代码风格的一致性。只需在 monorepo 的根目录中创建一个 .prettierrc 配置文件,它会自动应用在 monorepo 中的所有包上。
使用 ESLint,可以帮助我们分析 JavaScript 和 TypeScript 代码。和 Prettier 一样,它可以为 monorepos 轻松配置。你只需在 monorepo 的根目录中定义一个 .eslintrc.json 配置文件,它会应用于所有项目。
但是,如果 monorepo 中有很多文件,Prettier 或 ESLint 可能需要很长时间才能运行。这可以通过将脚本定义添加到本地包的 package.json 来解决,这个文件引用项目根目录中的 Prettier 和 ESLint 配置,这样可以让 Prettier 和 ESLint 只针对特定的包运行。

使用 Turborepo

有几个很棒的工具可以帮助 monorepos 提供流畅的开发体验。
其中一个工具是 Turborepo,它是一个强大的工具,有助于在 JavaScript 和 Typescript monorepos 中开发高质量和高性能的构建系统。它具有很多高级功能,其中之一是并行执行任务。
当我们从根文件夹执行 npm run dev 或 yarn dev 时,它会启动 monorepo 中所有可用的项目,只要这些项目的 package.json 文件中有一个 dev 脚本。同样的事情也适用于其他命令,例如 npm run build, npm run lint, npm run start……
在 Turborepo 中,你可以通过配置项目根文件夹下的 package.json 文件来实现:

"scripts": {
  "dev": "turbo run dev",
  "lint": "turbo run lint",
  "build": "turbo run build",
  "clean": "turbo run clean",
  ...
},
"devDependencies": {
  ...
  "turbo": "latest"
}

Turborepo 还附带了各种工具和许多其他配置,默认情况下允许你在深度嵌套的工作空间中并行执行脚本,或者你也可以选择按顺序执行它们或过滤它们。

"scripts": {
  "dev": "turbo run dev --filter=\"docs\"",
  ...
},

Turborepo 带有高级远程缓存功能,本地文件的高性能构建是默认功能,也适用于远程文件。你可以随时选择退出本地缓存。
此外,你还可以使用 Turborepo 创建用于执行脚本的 monorepo 管道。
你可以查看 Turborepo 的文档以了解更多信息。
除了 turborepo 外,还有其他类似的工具,比如 Lerna 和 Nx,你可以使用它们来实现相同的功能。

正确的构建工具

为你的 monorepo 项目的部署选择正确的构建工具是一个非常重要的事情。
你应该谨慎选择,因为如果没有做好构建工作,我们可能会遇到必须在仓库中部署所有代码的问题,即使部署的内容只包含必要的源文件。
就像我们使用 Jest 一样,我们也可以使用 Webpack 在一个可以配置为使用 Typescript 引用的 monorepo 中。这可以通过简单地使用 ts-loader 来实现,并且一切都可以设置成自动工作的模式。
我们还有更多工具可以使用,例如 esbuild。Esbuild 默认提供 TypeScript 支持,因此它会自动解析所有的本地引用,因为我们已经配置了 TypeScript project references。你还可以使用 @yarnpkg 插件添加其他配置,这有助于 Esbuild 从本地 Yarn 缓存中解析外部依赖项。
Changesets 也是一种流行的版本控制工具,用于管理存储库中的多个包,例如 monorepo,它为维护人员提供了一个工作流,有助于自动更新包版本和发布新包。

总结

在 monorepo 中使用 Typescript 可能需要一些繁琐的配置和使用一些最佳实践。
但这样做你将能够提高代码库的可维护性,并在你的公司整个代码库中实现了统一性和可见性,不再需要去跟踪不同的代码仓库。