monorepo项目的一次尝试 - Sara2009/start-monorepo-with-lerna-yarnworkspace GitHub Wiki

Multirepo vs Monorepo

对于一个庞大的大型项目,我们需要进行模块划分,拆分代码,这样才可以减少项目的复杂度,降低维护成本,采用模块依赖的方式,可以实现解耦,保持各模块的独立开发与维护。在管理这种由多个模块合成的大型项目时,有两种解决方案Multirepo和Monorepo,它们是两种不同的源码管理理念。

Multirepo

Multirepo的核心是一个模块一个仓库,每个模块都是独立开发与维护的,主工程仅提供必要的运行环境,业务模块全部依靠import,各业务模块分别属于独立的仓库,管控各自的版本发布。Multirepo是现在大多数项目采用的代码管理模式,这也和微服务的理念一致。

  • 每个模块都拥有完整自主的开发和发布管理,模块之间很少甚至没有耦合;
  • 模块之间采用import的方式进行依赖管理,当一个模块在本地修改的时候,其他模块需要采用npm link的方式进行开发调试,这使得当一个模块修改的时候,引发的依赖更新管理很困难。
  • 版本发布很分散,当有一个模块进行发布更新时,需要人工自主评估其他模块是否需要跟进发布;
  • 每个模块都可以自主设置Access Control,提高代码安全。

Monorepo

Monorepo则是将项目相关的所有模块都放在一个仓库中进行开发和维护。

  • 在一个仓库中可以采用统一构建、CI、测试和发布流程,但是这也需要开发更复杂的构建工具来支持;
  • 跨模块的操作和修改很容易,方便管理版本和依赖管理;
  • 代码仓库的体积都比较大;
  • 方便统一处理issue和生成ChangeLog;
  • 出于代码安全层面考虑,Monorepo不能很好的控制各个模块的代码权限,无法做权限隔离,维护一个模块可以看到所有模块的代码;

如何选择?

当一个项目中各个模块之间存在紧密联系,但同时确实存在明显的业务边界,则比较适合采用monorepo的代码管理模式。而对完全不同的多个业务,每个模块很独立,功能不重合的,最好还是使用Multirepi的方式。

Lerna

Lerna是业界知名度最高的Monorepo管理工具,根据其规范,我们制定了项目的目录结构(仅示意),每个模块的源码放在src目录下,构建结果在es/dist/lib目录,每个模块自有一个package.json,只记录着该模块自身的依赖,在项目根目录下统一维护的package.json则记录整个项目所有模块的构建依赖,比如统一的lint、test和打包工具。

.
|-- packages
|    |-- module-A
|      |-- src
|      |-- es
|      `-- package.json
|    |-- module-B
|      |-- src
|      |-- dist
|      `-- package.json
|-- config
|-- node_modules
'-- package.json

对于每个模块的构建产物,我们进行了如下的规范约束:

  • src:源代码
  • lib:commonjs规范的构建产物
  • es:ESM规范的构建产物
  • dist:umd规范的构建产物

那么相应的,需要在每个模块的package.json中进行如下声明:

{
  "main": "lib/index.js",
  "module": "es/index.js",
  "browser": "dist/index.js"
}

搭建一个monorepo项目

  1. 全局安装Lerna
npm install -g lerna
  1. 初始化项目
lerna init

运行完毕则会生成packages文件夹、lerna.json和package.json文件,然后添加.gitignore、README.md和scripts等相关文件。

.
|-- packages
|-- scripts
|-- .gitignore
|-- README.md
|-- lerna.json
'-- package.json

在.gitignore中添加:

node_modules
es
lib
dist

项目根目录下生成的package.json中设置了private属性,说明是私有仓库,不允许发布。

{
  "name": "root",
  "private": true,
  "devDependencies": {
    "lerna": "^3.20.2"
  }
}

如果需要使用tnpm,则配置lerna.json:

{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0",
  "npmClient": "tnpm"
}
  1. 新建package

在项目根目录下执行lerna create来创建package,也可以手动创建。

lerna create <name>

执行完毕后会生成以下文件,然后可以手动添加自己需要的文件,比如src,以及根据情况修改package.json。

.
|-- packages
|     |-- module1
|       |-- __test__
|       |-- src
|       |-- lib
|         |-- module1.js
|       |-- package.json
|       |-- README.md
|     |-- module2
|-- scripts
|-- .gitignore
|-- README.md
|-- lerna.json
'-- package.json
  1. 添加依赖

虽然是同一个repo,但是每个package需要保持其独立性,package之间的依赖依旧要使用包名的方式,在各个package下的package.json中添加dependencies,在项目根目录下的package.json中添加devDependencies。例如上面module2需要依赖module1,那么直接在module2的package.json中添加依赖module1即可,接下来执行lerna bootstrap,这会在项目根目录和packages下都安装各自的依赖,创建node_modules,packages下的相互依赖则是通过符号链接symlink来实现的,对开发调试很友好。

lerna bootstrap会主动分析packages并自创根据package.json中的依赖声明来创建需要的npm link,将模块链接到对应的运行项目中去方便本地开发与调试,

lerna bootstrap会在每个package下安装添加的依赖包,可以采用--hoist模式来减少重复安装external依赖,lerna bootstrap --hoist [glob]会将匹配glob的依赖包安装在项目根目录下,这样可以减少在各个package下重复安装。每个package下的node_modules/.bin/ 目录会链接到根目录下,可以很方便执行npm scripts项目结构

lerna也提供add命令来添加本地或者远程依赖包,可以参考官方的Usage

lerna add <package>[@version] [--dev] [--exact] [--peer]
  1. 清理环境

    $ lerna clean # 清理所有的node_modules
    $ lerna run clean # 执行所有package的clean操作,删除编译产物

