了解MultiRepo,看完这一篇文章就够了

1,339 阅读9分钟

前言

在传统项目开发模式中每个项目都对应单独的一个代码仓库。这种模式称之为方式MultiRepo。

MultiRepo缺点

在开发过程中,经常遇到一些组件或者功能复用的情况。一般情况我们是将公共的逻辑代码抽取出来,然后发布到npm,这样我们所有的项目通过安装依赖包就能实现复用。

如果依赖包里面某个功能或者工具函数出错了或者我们需要更新,我们需要做一下几个步骤:

  1. 去修改源代码
  2. 重新发布新包
  3. 通知所有项目安装新版本依赖包。

只是修改或者更新依赖包,就需要这么复杂的步骤,在开发过程中,修改代码和bug是不可避免的。这样就增加了很多的开发工作。

Monorepo好处

由于所有的项目放在一个仓库当中,复用起来非常方便,如果有依赖的代码变动,那么用到这个依赖的项目当中会立马感知到。并且所有的项目都是使用最新的代码,不会产生其它项目版本更新不及时的情况。

另外所有项目复用一套标准的工具和规范,无需切换开发环境,如果有新的项目接入,也可以直接复用已有的基建流程,比如 CI 流程、构建和发布流程。这样只需要很少的人来维护所有项目的基建,维护成本也大大减低。

概念

Monorepo(monolithic repository) 是管理项目代码的一个方式,把多个项目放在一个仓库里面。

目前最常见的 monorepo 解决方案是 lerna 和 yarn 的 workspaces 特性。用 yarn 处理依赖问题,lerna 处理发布问题。

目录结构大致如下:

├── packages
|   ├── pkg1
|   |   ├── package.json
|   ├── pkg2
|   |   ├── package.json
├── package.json

Lerna

Lerna 是 npm 模块的管理工具,为项目提供了集中管理 package 的目录模式,如统一的 repo 依赖安装、package scripts 和发版等特性。

安装

全局安装

npm i -g lerna

初始化项目

lerna init

初始化后,会生成 packages 空目录和 package.json 和 lerna.json 配置文件,配置文件如下:

//package.json
{
  "name": "root",
  "private": true, // 私有的,不会被发布,是管理整个项目,与要发布的npm包解耦
  "devDependencies": {
    "lerna": "^3.22.1"
  }
}

//lerna.json
{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0"
}

创建项目

执行命令后可修改包信息,这里创建 @monorepo/components 和 @monorepo/utils

lerna create @monorepo/components

安装依赖

// 为所有 package 增加 lodash 模块
lerna add lodash
// 为 @monorepo/utils 增加 lodash 模块(lodash可替换为内部模块,如@monorepo/components)
lerna add lodash --scope @monorepo/utils

erna add 的鸡肋之处是一次只能安装一个包

依赖包管理

一般情况下 package 的依赖都是在各自的 node_modules 目录下,这不仅增加了包的安装和管理成本,还可能会出现同一个依赖有多个的情况。所以可把所有 package 的依赖包都提升到工程根目录。

lerna 和 yarn workspace 都可以把依赖包提升到 repo 根目录管理。lerna 在安装依赖(lerna bootstrap)提供了--hoist 选项。

yarn workspace

搭建环境

在 monorepo 管理的项目中,各个库之间存在依赖,如 A 依赖于 B,因此我们通常需要将 B link 到 A 的 node_module 里,一旦仓库很多的话,手动的管理这些 link 操作负担很大,因此需要自动化的 link 操作,按照拓扑排序将各个依赖进行 link。

解决方式:通过使用 workspace,yarn install 会自动的帮忙解决安装和 link 问题。

yarn install # 等价于 lerna bootstrap --npm-client yarn --use-workspaces

package.json & lerna.json 如下:

//lerna.json
{
  "packages": ["packages/*"],
  "npmClient": "yarn",
  "useWorkspaces": true, // 使用 yarn workspaces
  "version": "0.0.0"
}

