Better

Ethan的博客,欢迎访问交流

use nx or turbo about monorepo in 2025

前端仓库的 monorepo 技术选型很长时间没有更新了,赶紧抓住空闲时间更新一波,当初选用的是 lerna + yarn workspaces 的方式,社区关于 monorepo 这一块也有很大的发展,期间 lerna 经历过一次停止更新后又恢复,一顿搜索后,现在更多的是使用 nx 和 turbo,这里简单学习比对一下。

背景

使用 monorepo 的核心原因

  • 方便跨项目代码复用,同时每个项目可以独立迭代(子包可以单独做版本发布)
  • 统一各个项目的开发环境配置(工程统一标准化 lint/test/build),如代码风格、代码检查、测试配置和打包构建等
  • 相比 multi-repo 而言,代码评审更高效一些,因为不会缺失上下文

作为一个独立的包而言,最关键的特性:独立构建、独立测试、版本管理与发布,可以重点关注 monorepo tooling 是如何最大化支持这些特性的。

之前使用 lerna@4 存在的问题

  • 设计过于简单,仅关注任务执行,如构建、测试、版本管理和发布,但并没有提供类似 cache 能力
  • 多 pkg 协同工作时,开发体验不够丝滑,比如开发 app 时,如果需要对 lib 进行修改,需要手动开启 lib watch 自动编译
  • 集成新的技术选型或新建项目时,需要开发者自己考虑很多的配置问题,之前我尝试通过 template 模版的方式优化该痛点,但维护 template 不过期很费心的

将 Package 类型氛围 Application Packages 和 Library Packages,为了区分两者,社区偏好使用 packages 存放库代码,使用 apps 存放应用代码。

在了解 nx 和 turbo 之前,先了解一下不同 Compilation Strategies 的区别

  • Just-in-Time Package:应用程序打包器在包使用时编译包,仅需少量配置,这种策略在如下情况时十分有用
    • 您的应用程序是使用现代的打包器(如 turbopack、webpack 或 Vite)构建的
    • 您希望避免配置和设置步骤
    • 您对应用程序的构建时间感到满意,即使您无法为包命中缓存
  • Compiled Package:通过适当的配置,使用 tsc 或打包器等构建工具编译包。
    • 大多数编译包应该使用 tsc 即可,因为该包大概率被使用了打包器的应用消费,应用打包器将准备好库包,以便在应用程序的最终包中分发,处理 polyfill、语法降级和其他等问题
    • 只有当你有特定的用例需要捆绑器时,才应该使用它,比如将静态资产捆绑到包的输出中。
    • 编译包需要更深入的知识和配置来创建编译输出
  • Publishable Package:编译并准备一个要发布到 npm 仓库的包。这种方法需要最多的配置
    • 由于你不知道你的包最终会被消费者如何使用,因为需要遵循最严格的打包策略要求

nx

nx 的特性

  • 核心定位:任务运行器
  • 通过脚手架机制使创建项目更简单
  • 丰富的插件生态让你快速集成特定选型
  • 通过 caching 和 parallelization 技术提升构建速度
  • 接入大模型,预设好合适的 nx-specific 上下文
  • 帮助你更好管理 monorepo 项目依赖关系,项目越大使用

nx plugins 最关键的作用:抽象一些较低级别的工具和预配置,从而获得更多一致性。

底层原理与核心配置

  • npm workspaces:帮你管理各自的依赖和软链(早期使用 TypeScript path aliases 机制)
  • TypeScript project references 实现对依赖的增量 build
  • project.json 项目级配置
  • nx.json 全局配置
  • plugins 机制

project references 需要指定一个 tsconfig 配置文件:The path property of each reference can point to a directory containing a tsconfig.json file, or to the config file itself (which may have any name).

常用插件

  • @nx/js 适用于 JS/TS 项目的生成器
  • @nx/react 适用于 react 项目的生成器

常用命令

  • npx nx graph 展示项目的依赖关系图
  • npx nx show projects 显示项目列表
  • npx nx run-many -t <target_name> 所有项目中同时运行
  • npx nx effected -t <target_name> 仅有影响的项目运行
  • npx nx g : [options]
  • npx nx migrate latest 自我更新

小试牛刀

我们从项目创建、项目引用、项目测试、项目打包、项目发布这几个方面浅玩一下,假设我们需要创建的项目 Team Name 是 hedgebag。

只考虑新项目(不考虑已有项目如何迁移过去),直接使用 CLI 工具创建项目,我们选择用 Next 创建一个 React 项目

# JavaScript Only
npx create-nx-workspace@latest

为了测试项目引用,使用 @nx/js 生成一个 utils lib,命令如下,会询问 build/test/lint 等选型。

npx nx g @nx/js:lib packages/utils

在 React 项目使用 utils lib,在 package 中声明 utils 依赖,版本修饰符使用 * 即可,表示这是一个 internal package。

{
  "devDependencies": {
    "@my-org/some-project": "*"
  }
}

此时如果你仔细查看 React 项目和 utils 包的 package.json,之前最为熟悉的 scripts 字段并不存在,那我该如何 dev/build/lint/test 呢。这里就是 nx 最大的魔法存在,你需要使用借用 nx 工具进行命令调用,如

npx nx dev city-design
npx nx build utils
npx nx test utils # 如果有额外参数需要透传给测试工具,如 jest --watch,直接在最后添加命令即可

