基于 Rush 实现 Monorepo 的入门体验

本文参考官方文档学习后总结了关于 Monorepo + Rush + PNPM 的入门实践,以更容易理解的方式组织了文章结构,如果你也需要构建一个大型多项目仓库,不妨可以看看本文

  • 本文不对 Monorepo 做介绍

为什么需要 Rush?Rush 解决了什么问题?

rush是为大型团队准备的,它可以给你提供一个仓库下维护多个项目的构建和发布方案。

包管理存在的问题:当我们使用 Monorepo 的方式来组织项目时,为各项目安装依赖的过程中可能会出现各种问题(如幻影依赖(phantom dependencies)NPM 分身(doppelgangers)

  • Rush 使用符号链接来为每个项目重新构建一个准确的 “node_modules” 文件
  • Rush 一次性安装全部原始依赖在 root 下的 common/temp 目录下,从该目录下提供 Symlink 给到个项目去引用。

Rush 还有什么新特性?

  • 提供项目间自动 link

  • 多线程构建多项目,加快构建速度

  • 自动检测有改动的项目,按配置产出版本号和 changelog

前置知识

shrinkwrap 文件

shrinkwrap 文件是 包管理器的一项功能。可以按照当前项目 node_modules 目录内的安装包情况生成稳定的版本号描述

  • 可能是 shrinkwrap.yaml, npm-shrinkwrap.json, package-lock.json, 或 yarn.lock
  • 由于我们并不知道你使用了哪种包管理工具,因此使用 “shrinkwrap” 来泛指这些文件。

通常,包管理工具会在每个项目文件夹内创建 shrinkwrap 文件,但是在 Rush 中,整个项目共用存储在 common/config/rush “ 目录下的一个 shrinkwrap 文件,它会被存储在 Git 内。

  • 将所有依赖信息整合到单独一个 shrinkwrap 内有一些优势,例如减少冲突、方便查看 diff, 还能提高安装速度。

快速开始

环境准备

安装长期支持的 NodeJS,当然也可以用 nvm 去管理 NodeJS 的版本

以 win 10 演示

Rush 应该结合哪个包管理工具呢?

目前比较主流的包管理工具有:NPM、YARN、PNPM

用哪个包管理工具都是视需求而定的,每个包管理工具都有自己的利弊,但如果是 Monorepo + Rush,则推荐用 PNPM

  • PNPM 能解决 NPM 分身的问题,因为复杂的 Monorepo 中这个问题还是比较容易出现的

安装 Rush

全局安装 Rush(以管理员身份运行命令行窗口)

1
2
3
4
5
# 安装
npm install -g @microsoft/rush

# 帮助手册(如果有显示则表示安装成功)
rush -h

目录规范

  • apps:MWA 应用,如 Web、H5、Electron 应用等
  • examples:模板、示例、演示项目等
  • features:可复用的业务模块
    • 如请求状态码的处理
    • 需要客户端和服务端共用部分逻辑
  • services 后端服务

开启新项目

目的:使用 rush 实现 monorepo 项目管理

初始化

1.新建 gitlab 仓库 monorepo-rush

  • 克隆到本地
  • 进入仓库后进行初始化
1
2
3
4
5
6
rush init

# 提交到仓库(这个看个人选择)
git add .
git commit -m 'initial'
git push

主要生成以下目录,更多请查看

1
2
3
4
5
6
7
8
9
10
11
12
13
monorepo-rush
├─ .gitattributes // 告诉 Git 不要对哪些 shrinkwrap 文件进行合并,因为该操作并不安全(如果不用 git 可以删除)
├─ .gitignore // 告知 Git 不要跟踪哪些文件(如果不用 git 可以删除)
├─ .travis.yml // 配置 Travis CI 服务来在 PR 中使用 Rush (如果不用 Travis 可以删除)
├─ common
│ ├─ config
│ │ └─ rush
│ │ ├─ .pnpmfile.cjs // 用于解决 package.json 文件下错误的依赖关系(如果不使用 PNPM 可以删除)
│ │ ├─ .npmrc // Rush 用该文件配置源,无论是 PNPM, NPM 或者 Yarn
│ │ ├─ command-line.json // 用于自定义 Rush 的命令行命令或参数
│ │ ├─ common-versions.json // 用于指定 NPM 包的版本,它影响 Rush 仓库下所有项目
│ │ └─ version-policies.json // 用于定义发布配置
└─ rush.json // Rush 内主要的配置文件

如果你的分支中已经存在这些文件,rush init 将会发出警告并且不会覆盖已有的文件

自定义配置

配置文件中已经有大量的示例了

rush.json 必须要了解的几个配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"rushVersion": "5.66.0", // Rush 版本,尽量保持最新的:https://www.npmjs.com/package/@microsoft/rush

/**
* 选定包选择器及其版本。
* Rush 会安装适用于自身版本的包管理器,这可以保证构建过程和本地环境隔离。
*
* 选中一个包选择器:"pnpmVersion", "npmVersion", 或 "yarnVersion"。
* 默认是 pnpm
* 选完后,删除 common/config/rush 中之前的 shrinkwrap 文件和其他包管理器相关文件。(否则 Rush 将会报告不支持配置文件),然后运行 rush update --full --purge
*/
"pnpmVersion": "2.15.1",
// "npmVersion": "4.5.0",
// "yarnVersion": "1.9.4",

// 是否使用“类别目录”模型
// 参考 projectFolderMinDepth 和 projectFolderMaxDepth 的注释

// 配置源的访问权限
// 初始的 .npmrc 被配置为使用公开的 NPM 源。如果你将要使用私有源,你应该更新 common/config/rush/.npmrc 文件。
}