//package.json
{
  "name": "root",
  "private": true,
  "workspaces": [ //指定 workspace 路径
    "packages/*"
  ],
  "devDependencies": {
    "lerna": "^3.22.1"
  }
}

清理环境

在依赖乱掉或者工程混乱的情况下,清理依赖

    lerna clean # 清理所有packages的node_modules目录,不能删除根目录的node_modules
    yarn workspaces run clean # 执行所有package的clean操作(应是需自行写脚本)

安装/删除依赖

普通项目: 通过 yarn add 和 yarn remove 即可简单解决依赖库的安装和删除问题

monorepo:一般分为三种场景

  • 给某个 package 安装依赖:yarn workspace packageB add packageA 将 packageA 作为 packageB 的依赖进行安装
  • 给所有的 package 安装依赖: 使用 yarn workspaces add lodash 给所有的 package 安装依赖
  • 给 root 安装依赖:一般的公用的开发工具都是安装在 root 里,如 typescript,我们使用 yarn add -W -D typescript 来给 root 安装依赖

对应的三种场景删除依赖如下

  • yarn workspace packageB remove packageA
  • yarn workspaces remove lodash
  • yarn remove -W -D typescript

安装完依赖文件结构

安装完依赖文件结构

提交规范

在构建和发布之前还需要做一些关于代码提交的配置

commitizen && cz-lerna-changelog

commitizen 是用来格式化 git commit message 的工具,它提供了一种问询式的方式去获取所需的提交信息。 cz-lerna-changelog 是专门为 Lerna 项目量身定制的提交规范,在问询的过程,会有类似影响哪些 package 的选择。

我们使用 commitizen 和 cz-lerna-changelog 来规范提交,为后面自动生成日志作好准备。 因为这是整个工程的开发依赖,所以在根目录安装:

yarn add commitizen cz-lerna-changelog -D -W

安装完成后,在 package.json 中增加 config 字段,把 cz-lerna-changelog 配置给 commitizen。同时因为 commitizen 不是全局安全的,所以需要添加 scripts 脚本来执行 git-cz

{
  "name": "monorepo",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "commit": "git-cz"
  },
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-lerna-changelog"
    }
  },
  "devDependencies": {
    "commitizen": "^4.2.1",
    "cz-lerna-changelog": "^2.0.3",
    "lerna": "^3.22.1"
  }
}

之后在常规的开发中就可以使用 yarn run commit 来根据提示一步一步输入,来完成代码的提交。

commitlint && husky

上面我们使用了 commitizen 来规范提交,但很难靠开发自觉使用 yarn run commit 。万一忘记了,或者直接使用 git commit 提交怎么办?所以在提交时校验提交信息,如果不符合要求就不让提交,并提示。校验的工作由 commitlint 来完成,校验的时机则由 husky 来指定。husky 继承了 Git 下所有的钩子,在触发钩子的时候,husky 可以阻止不合法的 commit,push 等等。

安装 commitlint 以及要遵守的规范:

yarn add --W husky @commitlint/cli @commitlint/config-conventional

在工程根目录为 commitlint 增加配置文件 commitlint.config.js 为 commitlint 指定相应的规范

module.exports = {
  extends: ['@commitlint/config-conventional']
}

在 package.json 中增加如下配置

"husky": {
  "hooks": {
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
  }
}

"commit-msg"是 git 提交时校验提交信息的钩子,当触发时便会使用 commitlint 来校验。安装配置完成后,想通过 git commit 或者其它第三方工具提交时,只要提交信息不符合规范就无法提交。从而约束开发者使用 yarn run commit 来提交。

eslint && lint-staged

除了规范提交信息,代码本身肯定也少了靠规范来统一风格。

yarn add -D -W standard lint-staged

eslint 就是完整的一套 JavaScript 代码规范,自带 linter & 代码自动修正。自动格式化代码并修正,提前发现风格以及程序问题, 同时也支持 javascript 的代码规范校验,eslintrc.json:

module.exports = {
  env: {
    browser: true,
    es2020: true
  },
  extends: ["eslint:recommended""plugin:vue/essential"],
  parserOptions: {
    parser: "babel-eslint"
  },
  plugins: ["vue"],
  rules: {
    "prettier/prettier": [
      "off",
      {
        quotes0
      }
    ]
  }
}