最后再来看包版本管理和发布,nx release 指令会帮你管理好版本、 changelog 以及将新版本发布到 NPM 市场。

  1. 在 nx.json 中通过 release.projects 配置定义好需要被 nx 管理发布的包,如 ["packages/*"]
  2. 在项目自身的 package.json 中将 private 设置为 false
  3. 使用 nx release 命令,常用参数有 --first-release、--dry-run 等

了解 nx 中各式各样的 tsconfig.json

  • tsconfig.base.json 最基础的被共享的配置,常用于配置统一的编译目标
  • tsconfig.json 项目级配置,继承 base 同时定义特有的配置,目的是为你的 IDE 提供对 tsconfig.*.json 文件引用
  • tsconfig.app.json 应用项目的生产环境配置,如排除测试文件
  • tsconfig.lib.json 库项目的发布配置,如开启 composite 启用项目引用,强制生成生命文件
  • tsconfig.spec.json 测试代码的独立配置,如单独配置测试框架类型,仅包括测试文件等

notice: 在学习实践过程中,官网视频讲解演示引用需要 build 的 ts lib 时,称此时该 lib 需要 pre-build 才能使用,但这会导致问题就是开发环节中对该 lib 进行修改,需要手动 build 一次才会生效,这很麻烦,因此 nx 还提供了 watch-deps 命令,但开启时修改 lib 会自动重新 build。但这在最新代码中,无需开启 watch-deps,因为通过 package.json Conditional Exports 进行了优化,当为 development 模式时,直接加载 ts 文件,而不是 build 后的 js 文件。

在 nx lib pkg 实践的过程发现 import/export .ts 文件时,路径会自动提供且只提供对应的 .js 文件,为什么会这样呢?原因是使用 TypeScript moduleResolution 设置为 nodenext 特性时,要求必须显示指定文件后缀,即使是 ts 文件,这样设计的目的是与 node.js 解析 ECMAScript 模块保持一致。如果你不想要声明后缀,你需要一个 bundler(如 webpack/rollup/esbuild) 或 build(如 babel) 工具,帮助你在处理打包或转移时自动添加后缀,此时需要把 moduleResolution 设置为 bundler。

turbo

turbo 同样是依赖包管理器 workspaces 特性,但在任务执行这一块,没有 nx 那么多魔法,你需要进行手动定义任务流,你可以在 turbo.json 和项目中 package.json 中 script 查看对于任务的定义,turbo 对于依赖关系和 cache 功能都依靠 turbo.json 进行描述。

turbo 使用起来更轻量简单,追求速度和简单,重点关注优化 build/test 缓存和任务编排,设计成与现有工具无缝集成。nx 看上去是 turbo 的超集,它可以做 turbo 能做的一切,甚至更多,所以 nx 宣称不需要特殊的配置,它只是在 turbo 工作空间上工作。你可以理解成 turbo 追求简单,解决了最核心痛点(缓存和任务编排),其他锦上添花的功能就先不考虑,如脚手架项目搭建,快速集成特定选型,依赖关系图等,因此中小型前端项目可以考虑 turbo,何况 nx 不是宣称可以无缝在 turbo 项目上使用吗,岂不是后期需要用 nx 时,也可以轻松切换?

在 turbo 中,你需要自己管理好项目的创建,包括环境配置、任务定义、以及集成 turbo 任务管理等(lint/test/build),不同于 nx 提供了 generators plugin 通过问答方式自动进行代码生成,毕竟搭建一整套环境是很繁琐的。turbo 选择让您以自己的方式处理工具,根据自己的喜好配置任何工具。

turbo 常用命令

  • turbo build 根据依赖图构建所有的项目,可以通过 --filter 参数筛选,也可以进入指定项目下,单独进行构建。 --affected 用于筛选受影响的项目
  • turbo run 无参数调用,可以查看可以运行哪些任务,turbo build 是 turbo run build 的简写
  • turbo watch 代码更改时自动运行任务

小试牛刀

turbo 的使用更接近标准的工作方式,使用 CLI 创建好项目后,你需要创建 App 或 lib 都需要手动进行,比如你要创建 next 项目,可以使用 next CLI 将项目创建在 apps/app_name 下即可,创建 lib 如 utils 包,则创建在 packages/utils 即可,需要你自己自行集成 build/test 等,当然前提是 apps 和 packages 是已经做好了 workspaces 配置,然后项目各自的 script 手动定义好且尽可能标准化,如 dev/build/test/lint/type-check 等,因为同样要交给 turbo 进行命令驱动,项目之间的依赖关系,如何缓存等都交给 turbo.json 进行配置。

由于 turbo 足够简单,自身并不考虑打包、测试、版本发布等,但官方文档写的很清晰,也给你推荐了一些社区工具,如

  • ts 项目需要打包:tsup 简单且快速的打包 TypeScript 项目的库
  • 版本管理和发布:changesets 版本管理和更新日志管理

简单总结

turbo 更加简单,但需要的锤子到给到你了,由你自己决定是否使用和怎么用,给你更多的自由度,nx 则更像是已经组装好的成品,你只需要告诉它需要什么,我会给你准备好,在一致性上做出的努力更多,不够自由,自己初看起来,会觉得有点魔法。

资料



留言