Monorepo架构: Lerna、NX、Turbo等对比与应用分析
概述
- 对于大型的 Monorepo 项目来说,Nx 绝对算是神器,在包管理和版本控制部分有优势
- 对于大型 Monorepo 项目,Nx 是非常实用的工具,在包管理、版本控制以及构建、测试优化等方面都有一定作用
- 下面我们来对比一下这几种工具
NPM 包流行度与状态分析
- 首先打开浏览器,访问 @microsoft/rush-vs-lerna-vs-nx-vs-turbo
- 我们看到一个横向的 NPM 下载记录。过去一年,下载量最多的是 Nx,其次是 Turbo,然后是 Lerna,最后是 Rush
- 从下载量能看出它们的流行程度,Nx 的使用量最多
- 再看这些项目仓库的状态,仓库数量最多的是 Lerna,其次是 Nx,它们的更新频次都很高,基本在一到两个月内都有提交,且版本都是正式版本,没有出现零点几的版本
- 介绍 NPM 包的目的,一是了解包的流行度,从侧面了解其生态情况;二是了解包的更新频次和维护状态
需求选型及工具关系介绍
(一)Nx 与 Lerna 的关系
- Nx 是由前谷歌员工开发的构建系统,利用了谷歌内部工具。Lerna 和 Nx 背后的公司都是 NRWL,NRWL 接管了 Lerna 的管理、维护和更新工作。
- Nx 利用 Lerna 检测工作区中的包及其依赖关系,Lerna 遵循 Nx 强大的任务运行程序来运行脚本,允许并行运行、版本缓存结果,并将其分布在多台机器上,同时确保尊重包之间的依赖关系。
- 有关 Lerna 和 Nx 版本的兼容信息,可在 Lerna 官网查看
- Nx 的重心在构建功能,也包含开发、测试和构建优化等;而 Lerna 更聚焦于包的版本管理和发布
- 如果不需要使用 Nx 扩展功能,开源项目免费;若使用云特性或云计算资源,则需收费
- Nx 和 Lerna 可以互补使用,既可用 Lerna 管理包的版本和发布,也可用 Nx 加速构建过程、管理项目依赖
(二)Nx 与 Turbo 的关系
- Turbo 是 2021 年 12 月发布的构建工具,借鉴了 Nx 的方法,因此有人会比较 Nx 和 Turbo。
- Turbo 是 Nx 的一个子集,同时使用 Turbo 和 Nx 没有意义。Nx 发布了自己的 SaaS 产品,Turbo 虽也类似,但未分离出来,所以在指南中将 Turbo 与 Nx Cloud 进行联合比较。
- 若不想使用 Nx Cloud,可以构建自己的 API 来缓存项目构建过程中的文件,以提速项目构建。
Nx 与 Turbo 的特性对比
1 ) 渐进式采用
Turbo 和 Nx 的集成只需在项目中添加一个新的配置文件,不会破坏项目结构。
2 ) 工作空间部分
对于复杂的工作空间,Nx 和 Turbo API 都会分析并生成配置文件
Nx 的可视化视图比 Turbo 更具体,有交互式工具查看,更直观
3 ) 检测受影响的项目或包
- 两者都支持本地任务协调,包括任务并行,但 Nx 在可插拔方面支持得更好。
4 ) 本地计算缓存
- 两者都支持,可利用缓存提升项目构建速度。不过在存储文件策略上有差异,Nx 的输出差异更小,Turbo 有自己的输出规则,在处理某些输出时效果不如 Nx
5 ) 远程计算缓存:两者都支持
6 ) 分布式任务执行
- Nx 支持,能在多台机器上运行命令,保留单台机器开发体验
- Turbo 不支持,对大型项目有影响
7 ) 生态方面
- Nx 有专门的 VSCode 插件以及其他编辑器插件,对开发者更友好
- Turbo 没有
8 ) 配置方面
- 过去五年 Nx 不断壮大,Nx 和 Turbo 生成的配置量相同
9 ) 终端环境
Nx 不会修改终端环境,对喜欢自己终端环境的开发者更友好
Turbo 会有自己的输出颜色设置
10 ) 插件支持:Nx 的插件支持范围更广
11 ) 构建性能:
- 相同的五个 Next.js 应用项目,使用 Nx 构建大概花费 0.64 秒,使用 Turbo 大概花费 4.4 秒
- 测试时 Turbo 已处于相对稳定状态,后续可能有性能提升
- 虽然 Turbo 主要由 Go 和 Rust 编写,Nx 由 TS 编写,但大部分繁重计算核心用 Node.js 和 Rust 功能模块完成,性能不受影响
- Nx 借鉴了类似 React 的方法,在很多方面比 Turbo 快,但启动时每次会消耗 70 毫秒,不过实际中影响不大, 启动1k个项目也是 70毫秒
Nx 与 Rush 的关系及对比
Rush 由微软的 SharePoint 团队维护和推动,有中文文档,对中文开发者友好,其生态也较完整。与 Nx 对比:
1 ) 构建特定项目
- 有人认为 Nx 不支持直接针对某个项目构建,实际上目前 Nx 已支持
2 ) NPM 依赖项隔离
- Rush 默认所有项目使用根 package.json 中特定的一组共享包
- 更符合 Node 模块使用习惯。
3 ) 查看依赖关系图
- Rush 没有社区插件,不支持使用现代化工具或框架
- 如 Next.js、Storybook、Vue 等
4 ) 语义化包依赖发布
- Rush 可以提供语义化的包依赖发布,Nx 可借助 Lerna 提供类似功能
Lerna解析:常用命令、版本控制与发包
1 ) 初识Lerna
- 我们先打开Lerna的官网,官网页面有这样一句话 “The original tool for JavaScript monorepos”,这表明Lerna的应用场景主要是针对Monorepo架构。
- 官网还提到,Lerna提供了
init
命令,帮助我们快速初始化Lerna项目。点击 “Get started” 处会有相关视频,大家可以参考视频学习Lerna的一些命令。
2 )Lerna基础操作与核心概念
- Lerna作为一个CI工具,可以操纵和运行任务。在官网的 “features” 中,第一个功能是 “run tasks”,这和我们常用的PNPM类似
- 当执行某一个
run
命令时,它会对Monorepo中的所有项目执行相同命令,比如build
命令,无需手动逐个执行 - 并行运行命令:Lerna提供了并行运行命令,如
lerna run test, field, mete
,会同时针对所有workspace运行这三个脚本 - 针对特定包运行:如果要针对某一个包运行命令,可使用
scope
参数。这和PNPM的filter
关键词类似,但使用方式不同,Lerna是将scope
加在命令后面
3 )项目实践
- 课程资料文件夹中有一个名为 “nernon” 的示例项目,这是一个基础的React应用,即使不了解React,也不影响我们学习Lerna命令。
- 下载并解压项目后,在终端使用Node 18.19.0版本,用PNPM安装依赖。
- 项目中有四个workspace,我们可以使用PNPM的
filter
过滤特定名称的应用并执行脚本。 - 例如,项目中的
remixapp
依赖于future
和header
组件,使用PNPM运行项目时,需要依次执行多个命令,较为繁琐。而使用Lerna可以简化这个过程。
4 )Lerna与PNPM对比
- 使用PNPM管理Monorepo项目时,执行构建命令可能需要手动处理依赖关系。
- 而Lerna通过初始化命令(如
npx lerna init
)可以创建lerna.json
文件,记录项目基础信息。 - 使用
npx lerna ls
命令可以查看Lerna管理的包。
5 )Lerna的优势
- 缓存功能:Lerna通过
lerna cache
命令提供缓存功能。执行npx lerna cache
会有向导提示,我们可以根据项目情况选择需要按顺序执行的脚本、可缓存的脚本以及脚本的输出目录等信息。配置完成后,会生成nx.js
文件记录配置信息。再次执行构建命令时,如果项目文件未发生变化,会使用本地缓存,大大提高构建速度。 - 版本控制:Lerna可以方便地进行语义化版本号控制,使用
npx lerna version
命令并可添加--no-private
参数,排除私有仓库的包。在执行该命令前,需要确保项目的Git仓库已正确配置并推送到远程仓库。执行命令后,会出现可供选择的版本号列表,选择合适的版本号并确认后,Lerna会自动更新版本号并提交到仓库。 - 包发布:Lerna发布包非常方便,使用
lerna publish
命令可以自动推送和发布packages
中发生修改且版本号变化的包。如果要发布到远端仓库,可添加--from-package
参数。发布前需要确保已登录到相应的NPM仓库。
6 )Lerna的其他功能
- 监视功能:Lerna的
watch
功能在6.4.0版本之后可用。使用lerna watch
命令可以监视workspace文件的变化,并执行对应的NPM脚本。可以通过--scope
指定包名,或使用--since
参数,只有当文件发生变化时才执行构建命令。使用lerna watch
的好处是构建出的版本直接适用于最终发布,免去了中间构建文件不适合发布的问题 - Lerna在Monorepo项目管理中具有诸多优势,如缓存、版本控制、包发布等功能,能有效提高开发效率和项目管理的便捷性。下节课我们将学习其他工具
Turborepo上手:缓存、运行脚本与Lerna、NX横向对比
- Turborepo,它有三种应用场景
- 1.添加到现有项目(Add to existing project):即 Turborepo 加入到已存在的项目中
- 2.创建全新的 Monorepo 项目(Create a new Monorepo):使用 Turborepo 创建一个新的 Monorepo 项目
- 3.添加到已有的 Monorepo 项目:将 Turborepo 添加到已有的 Monorepo 项目里
- 之前实践的 Lerna,通常是在已有的项目中添加,这种场景更为常见。
- Turborepo 也具备创建新的 Monorepo 的能力,它内置了一些模板
- 接下来我们进行实践,包括添加到项目中,并与所选项目做对比
1 ) 创建新的 Monorepo 项目
我们来创建一个新的 Monorepo 项目,查看其代码形式。选择 pnpm,打开终端工具,找到一个空的目录。
- 执行命令:使用
pnpm dlx turborepo@latest
回车执行。 - 进入提示列表:显示询问是否要创建 Turborepo,输入一个名字,如
turborepo-monorepo-model-create
回车执行。 - 选择模板:选择
pnpm - workspace - base
,然后开始下载对应的文件。 - 等待初始化:下载完成后,会帮我们完成初始化,包含 docs 文档、web 页面等。紧接着开始安装对应的依赖。
- 运行项目:执行
pnpm run dev
回车。需cd
到刚才创建项目的目录。
2 ) 项目结构与构建测试
项目初始化完成后,我们可以查看其项目结构。在根目录的 package.json
中,有 Turborepo 相关配置,以及 ESLint 配置、TypeScript 配置等。
- 构建项目:执行
pnpm build
回车。首次执行时,没有缓存,会开始执行构建流程,构建完成后会将结果缓存下来。构建完成 web 和 docs 总共花费了 14.061 秒。 - 再次构建:再次执行构建命令,速度明显加快,仅用 290 毫秒就执行完成,这体现了缓存的作用。
3 ) Turborepo 的能力与不足
能力
- 缓存能力:Turborepo 支持缓存,包括本地缓存和远端缓存。远端缓存有两种方式:一是使用 Vercel,但在国内连接可能不稳定;二是自定义远端缓存(Custom Remote Cache),不过需要开发对应的接口,且没有具体开发指引,适合大型团队。
- 运行脚本能力:Turborepo 能自动处理依赖,支持并行运行任务。例如同时执行
pnpm run link build test
命令,它会智能处理任务顺序,充分利用 CPU 多线程能力。多次执行构建命令,会因缓存加速,显示所有任务都被 Turborepo 加速和缓存。
不足
- 缺乏 Lerna 的版本控制和发包能力。
- 其任务图(Task Graph)没有像 NX 那样全面美观的 UI 界面。不过在过滤运行、配置等方面,NX 和 Lerna 也有较好的支持。
4 ) 与 NX 对比
- 后续将 Turborepo 与 NX 在相同项目中进行比较,从构建时间、速度和加速效果等方面做横向对比。
- 在 Turborepo 项目中加入 NX,使用
npx nx init
执行,完成 NX 的初始化。 - 将
build
和link
加入缓存,构建build
需按顺序执行,link
则不需要。 - 在配置输出目录时,NX 的
nx.json
只支持一个输出目录。 - 通过执行构建命令对比发现,使用缓存后,NX 和 Turborepo 都能提升构建速度,且 NX 的执行速度相对较快。
Turborepo 远程缓存使用流程(Vercel 配合)
远端缓存就是在一个团队里共享其他人构建过后的缓存文件,以提升 Monorepo 项目的构建速度
-
如果在单台机器上执行
npm rebuild
,花费了 9.4 秒。另外一台机器再运行同样的操作,同样需要花费 9.4 秒。 -
这样,每个人执行 Monorepo 项目时,即便项目没有修改,都需要花费 9.4 秒。
-
但如果加入了远端缓存,只要有一个小伙伴运行过一次项目,其他小伙伴再运行时就会进入加速状态,缓存得到有效应用。
-
这样一来,对机器性能的要求就不需要那么高,也不用等待那么长时间。我们可以用一台机器做 CI,将推送到仓库的代码全部进行构建。
-
在本地调试或进行业务功能开发时,把已缓存的公共模块的打包文件拉取到本地,运行调试脚本或构建脚本,从而加速本地开发。
-
那它是怎么操作的呢?这里直接给出了几个命令,这种学习方式不太友好
-
有 Vercel 出品的关于如何将 Turborepo 部署到 Vercel 上的流程文档,更加详细
-
下面以文档为例,介绍集成步骤,主要分为两个步骤:
1 ) 步骤一:处理本地环境变量
- 主要在
package.json
中设置一些环境变量,如NODE_ENV
环境变量以及GLOBAL_ENV
环境变量等。 - 这些环境变量是在传递项目时使用的,生产环境和本地环境所需的变量可能不同,需根据实际情况设置。
2 ) 步骤二:在 Vercel 上创建项目
- 登录 Vercel 界面。若未注册登录,可选择使用 GitHub 账号登录。登录后,在左侧选择自己的账号。
- 在 GitHub 上创建一个远端仓库。创建好后,在 Vercel 界面点击
Import
,进入项目配置界面。 - 配置界面中有
Build Settings
,采用默认选项即可。可切换构建类型,如选择web
,然后点击Continue
,上面会显示默认命令,无需修改,继续下一步。 - 配置完成后,回到相关界面,有一个
Setup Remote Cache for Turborepo on Vercel
的操作。打开新终端,执行npx turborepo login
进行跳转授权。授权完成后,执行npx turborepo link
,会询问是否同意远端缓存,选择账号后回车执行,显示成功设置。
3 ) 测试缓存效果
- 在官方脚本或命令中有说明,删除
node_modules
下的cache
文件夹,再执行构建命令,若出现Remote Caching Enabled
标志,说明缓存生效。 - 执行构建时,首次执行可能会从远端下载缓存,花费时间较长,但之后再执行就会变快。
已有项目集成 Turborepo 和 Vercel 远程缓存的流程
- 创建账号和仓库:创建 Vercel 账号,在 GitHub 上创建一个自己的仓库,如
turborepo-learn-remote-caching
,复制仓库地址。 - 初始化项目仓库:在项目中初始化仓库,添加远程仓库地址,将本地代码进行
commit
提交并推送到远端。 - 配置项目文件:打开项目的
package.json
,添加packageManager
字段,选择npm
并指定版本,如8.12.1
。 - 安装和配置 Turborepo:全局安装
turborepo
,在项目根目录创建turbo.json
文件,复制相关schema
内容并粘贴。创建pipeline
并粘贴到schema
后面,删除不必要的注释和output
部分。 - 添加依赖和测试:在
package.json
中添加turbo
依赖,打开终端工具安装依赖。先在本地执行turbo build
进行测试,确保能正常构建。 - 集成 Vercel:在 Vercel 上点击
New Project
,选择在 GitHub 上创建的仓库进行Import
。使用npx turborepo link
关联项目,选择相应项目范围,显示success
表示关联成功。 - 同步缓存:小伙伴们在本地执行
turbo build
前,先删除node_modules
里的对应缓存,这样执行时不仅会产生缓存文件,还会将其同步到远端服务器。再次删除缓存文件后执行turbo build
,就相当于模拟其他小伙伴执行构建命令,会使用远端服务器上的缓存。
存在的问题
turbo.json
的配置不够自动化,不如其他工具(如 Nx)方便。- 不一定非要部署到 Vercel 上,有更多选择会更灵活。
- Vercel 对于国内小伙伴来说,连接速度可能较慢,部分区域网络连通性不佳。
- Vercel 共享缓存的功能需要开通付费版本(Pro Plan),每月费用 20 美元。若没有刚需,使用本地缓存即可。