lint-staged staged 是 Git 里的概念,表示暂存区,lint-staged 表示只检查并矫正暂存区中的文件。一来提高校验效率,二来可以为老的项目带去巨大的方便。

package.json 配置:

{
  "name""monorepo",
  "private"true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "c""git-cz"
  },
  "config": {
    "commitizen": {
      "path""./node_modules/cz-lerna-changelog"
    }
  },
  "husky": {
    "hooks": {
      "pre-commit""lint-staged"
    }
  },
  "lint-staged": {
    "*.(vue|js)": [
      //"eslint --fix",
      "prettier --write"
    ]
  },
  "devDependencies": {
    "commitizen""^4.2.1",
    "cz-lerna-changelog""^2.0.3",
    "lerna""^3.22.1",
    "lint-staged""^10.2.13",
    "standard""^14.3.4"
  }
}

安装完成后,在 package.json 增加 lint-staged 配置 "prettier --write",校验时机定在 pre-commit,在 husky 的配置中增加 pre-commit 的钩子用来执行 lint 校验。

使用 Lerna 构建和发布

项目构建

各个 package 之间存在相互依赖,如 packageB 只有在 packageA 构建完之后才能进行构建,否则就会出错,这实际上要求我们以一种拓扑排序的规则进行构建。

lerna 支持按照拓扑排序规则执行命令, --sort 参数可以控制以拓扑排序规则执行命令

lerna run --stream --sort build

可在根目录的 package.json 下配置

"scripts": {
  "build": "lerna run --stream --sort build"
},

运行 yarn build 对项目进行打包

版本升级及发包

项目测试完成后,就涉及到版本发布,版本发布一般涉及到如下一些步骤:

  • 条件验证: 如验证测试是否通过,是否存在未提交的代码,是否在主分支上进行版本发布操作
  • version_bump:发版的时候需要更新版本号,这时候如何更新版本号就是个问题,一般大家都会遵循 semVer 语义
  • 生成 changelog: 为了方便查看每个 package 每个版本解决了哪些功能,我们需要给每个 package 都生成一份 changelog 方便用户查看各个版本的功能变化。
  • 生成 git tag:为了方便后续回滚问题及问题排查通常需要给每个版本创建一个 git tag
  • git 发布版本:每次发版我们都需要单独生成一个 commit 记录来标记 milestone
  • 发布 npm 包:发布完 git 后我们还需要将更新的版本发布到 npm 上,以便外部用户使用

yarn 官方并不打算支持发布流程,只是想做好包管理工具,因此这部分还是需要通过 lerna 支持

lerna 提供了 publish 和 version 来支持版本的升级和发布, publish 的功能可以即包含 version 的工作,也可以单纯的只做发布操作。

lerna version 更新版本

  • 找出从上一个版本发布以来有过变更的 package
  • 提示开发者确定要发布的版本号
  • 将所有更新过的的 package 中的 package.json 的 version 字段更新
  • 将依赖更新过的 package 的 包中的依赖版本号更新
  • 更新 lerna.json 中的 version 字段
  • 提交上述修改,并打一个 tag
  • 推送到 git 仓库

lerna publish

版本自动更新可使用--conventional-commits 参数会自动的根据 conventional commit 规范和 git commit message 记录帮忙确定更新的版本号:

// lerna.json
{
  "packages": ["packages/*"],
  "npmClient""yarn",
  "useWorkspaces"true,
  "command": {
    "version": {
      "conventionalCommits"true # 生成changelog文件以及根据commit来进行版本变动
    }
  },
  "ignoreChanges": ["**/*.md"], # md文件更新,不触发版本变动
  "version""0.0.0"
}

包内的 package.json 还需 publishConfig 配置

"publishConfig": {
  "access": "public" // 如果该模块需要发布,对于scope模块,需要设置为public,否则需要权限验证
}

最后执行命令发布

lerna publish [from-git]

点个关注,互相监督学习进步!