monorepo 实践

使用环境:Volta

复用 packages:workspace

使用 monorepo 策略后,收益最大的两点是:

  1. 避免重复安装包,因此减少了磁盘空间的占用,并降低了构建时间
  2. 内部代码可以彼此相互引用
    为了实现前面提到的两点收益,您需要在代码中做三件事:
  3. 调整目录结构,将相互关联的项目放置在同一个目录,推荐命名为  packages
  4. 在项目根目录里的  package.json  文件中,设置  workspaces  属性,属性值为之前创建的目录;
  5. 同样,在  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  文件中直接通过文件名来引用。这使我们可以实现如下目的:

  1. 子项目间复用脚本命令
  2. 像写代码一样编写脚本命令,无论它有多复杂,而在调用时,像调用函数一样调用

注意,我们脚本分为两类「package 级别」与「workspace 级别」,并且分别放在两个文件夹内。这样做的好处在于,我们既可以在项目根目录执行全局脚本,也可以针对单个项目执行特定的脚本。

通过使用 scripty,子项目的  package.json  文件中的  scripts  属性将变得非常精简:

{
  ...
  "scripts": {
    "test": "scripty",
    "lint": "scripty",
    "build": "scripty"
  },
  "scripty": {
    "path": "../../scripts/packages" // 注意这里我们指定了 scripty 的路径
  },
  ...
}

统一包管理:Lerna

格式化 commit 信息

commitLinthusky

如何迁移

Lerna 为我们提供了  lerna import  命令,用来将我们已有的包导入到 monorepo 仓库,并且还会保留该仓库的所有 commit 信息。然而实际上,该命令仅支持导入本地项目,并且不支持导入项目的分支和标签

那么如果我们想要导入远程仓库,或是要获取某个分支或标签该怎么做呢?答案是使用  tomono,其内容是一个 shell 脚本。

使用 tomono 导入远程仓库,您所需要做的只有两件事:

  1. 创建一个包含所有需要导入 repo 地址的文本文件;
  2. 执行 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