monorepo 实践
使用环境:Volta
复用 packages:workspace
使用 monorepo 策略后,收益最大的两点是:
- 避免重复安装包,因此减少了磁盘空间的占用,并降低了构建时间;
- 内部代码可以彼此相互引用;
为了实现前面提到的两点收益,您需要在代码中做三件事: - 调整目录结构,将相互关联的项目放置在同一个目录,推荐命名为
packages; - 在项目根目录里的
package.json文件中,设置workspaces属性,属性值为之前创建的目录; - 同样,在
package.json文件中,设置private属性为true(为了避免我们误操作将仓库发布);
经过修改,您的项目目录看起来应该是这样:
.
├── package.json
└── packages/
├── @mono/project_1/ # 推荐使用 `@<项目名>/<子项目名>` 的方式命名
│ ├── index.js
│ └── package.json
└── @mono/project_2/
├── index.js
└── package.json而当您在项目根目录中执行 npm install 或 yarn install后,您会发现在项目根目录中出现了 node_modules 目录,并且该目录不仅拥有所有子项目共用的 npm 包,还包含了我们的子项目。因此,我们可以在子项目中通过各种模块引入机制,像引入一般的 npm 模块一样引入其他子项目的代码。
统一配置
您一定同意,编写代码要遵循 DRY 原则(Don’t Repeat Yourself 的缩写)。那么,理所当然地,我们应该尽量避免在多个子项目中放置重复的 eslintrc,tsconfig 等配置文件。幸运的是,Babel,Eslint 和 Typescript 都提供了相应的功能让我们减少自我重复。
TypeScript
我们可以在 packages 目录中放置 tsconfig.settting.json 文件,并在文件中定义通用的 ts 配置,然后,在每个子项目中,我们可以通过 extends 属性,引入通用配置,并设置 compilerOptions.composite 的值为 true,理想情况下,子项目中的 tsconfig 文件应该仅包含下述内容:
{
"extends": "../tsconfig.setting.json", // 继承 packages 目录下通用配置
"compilerOptions": {
"composite": true, // 用于帮助 TypeScript 快速确定引用工程的输出文件位置
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}Eslint
对于 Eslint 配置文件,我们也可以如法炮制,这样定义子项目的 .eslintrc 文件内容:
{
"extends": "../../.eslintrc", // 注意这里的不同
"parserOptions": {
"project": "tsconfig.json"
}
}Babel
Babel 配置文件合并的方式与 TypeScript 如出一辙,甚至更加简单,我们只需在子项目中的 .babelrc 文件中这样声明即可:
{
"extends": "../.babelrc"
}
当一切准备就绪后,我们的项目目录应该大致呈如下所示的结构:
.
├── package.json
├── .eslintrc
└── packages/
│ ├── tsconfig.settings.json
│ ├── .babelrc
├── @mono/project_1/
│ ├── index.js
│ ├── .eslintrc
│ ├── .babelrc
│ ├── tsconfig.json
│ └── package.json
└───@mono/project_2/
├── index.js
├── .eslintrc
├── .babelrc
├── tsconfig.json
└── package.json统一命令脚本:scripty
scripty 允许您将脚本命令定义在文件中,并在 package.json 文件中直接通过文件名来引用。这使我们可以实现如下目的:
- 子项目间复用脚本命令;
- 像写代码一样编写脚本命令,无论它有多复杂,而在调用时,像调用函数一样调用;
注意,我们脚本分为两类「package 级别」与「workspace 级别」,并且分别放在两个文件夹内。这样做的好处在于,我们既可以在项目根目录执行全局脚本,也可以针对单个项目执行特定的脚本。
通过使用 scripty,子项目的 package.json 文件中的 scripts 属性将变得非常精简:
{
...
"scripts": {
"test": "scripty",
"lint": "scripty",
"build": "scripty"
},
"scripty": {
"path": "../../scripts/packages" // 注意这里我们指定了 scripty 的路径
},
...
}统一包管理:Lerna
格式化 commit 信息
如何迁移
Lerna 为我们提供了 lerna import 命令,用来将我们已有的包导入到 monorepo 仓库,并且还会保留该仓库的所有 commit 信息。然而实际上,该命令仅支持导入本地项目,并且不支持导入项目的分支和标签
那么如果我们想要导入远程仓库,或是要获取某个分支或标签该怎么做呢?答案是使用 tomono,其内容是一个 shell 脚本。
使用 tomono 导入远程仓库,您所需要做的只有两件事:
- 创建一个包含所有需要导入 repo 地址的文本文件;
- 执行 shell 命令:
cat repos.txt | ~/tomono/tomono.sh(这里我们假定您的文本文件名为repos.txt,且您将 tomono 下载在用户根目录;
repo 文件内容示例如下:
// 1. Git仓库地址 2. 子项目名称 3. 迁移后的路径
git@github.com/backend.git @mono/backend packages/backend
git@github.com/frontend.git @mono/frontend packages/frontend
git@github.com/mobile.git @mono/mobile packages/mobile