前言
在传统项目开发模式中每个项目都对应单独的一个代码仓库。这种模式称之为方式MultiRepo。
MultiRepo缺点
在开发过程中,经常遇到一些组件或者功能复用的情况。一般情况我们是将公共的逻辑代码抽取出来,然后发布到npm,这样我们所有的项目通过安装依赖包就能实现复用。
如果依赖包里面某个功能或者工具函数出错了或者我们需要更新,我们需要做一下几个步骤:
- 去修改源代码
- 重新发布新包
- 通知所有项目安装新版本依赖包。
只是修改或者更新依赖包,就需要这么复杂的步骤,在开发过程中,修改代码和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 -D -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",
{
quotes: 0
}
]
}
}
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]
点个关注,互相监督学习进步!