新建 React 项目

根据规范,前端项目需要在根目录的 apps 目录下创建

请自行安装脚手架

1
2
3
4
mkdir apps
cd apps

npx create-react-app my-app --template typescript

删除一些可能有影响的文件

  • 删除本地的 shrinkwrap 文件,因为它会被 Rush 的 shrinkwrap 文件代替。
  • 考虑删除 .npmrc 文件,因为 Rush 使用了 common/config/rush/.npmrc.
  • 考虑删除项目的 Git 配置文件,除非该项目有单独的配置。
1
2
3
4
5
6
7
cd my-apps

rm -rf node_modules
rm -rf shrinkwrap.yaml npm-shrinkwrap.json package-lock.json yarn.lock
rm -rf .npmrc # (如果存在的话)
rm -rf .gitattributes # (如果存在的话)
rm -rf .gitignore # (如果存在的话)

告知 Rush 需要接管 my-app 这个项目

  • 编辑 rush.json
1
2
3
4
5
6
"projects": [
{
"packageName": "my-app",
"projectFolder": "apps/my-app"
}
]

为什么 Rush 不能自动检测到这个项目?

Rush 并不会使用通配符来检测项目。该设计有以下考虑:

  1. 深度优先搜索开销太大,尤其是需要重复收集列表时;
  2. 在带有缓存的 CI 机器上,搜索可能会遗漏掉之前构建中的文件;
  3. 集中式的管理所有项目以及其重要的元数据是很有用的,例如,可以让审批等策略更简单。

执行 rush update 安装依赖

1
2
3
4
5
6
7
8
rush update

# 提交到仓库(这个看个人选择)
# 自行切换到根目录下
git checkout -b dev
git add .
git commit -m 'feat: add apps/my-app project'
git push -u origin dev

如果这是仓库内的第一个项目,你将会发现 rush update 创建了一些新文件:

  • common/config/rush/pnpm-lock.yaml: 公共的 shrinkwrap 文件 (此处假定使用了 PNPM)
  • common/scripts/install-run-rush.js: 用于在 CI 任务中以一种可靠的方式来启动 Rush
  • common/scripts/install-run.js: 用于在 CI 任务中以一种可靠的方式来启动任意工具

验证新项目是否构建成功

1
2
3
# 在根目录下
rush build
# Rush 会寻找项目内的 **package.json** 文件内 `"scripts"` 字段下的 `"build"` 指令

Rush 通常会使用系统的 PATH 环境变量来查找脚本,然而,如果你制定了诸如 “gulp” 或者 “make” 等单个单词的指令,Rush 会首先在 common\temp\node_modules\.bin 目录下查找该指令。

