对于维护过多个package(功能相近)的同窗来讲,都会遇到一个选择题,这些package是放在一个仓库里维护仍是放在多个仓库里单独维护。Multirepo 是比较传统的作法,即每个 package 都单独用一个仓库来进行管理。Monorepo 是管理项目代码的一个方式,指在一个项目仓库 (repo) 中管理多个模块/包 (package),不一样于常见的每一个模块建一个 repo。前端
目前有很多大型开源项目采用了这种方式,如 Babel
,React
, Meteor
, Ember
, Angular
,Jest
, Umijs
, Vue
, 还有 create-react-app
, react-router
等。几乎咱们熟知的仓库,都无一例外的采用了monorepo 的方式,能够看到这些项目的第一级目录的内容以脚手架为主,主要内容都在 packages目录中、分多个 package 进行管理。node
目录结构以下:react
├── packages
| ├── pkg1
| | ├── package.json
| ├── pkg2
| | ├── package.json
├── package.json
复制代码
monorepo 最主要的好处是统一的工做流和Code Sharing。好比我想看一个 pacakge 的代码、了解某段逻辑,不须要找它的 repo,直接就在当前 repo;当某个需求要修改多个 pacakge 时,不须要分别到各自的 repo 进行修改、测试、发版或者 npm link,直接在当前 repo 修改,统一测试、统一发版。只要搭建一套脚手架,就能管理(构建、测试、发布)多个 package。git
一图胜千言:typescript
虽然拆分子仓库、拆分子 npm 包是进行项目隔离的自然方案,但当仓库内容出现关联时,没有任何一种调试方式比源码放在一块儿更高效。shell
结合shop-service门户的实际场景和业务须要,自然的 MonoRepo ! 一个理想的开发环境能够抽象成这样:npm
“只关心业务代码,能够直接跨业务复用而不关心复用方式,调试时全部代码都在源码中。”json
在前端开发环境中,多 Git Repo,多 npm 则是这个理想的阻力,它们致使复用要关心版本号,调试须要 npm link。而这些是 MonoRepo 最大的优点。bootstrap
上图中提到的利用相关工具就是今天的主角 Lerna ! Lerna是业界知名度最高的 Monorepo 管理工具,功能完整。小程序
Lerna 是一个管理多个 npm 模块的工具,是 Babel 本身用来维护本身的 Monorepo 并开源出的一个项目。优化维护多包的工做流,解决多个包互相依赖,且发布须要手动维护多个包的问题。
推荐全局安装,由于会常常用到 lerna 命令
npm i -g lerna
复制代码
lerna init
复制代码
其中 package.json & lerna.json 以下:
// package.json
{
"name": "root",
"private": true, // 私有的,不会被发布,是管理整个项目,与要发布到npm的解耦
"devDependencies": {
"lerna": "^3.15.0"
}
}
// lerna.json
{
"packages": [
"packages/*"
],
"version": "0.0.0"
}
复制代码
增长两个 packages
lerna create @mo-demo/cli
lerna create @mo-demo/cli-shared-utils
复制代码
分别给相应的 package 增长依赖模块
lerna add chalk // 为全部 package 增长 chalk 模块
lerna add semver --scope @mo-demo/cli-shared-utils // 为 @mo-demo/cli-shared-utils 增长 semver 模块
lerna add @mo-demo/cli-shared-utils --scope @mo-demo/cli // 增长内部模块之间的依赖
复制代码
lerna publish
复制代码
上述1-5步已经包含了 Lerna 整个生命周期的过程了,但当咱们维护这个项目时,新拉下来仓库的代码后,须要为各个 package 安装依赖包。
咱们在第4步 lerna add 时也发现了,为某个 package 安装的包被放到了这个 package 目录下的 node_modules
目录下。这样对于多个 package 都依赖的包,会被多个 package 安装屡次,而且每一个 package 下都维护 node_modules
,也不清爽。因而咱们使用 --hoist 来把每一个 package 下的依赖包都提高到工程根目录,来下降安装以及管理的成本。
lerna bootstrap --hoist
复制代码
{
"packages": [
"packages/*"
],
"command": {
"bootstrap": {
"hoist": true
}
},
"version": "0.0.1-alpha.0"
}
复制代码
配置好后,对于以前依赖包已经被安装到各个 package 下的状况,咱们只须要清理一下安装的依赖便可:
lerna clean
复制代码
node_modules
中了。
lerna不负责构建,测试等任务,它提出了一种集中管理package的目录模式,提供了一套自动化管理程序,让开发者没必要再深耕到具体的组件里维护内容,在项目根目录就能够全局掌控,基于 npm scripts,使用者能够很好地完成组件构建,代码格式化等操做。接下来咱们就来看看,若是基于 Lerna,并结合其它工具来搭建 Monorepo 项目的最佳实践。
目前最多见的 monorepo 解决方案是 Lerna 和 yarn 的 workspaces 特性,基于lerna和yarn workspace的monorepo工做流。因为yarn和lerna在功能上有较多的重叠,咱们采用yarn官方推荐的作法,用yarn来处理依赖问题,用lerna来处理发布问题。能用yarn作的就用yarn作吧
普通项目:clone下来后经过yarn install,便可搭建完项目,有时须要配合postinstall hooks,来进行自动编译,或者其余设置。
monorepo: 各个库之间存在依赖,如A依赖于B,所以咱们一般须要将B link到A的node_module里,一旦仓库不少的话,手动的管理这些link操做负担很大,所以须要自动化的link操做,按照拓扑排序将各个依赖进行link
解决方式:经过使用workspace,yarn install会自动的帮忙解决安装和link问题
yarn install # 等价于 lerna bootstrap --npm-client yarn --use-workspaces
复制代码
在依赖乱掉或者工程混乱的状况下,清理依赖
普通项目: 直接删除node_modules以及编译后的产物。
monorepo: 不只须要删除root的node_modules的编译产物还须要删除各个package里的node_modules以及编译产物
解决方式:使用lerna clean来删除全部的node_modules,使用yarn workspaces run clean来执行全部package的清理工做
lerna clean # 清理全部的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
复制代码
普通项目:创建一个build的npm script,使用yarn build便可完成项目构建
monorepo:区别于普通项目之处在于各个package之间存在相互依赖,如packageB只有在packageA构建完以后才能进行构建,不然就会出错,这实际上要求咱们以一种拓扑排序的规则进行构建。
咱们能够本身构建拓扑排序规则,很不幸的是yarn的workspace暂时并未支持按照拓扑排序规则执行命令,虽然该 rfc已经被accepted,可是还没有实现, 幸运的是lerna支持按照拓扑排序规则执行命令, --sort参数能够控制以拓扑排序规则执行命令
lerna run --stream --sort build
复制代码
项目测试完成后,就涉及到版本发布,版本发布通常涉及到以下一些步骤
条件验证: 如验证测试是否经过,是否存在未提交的代码,是否在主分支上进行版本发布操做
version_bump:发版的时候须要更新版本号,这时候如何更新版本号就是个问题,通常你们都会遵循 semVer语义,
生成changelog: 为了方便查看每一个package每一个版本解决了哪些功能,咱们须要给每一个package都生成一份changelog方便用户查看各个版本的功能变化。
生成git tag:为了方便后续回滚问题及问题排查一般须要给每一个版本建立一个git tag
git 发布版本:每次发版咱们都须要单独生成一个commit记录来标记milestone
发布npm包:发布完git后咱们还须要将更新的版本发布到npm上,以便外部用户使用
咱们发现手动的执行这些操做是很麻烦的且及其容易出错,幸运的是lerna能够帮助咱们解决这些问题
yarn官方并不打算支持发布流程,只是想作好包管理工具,所以这部分仍是须要经过lerna支持
lerna提供了publish和version来支持版本的升级和发布, publish的功能能够即包含version的工做,也能够单纯的只作发布操做。
commitizen
是用来格式化 git commit message 的工具,它提供了一种问询式的方式去获取所需的提交信息。
cz-lerna-changelog
是专门为 Lerna 项目量身定制的提交规范,在问询的过程,会有相似影响哪些 package 的选择。以下:
commitizen
和
cz-lerna-changelog
来规范提交,为后面自动生成日志做好准备。
由于这是整个工程的开发依赖,因此在根目录安装:
yarn add -D commitizen
yarn add -D cz-lerna-changelog
复制代码
安装完成后,在 package.json
中增长 config 字段,把 cz-lerna-changelog
配置给 commitizen
。同时由于commitizen
不是全局安全的,因此须要添加 scripts 脚原本执行 git-cz
{
"name": "root",
"private": true,
"scripts": {
"commit": "git-cz"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-lerna-changelog"
}
},
"devDependencies": {
"commitizen": "^3.1.1",
"cz-lerna-changelog": "^2.0.2",
"lerna": "^3.15.0"
}
}
复制代码
以后在常规的开发中就可使用 yarn run commit
来根据提示一步一步输入,来完成代码的提交。
上面咱们使用了 commitizen 来规范提交,但这个要靠开发自觉使用yarn run commit
。万一忘记了,或者直接使用 git commit 提交怎么办?答案就是在提交时对提交信息进行校验,若是不符合要求就不让提交,并提示。校验的工做由 commitlint 来完成,校验的时机则由 husky 来指定。husky 继承了 Git 下全部的钩子,在触发钩子的时候,husky 能够阻止不合法的 commit,push 等等。
安装 commitlint 以及要遵照的规范
yarn add -D @commitlint/cli @commitlint/config-conventional
复制代码
在工程根目录为 commitlint 增长配置文件 commitlint.config.js
为commitlint
指定相应的规范
module.exports = {
extends: ['@commitlint/config-conventional']
}
复制代码
安装 husky
yarn add -D husky
复制代码
在 package.json
中增长以下配置
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
复制代码
"commit-msg"是git提交时校验提交信息的钩子,当触发时便会使用 commitlit 来校验。安装配置完成后,想经过 git commit 或者其它第三方工具提交时,只要提交信息不符合规范就没法提交。从而约束开发者使用 yarn run commit 来提交。
除了规范提交信息,代码自己确定也少了靠规范来统一风格。
安装
yarn add -D standard lint-staged
复制代码
eslint就是完整的一套 JavaScript(typescript) 代码规范,自带 linter & 代码自动修正。自动格式化代码并修正,提早发现风格以及程序问题, 同时也支持typescript的代码规范校验,eslintrc.json
配置:
{
"extends": [
"yayajing",
"plugin:@typescript-eslint/recommended"
],
"parser": "typescript-eslint-parser",
"plugins": ["@typescript-eslint"],
"rules": {
"eqeqeq":"off",
"@typescript-eslint/explicit-function-return-type": "off",
"no-template-curly-in-string": "off"
}
}
复制代码
lint-staged staged
是 Git 里的概念,表示暂存区,lint-staged
表示只检查并矫正暂存区中的文件。一来提升校验效率,二来能够为老的项目带去巨大的方便。
package.json配置
// package.json
{
"name": "root",
"private": true,
"scripts": {
"c": "git-cz"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-lerna-changelog"
}
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.ts": [
"eslint --fix",
"git add"
]
},
"devDependencies": {
"@commitlint/cli": "^8.1.0",
"@commitlint/config-conventional": "^8.1.0",
"commitizen": "^3.1.1",
"cz-lerna-changelog": "^2.0.2",
"husky": "^3.0.0",
"lerna": "^3.15.0",
"lint-staged": "^9.2.0"
}
}
复制代码
安装完成后,在 package.json
增长 lint-staged 配置,如上所示表示对暂存区中的 js 文件执行 eslint --fix
校验并自动修复。那何时去校验呢,就又用到了上面安装的 husky ,husky的配置中增长pre-commit
的钩子用来执行 lint-staged 的校验操做。
此时提交 ts 文件时,便会自动修正并校验错误。即保证了代码风格统一,又能提升代码质量。
有了以前的规范提交,自动生成日志便水到渠成了。再详细看下 lerna publish
时作了哪些事情:
找出从上一个版本发布以来有过变动的 package
提示开发者肯定要发布的版本号
将全部更新过的的 package 中的package.json的version字段更新
将依赖更新过的 package 的 包中的依赖版本号更新
更新 lerna.json 中的 version 字段
提交上述修改,并打一个 tag
推送到 git 仓库
CHANGELOG
很明显是和 version 一一对应的,因此须要在 lerna version 中想办法,查看 lerna version 命令的详细说明后,会看到一个配置参数 --conventional-commits
。没错,只要咱们按规范提交后,在 lerna version 的过程当中会便会自动生成当前这个版本的 CHANGELOG。为了方便,不用每次输入参数,能够配置在 lerna.json
中,以下:
{
"packages": [
"packages/*"
],
"command": {
"bootstrap": {
"hoist": true
},
"version": {
"conventionalCommits": true
}
},
"ignoreChanges": [
"**/*.md"
],
"version": "0.0.1-alpha.1"
}
复制代码
lerna version
会检测从上一个版本发布以来的变更,但有一些文件的提交,咱们不但愿触发版本的变更,譬如 .md 文件的修改,并无实际引发 package 逻辑的变化,不该该触发版本的变动。能够经过 ignoreChanges
配置排除。如上。
lerna version
不多直接使用,由于它包含在
lerna publish
中了,直接使用
lerna publish
就行了。
monorepo项目:测试有两种方式
使用统一的jest测试配置这样方便全局的跑jest便可,好处是能够方便统计全部代码的测试覆盖率,坏处是若是package比较异构(如小程序,前端,node 服务端等),统一的测试配置不太好编写
每一个package单独支持test命令,使用yarn workspace run test,坏处是很差统一收集全部代码的测试覆盖率
若是采用jest编写测试用例,支持typescript的话,须要初始化配置jest.config.js:
module.exports = {
preset: 'ts-jest',
moduleFileExtensions: ['ts'],
testEnvironment: 'node'
}
复制代码
到这里,基本上已经构建了基于lerna
和yarn workspace
的monorepo项目的最佳实践了,该有的功能都有:
完善的工做流
typescript支持
风格统一的编码
完整的单元测试
一键式的发布机制
完美的更新日志
……
固然,构建一套完善的仓库管理机制,可能它的收益不是一些量化的指标能够衡量出来的,也没有直接的价值输出,但它能在平常的工做中极大的提升工做效率,解放生产力,节省大量的人力成本。