一、前言
作者最近在在负责对部门的 monorepo 项目进行升级改造,之前一直使用的是 lerna 作为 monorepo 的管理工具,自从得知 Nx 团队收购 lerna 并将 Nx 作为它的加速器接入之后一直想找个机会在项目里实践一下,这部机会就来了嘛。通过学习本篇文章你会收获:
- Nx 出现的目的是什么。
- Nx 是如何体现他智能、快速和可拓展等特点的。
- Nx 与 lerna 的最佳实践,他们是如何相互配合的。
二、Nx 简介
Nx 是一个智能、快速和可拓展的构建系统,非常适用于 monorepo 的场景·,下面会具体介绍 Nx 是如何体现他的智能、快速以及可拓展的能力。
三、Nx 的定位
Nx 的出现主要有两个目标:
-
以最小的成本来加快现有的工作流程。
Nx 可以适用于任何类型的项目,而不仅仅局限于 monorepo,接入成本非常小,同时借助 Nx 对运行产物的缓存、任务并发执行、仅运行更改的模块等能力加快现有的工作流程。
-
无论项目的大小,总能够提供一流的开发体验。
Nx 为开发者提供的 IDE 插件(Nx Console)、可视化项目依赖关系命令等能力能够很好的提高开发者的开发体验。
四、Nx具备的功能
加速工作流程
1、Run Tasks Affected
当开发者需要对 workspace 中的所有项目进行 test 的时候需要执行下面这行命令:
nx run-many --target=test --all
但是随着 workspace 中的项目不断增多,每一次更新 n 个项目就重新 test 所有的项目会使得整体 test 效率变得非常的低,为了解决这个问题, Nx 会对更改的代码进行分析,然后找出需要重新测试的最小项目集合,此时就需要用到 affected 命令:
nx affected --target=test
比如下面这个例子,如果我更改了 Lib 库中的某个文件然后运行 nx affected --target=test,Nx 发现 App1 和 App2 都依赖了 Lib 库,因此他会重新对 App1、App2 和 Lib 重新进行测试。
2、Run Tasks in Parallel
Nx 使用了 Node.js 提供的 Child Process API 来生成子进程,并在子进程中执行任务,以此来达到并行执行任务的目的,比如如果要将运行任务的进程数增加到 5:
nx run-many --target=build --parallel=5
3、Cache Task Results
Nx 拥有最先进和经过实战检验的计算缓存系统,无论使用 Nx 去 build、test 或者 lint 项目,最终的结果都会被缓存,这能够大大减少一遍又一遍地 rebuild、retest 相同的代码带来的高昂代价。
结果可缓存意味着给定相同的输入,Nx 应该总是产生相同的输出,比如涉及到后端 API 的端到端(e2e)测试无法被缓存,因为后端的响应会影响测试结果。
下面通过运行以下命令两次来查看 Nx 缓存的效果:
nx run-many --target=build
第一次:
第二次:
4、Distributed Task Execution
大多数情况下开发者会将整个项目的 CI 作为单个作业启动,负责运行所需的所有任务,这些任务都是同步执行的,使用 Nx 进行此 CI 设置的脚本如下所示:
- nx affected --target=lint
- nx affected --target=test
- nx affected --target=build
该脚本将为当前更改受影响的所有项目运行lint、test 和 build 命令,这种方式的缺点也很明显,所有的任务必须同步执行,所需的时间随着项目的增长而变长,虽然借助 Nx 的 caching 和 affected command 能够有效的减少 CI 的时间,但是这在一些比较极端的情况下(某些更改导致所有项目受影响)并没有太大的效果。
为了提高最坏情况下 CI 时间的性能,就必须实施某种并行化策略,Binning(分享策略)就是一种并行化的策略,即将任务按照类型进行划分,比如说 lint、build、test 是三种不同的任务,然后为每一类任务分一个 agent(代理),最终不同类型的任务就能够并行执行,使用 Nx 进行此 CI 设置的脚本如下所示:
// planning-job.yml
// 获取受影响的项目列表
nx print-affected > affected-projects.json
// 将受影响的项目列表存储在 PROJECTS 环境变量中
node storeAffectedProjects.js
// lint-agent.yml
// 在 lint 代理中执行 lint 命令
nx run-many --projects=$PROJECTS --target=lint
// test-agent.yml
nx run-many --projects=$PROJECTS --target=test
// build-agent.yml
nx run-many --projects=$PROJECTS --target=build
如果某些 test 任务将 build 任务做为先决条件,这种模式可能会变得非常的困难,如下图所示测试test 被延迟,直到所有必要的 build 产物都准备就绪,但是 build 和 lint 任务可以立即开始。
同时 Binning 模式下还存在一些其他问题,比如说某些任务执行完之后会有一些空闲时间,如上图所示,在执行 test 之前,这个 agent 是出于空闲状态的,还有就是 build 的产物很难在不同的 agent 之间进行共享等,更多请查看详情。
为了解决 Binning 模式下的问题,Nx 设计了一套分布式任务执行模式,优点如下:
- 根据任务的平均运行时间将每个单独的任务分配给 agent 作业,将空闲时间减少到可能的最低限度。
- 能够保证任务以正确的顺序执行。
- 使用分布式缓存来共享 build 产物。
使用 Nx 的分布式任务执行后,我们的任务图将看起来更像这样:
智能
1、Dependency graph visualization
为了让 Nx 快速正确地运行任务,它创建了 workspace 中所有项目之间的依赖关系图,直观地探索此图有助于理解 Nx 以某种方式运行的原因以及获得代码架构的高级视图。
运行如下命令:
nx graph
Nx 将会启动一个本地服务并打开一个浏览器窗口,其中包含当前代码库项目图的交互式表示。
2、Code Generators
Nx Code Generators 是一个工具,用于在 Nx 项目中自动生成常见的应用程序框架和代码结构,例如,React 应用程序的 Code Generators 可以生成一个完整的应用程序框架,包括组件、服务、路由等。
下面列举了三类 Code Generators:
-
Plugin Generators
Nx 提供了一些第三方插件来帮助生成特定类型的代码,比如
@nrwl/react、@nrwl/angular、@nrwl/nest等,下面使用@nrwl/react来举例:-
创建一个新的 react monorepo 项目
> ~/ npx create-nx-workspace@latest --preset=react --packageManager=yarn ✔ Repository name · r> ✔ Application name · core ✔ Bundler to be used to build the application · webpack ✔ Default stylesheet format · less ✔ Enable distributed caching to make your CI faster · No -
在项目 core 中添加一个组件
> ~/react-monorepo npx nx g @nrwl/react:component shop --project=cor e ✔ Should this component be exported in the project? (y/N) · false CREATE apps/core/src/app/shop/shop.module.less CREATE apps/core/src/app/shop/shop.spec.tsx CREATE apps/core/src/app/shop/shop.tsx -
创建一个 react ui 库
> ~/react-monorepo npx nx g @nrwl/react:lib shared/ui ✨ Done in 17.31s. CREATE libs/shared/ui/project.json CREATE libs/shared/ui/.eslintrc.json CREATE libs/shared/ui/.babelrc CREATE libs/shared/ui/README.md CREATE libs/shared/ui/package.json CREATE libs/shared/ui/src/index.ts CREATE libs/shared/ui/tsconfig.lib.json CREATE libs/shared/ui/tsconfig.json UPDATE package.json CREATE libs/shared/ui/jest.config.ts CREATE libs/shared/ui/tsconfig.spec.json CREATE libs/shared/ui/src/lib/shared-ui.module.less CREATE libs/shared/ui/src/lib/shared-ui.spec.tsx CREATE libs/shared/ui/src/lib/shared-ui.tsx UPDATE nx.json UPDATE tsconfig.base.json
-
-
Local Generators
local generators 是指开发者自己编写的代码生成器,这些生成器可以在 Nx 项目中使用,通常用于自定义特定项目的代码生成需求,例如在生成组件时自动创建相关的样式文件和测试文件等。
-
Update Generators
Nx Update Generators 是 Nx 工具集中的一部分,它可以帮助开发者快速地升级项目依赖的第三方库、插件、以及 Nx 自身,比如说想要将
@nrwl/workspace升级到指定版本:nx migrate @nrwl/workspace@version这会获取指定版本的
@nrwl/workspace包,分析依赖关系并获取所有依赖包,直到所有依赖项都得到解决,这行命令会产生两个结果:- 更新 package.json
- 如果当前有等待的迁移,则会生成一个 ****migrations.json
需要注意的是此时还没有任何依赖被安装以及其他文件的更新,此时我们也可以去检查一下 package.json 的更改是否有符合预期,如果不符合可以手动更改,接下来就可以进行依赖的安装:
nx migrate --run-migrations
使用 Nx Code Generators 可以大大加速开发过程,使开发人员能够专注于业务逻辑而不是基础代码结构的编写。同时,它们还可以确保生成的代码遵循一致的规范和最佳实践,提高代码的可维护性和可扩展性
可拓展性
1、Plugin Features
Nx 就像构建工具中的 VSCode,除了内置的核心能力之外他还提供了功能拓展的接口,开发者可以开发自己的插件去拓展 Nx 的功能,Nx 官方也提供了许多插件,比如 @nrwl/angular 、 @nrwl/nest 等。
五、Nx & Lerna
Lerna
lerna 是一个流行的 monorepo 管理工具,它主要做了三件事:
- 为单个包或多个包运行命令(lerna run)
- 管理项目依赖(lerna bootstrap)
- 发布包、管理包的版本、生成变更日志(lerna publish)
虽然 lerna 非常适合管理依赖和包的发布,但是如果想拓展基于 lerna 的 monorepo 项目会变得非常痛苦,仅仅是因为 lerna 非常慢,而这正是 Nx 需要解决的痛点,因此可以使用 Nx 去加速机遇 lerna 的 monorepo 项目,两者能够相互配合。
将 Nx 与 Lerna 集成
对于基于 lerna 的 monorepo 项目来说,接入 Nx 的主要策略是继续使用 Lerna 的 bootstrap 和 publish 功能,使用 Nx 用于任务的调度以加速 Lerna,下面是具体的接入流程:
-
安装 Nx
npm install nx --save-dev -
添加 lerna.json 配置
{ ... "useNx": true }默认情况下 useNx 将设置为 false。
-
创建 nx.json
Nx 即使没有 nx.json 也能工作,但是如果想启用 Nx 的其他能力(产物缓存)就必须要在 monorepo 项目的根目录下创建一个 nx.json 并添加对应的配置项。
{ "extends": "nx/presets/npm.json", "tasksRunnerOptions": { "default": { "runner": "nx/tasks-runners/default", "options": { "cacheableOperations": ["build"] } } } }
完成这些步骤后,开发者可以像以前一样继续使用当前的 lerna 项目。所有命令都将以向后兼容的方式工作,但速度会快得多。
六、推荐的Nx配置
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
// 在build、lint、test 时启用缓存机制
"cacheableOperations": ["build", "lint", "test"],
// 任务的并发量
"parallel": 5
}
}
},
"targetDefaults": {
"build": {
// 在构建某个项目时自动构建它的依赖
"dependsOn": ["^build"]
}
}
}