如果进程返回非零退出码,Rush 将判定为失败,并阻塞随后的构建。

如果指令对 stderr 存在任意输入,则 Rush 将以错误、警告报告等形式来解释该输入。这将会中断构建流程(设计如此,如果你允许开发者以这种 “狼来了” 的形式合并 PR,很快你就会发现,报警提示很快就会堆积到没人再去看它们)。诸如 Jest 等的工具库认为向 stderr 写入信息是常见操作,对此需要你重定向它们的输出.

即使某个项目不需要被 rush build 处理,你依然需要保留 build 字段,将其设定为空字符串("") 后 Rush 会忽略它们。

常用命令

rush update

  • package.json 文件发生变化时,请务必运行 rush update

rush update 内做了些什么:

  1. Rush 检查或应用各种可能会改变 common/config 内文件的策略。
  2. Rush 会将所有项目内的 package.json 文件与仓库的公共 shrinkwrap 文件进行比较来检查是否有效。
  3. 若无效,则包管理工具会更新 shrinkwrap 文件。
  4. 无论如何,包管理工具都会将所有依赖安装到 common/temp/node_modules 目录下。
  5. 最后,Rush 会给每个项目下构建一个 node_modules 文件夹,该文件夹下内容通过符合链接到 common/temp/node_modules. (该操作等同于 rush link

CI 流水线中使用 rush install 来替代 rush update, 二者的不同点是 rush install 不会更新任何文件,相反,如果存在过失的数据,则会在 PR 上报错,并提示你执行 rush update 或者提示你 commit 其结果。(一些开发者为了防止 shrinkwrap 文件中不符合预期的更新,他们选择使用 rush install 当作常用指令,而不是 rush build

rush rebuild

  • 给仓库内的所有项目执行一个完整的、清除式的构建。
  • 如果你的工具链支持增量构建,那么你可以执行 rush build 来构建那些变动过的项目。

rushx

  • 仅仅想构建一个项目,在对应的项目下运行 rushx 指令,该指令相当于 npm run
1
rushx build # npm run build

rush change

如果你从事库会被 NPM 发布,那么你的仓库可能需要你在 PR 中添加相应的变更日志。如果没有添加,你的 PR 构建会在 rush change --verify 步骤失败。

为了书写变更日志,首先需要把变动以 commit 的形式提交到 Git 中,之后在仓库下执行 rush change, 该指令会检查 Git 历史,并根据变动情况提示你为每个变化的项目书写更新日志。每一条日志会被存储在 common/changes 下的独立文件中。你应该把这些文件添加到 Git 中,以便于后续的提交。

随后,Rush 的自动发布工作流会检查这些文件,以确定哪些包需要发布。它会删除这些文件,并将你的更新信息复制到包的 CHANGELOG.md 文件中。

⏵ 查看 更新日志编写 来获取更多写变更记录的提示。

rush scan

rush scan 指令可以快速地检查出幻影依赖的问题

日常使用组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 从 Git 获取最新的代码
git pull

# 按需安装 NPM 包
rush update

# 清理并重新构建所有项目
rush rebuild

# 进入某个项目内
cd ./my-project

# 假设 package.json 内存在 "start" 指令。
# (通过 "rushx" 来查看可用的命令)
rushx start

注意事项

不要用包管理工具安装或链接依赖

Rush 会在某个中心文件夹安装所有的依赖,之后使用符号链接给每个项目创建 “node_modules” 文件夹。

  • 不要使用包管理工具来安装或链接依赖。
    • 避免使用 npm install pnpm install yarn install 等操作,这些命令会干扰 rush 的符号连接。如果要使用,必须先执行 rush unlink 清除符号链接
  • 如果想执行 git clean -dfx 清理文件夹,必须先执行 rush unlink 清除符号链接,因为它对符号链接的处理并不好

如何重新生成符合链接?

  • rush update

强制重装所有包

Rush 的包管理工具命令是“增量”式的,Rush 有很多措施保证正确性。但是当你在本地调试时,有时会导致你的 NPM “node_modules” 文件夹变得不正确,最终导致奇怪的错误。

  • 执行 rush update --purge 强制重新安装所有包

最后

这里只是rush 的入门学习指引,更多内容可以查阅官网进行学习