lerna run <command>会执行每个package下的对应命令。

  1. 项目测试

    可以有两种测试方案:

    • 在项目根目录下使用统一的测试配置,比如选择Jest,可以方便统计所有代码的测试覆盖率,但是如果每个package的运行环境差距很大,比如小程序、browser、node等等,这会使得测试配置很复杂。
    • 每个package单独支持test命令,使用lerna run test --parallel,那么就无法统一收集所有代码的测试覆盖率。

优化git commit

我们希望所有的git提交能够遵循conventionalcommits规范,能够有类似下面这些开源项目中友好的commit message。 angular commits 如果仅靠人工约束,这很难保证绝对不会出现问题,因此尝试使用了commitizen来进行规范。首先安装以下依赖:

npm install commitizen cz-conventional-changelog -D

然后配置使用git-cz来替代git commit,接下来在提交时候运行npm run commit即可:

"scripts": {
  "commit": "git-cz"
},

结合husky + lint-staged来强制检测message是否符合要求:

"husky": {
  "hooks": {
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
    "pre-commit": "lint-staged"
  }
},
  "lint-staged": {
    "packages/src/**/*.(js|jsx)": [
      "git add"
    ]
}

Lerna天然支持conventionalcommits规范,每次修改完毕后执行lerna version --conventional-commits,让lerna来更新各个package的版本,--conventional-commits则表示自动生成CHANGELOG。Lerna的版本管理有两种模式,我们采用了默认的模式:

  • Fixed/Locked模式(默认):在lerna.json中维护统一的一个版本号,如果一个module有改动需要发布,则会更新到这个最新的version,每次更新版本的时候,每个package都是以这个统一的版本号为基准进行变化。
  • Independent模式:每个package维护自己的版本号。

发布publish

在每个package下都应该有对应的构建npm script,例如在每个package下的package.json中定义:

"scripts": {
  "watch": "...",
  "build": "...",
  "lint": "...",
  "test": "..."
}

在项目根目录下的package.json定义发布命令的npm script:

"scripts": {
  "version": "lerna version --conventional-commits",
  "prepublish": "lerna run build --stream",
  "publish": "lerna publish from-git"
}

配置from-git主要是用于结合CI平台进行publish,当需要进行发布时,首先在本地运行lerna version --conventional-commits,执行完毕后lerna会自动进行一个git tag的提交,然后就可以配置CI平台对git tag执行publish的操作(结合CI平台的发布,还在研究中)。

yarn workspaces

Yarn是由Facebook、Google、Exponent和Tilde联合推出了一个新的JavaScript包管理工具,用于解决npm存在的一些问题,速度更快,可以使用Yarn来替代npm。npm(1)和yarn(2)拥有共同的一些功能和特性(3),根据官方的说明yarn workspaces(4)只会提供一些workspace的基础命令,一些高级命令还是要使用lerna来完成,yarn workspaces应该结合lerna一起使用,无法替代lerna。 npm、yarn和yarn workspace的对比

项目配置

修改项目根目录下的package.json,设置workspaces:

{
  "name": "root",
  "private": true,
  "devDependencies": {
    "lerna": "^3.20.2"
  },
  "workspaces": [
    "packages/*"
  ]
}

修改lerna.json,使用yarn命令进行构建:

{
  "version": "0.0.0",
  "npmClient": "yarn",
  "useWorkspaces": true
}

命令对比

依旧可以使用lerna的各种command,也可以使用下面这几个基本的yarn命令:

  • yarn install等同于lerna bootstrap --hoist,区别在于yarn install会将所有的依赖都安装在项目根目录下,包括本地依赖,比如上面module2依赖module1,那么module1不会被symlink在module2下的node_modules下,而是都放在项目根目录下。 lerna+yarn workspace项目结构

  • yarn workspace <workspace_name> add <package> 用于添加依赖,当添加本地依赖的时候有可能会遇到这个错误Couldn't find package,这是yarn的一个issue,解决方案有这样两个:

    • 指定本地local依赖的版本号;
    • 使用yarn install来安装依赖。
  • yarn workspaces run <command>会执行每个package下的命令,和lerna run类似,但是yarn workspaces run不支持按照依赖的拓扑排序规则执行命令,这一提案已经被Accepted但还未实现,这可能会影响构建结果,比如module2依赖module1,必须在module1构建完成后才能构建,这个时候就只能用lerna run --stream buildlerna run还具有更多高级的设置,因此建议使用lerna run

使用规范

从上面的分析可以得出以下的使用规范,简而言之,就是使用yarn workspaces来管理整个monorepo项目的依赖,lerna则用于构建和发布。

  • Yarn用于处理项目的依赖管理;
  • Lerna用于运行项目的lint、测试、构建和发布命令等,lerna run的功能要比yarn workspaces run更强大;
  • 在packages中一个package一个文件夹,所有的package的目录结构一致;
  • 每个package中只定义其运行时依赖 dependencies
  • 所有的构建开发依赖 devDependencies 在项目根目录下共享;
  • 所有的package共享版本号,使用lerna version 进行统一的维护和管理。

开发网配置

yarn config set proxy "http://127.0.0.1:12639"
yarn config set registry "http://r.tnpm.oa.com/"

参考文献

monorepo 项目改造反思

Multirepo vs Monorepo

Lerna

Why Lerna and Yarn Workspaces is a Perfect Match for Building Mono-Repos – A Close Look at Features and Performance

Monorepo setup with Lerna and Yarn workspaces

基于lerna和yarn workspace的monorepo工作流

⚠️ **GitHub.com Fallback** ⚠️