前言
在日常开发往往需要把公共代码封装复用,在项目比较少的时候可以通过复制代码到不同项目共享,但面对多个项目时,这种复制行为会导致维护问题。
为了解决这个问题,常见的办法是封装一个npm包发布到仓库或者通过git安装,但随着拆分颗粒越来越小,如果还是每个包独立一个仓库,就会产生新的维护问题,例如一个包被多个包依赖,当更新一个包时,需要通知其他包逐个更新,这会产生大量工作。
社区最近流行一种叫monorepo的方式管理仓库,就是把所有代码都存储到一个仓库,例如Vue3、babel、Taro都用这种方式,这种方式到底如何组织代码,又要用什么工具。下文将通过对比monorepo和multirepo的优缺点、lerna基本使用方法让大家初步了解单仓库设计。
monorepo和multirepo
多仓库管理Multirepo
Multirepo(multiple repository),即传统的多仓库管理方式,每一个 package 都单独用一个仓库来管理,结构如下图
优点:
- 各模块独立管理,可自行选择构建工具,编码规范、单元测试等配套设施。
缺点:
- 仓库分散不方便管理,当需要修改不同模块代码,需切换不同仓库。
- 版本更新繁琐,当一个模块更新版本后,其依赖模块都需要逐一更新。
- 构建脚本和配置无法共享
单仓库管理Monorepo
Monorepo 的全称是 monolithic repository,即单体式仓库,把所有相关的 package 都放在一个仓库内进行管理,目前 React、Babel、Umi、Taro、 Vue3 都在使用,结构如下图。
优点:
- 统一构建工具和构建脚本、复用lint规则之类
- 代码都在一个仓库,方便跨应用代码管理
- 方便版本管理
缺点:
- 单个仓库管理所有代码,仓库过大,无法做权限管理
- 引入新技术,增加开发门槛
vue的单仓库目录结构
如下图vue3源码就是基于monorepo的思想组织代码,他把每个模块独立成一个子包,每个子包都是独立且具备完整功能的模块,我们可以安装单独一个子包,也可以整个仓库安装。
monorepo解决方案
目前monorepo管理并没有一个标准,不同仓库有不同的实现方式,例如vue3是通过自定义脚本管理仓库,但是如果你不想折腾也可以用社区开源方案,目前比较知名的方案有lerna和rush.js。
lerna
成熟的monorepo管理工具,经过多年的迭代和大型开源项目的实践,有较高的稳定性和功能完整性,他提供例如初始化,版本依赖更新,版本发布,changelog生成等功能。
rush.js
微软开源的monorepo管理方案,它默认基于单仓库管理代码,适合大型项目使用,他解决了依赖版本混乱,幽灵依赖等问题。
Lerna管理仓库
下面演示使用lerna命令创建monorepo仓库,包之间创建关联和版本更新
初始化仓库
lerna init
执行命令会创建一个monorepo仓库,命令执行内容如下:
- 添加
lerna到package.josn的devDependency - 创建
lerna.json并初始化version属性
lerna提供两种版本管理方式,第一种为所有子项目使用相同版本号,第二种是子项目维护各自版本。lerna默认使用第一种,如果要子项目各自维护版本,可执行lerna init --independent初始化。
添加子项目
现在packages目录是空的,可通过lerna create <name>创建项目
# 创建两个子项目
lerna craete tiangong-utils
lerna create tiangong-hooks
创建后目录如下图,其中子项目和普通项目没什么差别
添加依赖
把远程或本地npm包安装到所有子项目,可使用lerna add <包名>,他和npm install <包名>最大的区别是会对所有子项目添加依赖。
# 给所有子项目安装lodash
lerna add lodash
执行后,tiangong-utils和tiangong-hooks都增加lodash依赖,如果你想只对单个项目增加依赖,可增加scope参数,lerna add <包名> --scope=本地项目名
安装本地依赖
假如tiangong-hooks要依赖tiangong-utils,可执行lerna add tiangong-utils --scope=tiangong-hooks
由于已经作为依赖安装,hooks项目可直接通过包名引入
import utils from 'tiangong-utils'
使用workspace优化node_module
现在每个子项目的node_module都在自己的目录下,如下图
我们可以通过yarn或者npm7的workspace把node_module提取到最外层
方法一:在执行bootstrap命令加入--use-workspaces参数
lerna bootstrap --npm-client=yarn --use-workspaces
方法二:更好的办法是修改配置文件
lerna.json增加下面内容
{
"npmClient": "yarn",
"useWorkspaces": true
}
package.json增加workspaces数组
{
"workspaces": ["packages/*"]
}
发布版本
如果我们发布的新版本对多个项目有修改,手动发布将会很麻烦,例如需要对逐个项目打包、生成changelog、打tag、推送到npm、单元测试等。
lerna提供了一个命令可以帮助我们自动完成以上操作,只要执行lerna publish lerna会自动检查仓库下变更的项目,执行相关的发布流程,你只要根据提示对话输入即可。
xboss-lerna仓库
xboss-lerna是我基于monorepo思想组织的仓库,用于管理vue-hooks、utils库、组件库等资源,仓库地址:github.com/zhengguoron…
其中组件的构建脚本没有使用rollup和webpack而是使用vant-cli,我之前的两篇文章《手把手系列:打造企业级vue-hooks》 《手把手系列:打造企业级vue-components》 对构建方面做了一些调研,发现hooks、utils类型的纯JS库构建并不复杂,只要把ts转换为js、生成类型定义、输出esm和cjs即可。
而组件的构建,只是在基于纯JS库的构建流程上加入对.vue或.tsx的解析,要实现这些特性如果使用rollup打包,需要安装不少第三方插件,而这些插件有不同的兼容问题,或者对vue3新特性维护落后,最终导致打包困难。
后来我发现vant-cli提供了构建脚本、文档生成、单元测试等特性,我通过阅读源码,认为它的结构是比较清晰的,这对于后续的定制化开发较友好,所以选择vant-cli作为构建脚本。
这是xboss-lerna的目录结构
各子项目功能介绍
xboss-cli: 提供构建、测试、文档生成、版本发布等功能
xboss-components: 基于vue3的组件库
xboss-eslint-config: eslint统一配置,所有项目基于该配置做扩展
xboss-http: 基于axios封装的网络请求模块
xboss-stylelint-config: stylelint统一配置,所有项目基于该配置扩展
xboss-use: 基于compostion-api的hooks库
xboss-utils: 纯js工具库
如果你想深入了解,可到 github下载源码
总结
本文通过对比monorepo和multirepo两种仓库管理模式和演示lerna常用命令让大家初步了解monorepo的使用,最后一节提供的xboss-lerna仓库演示了monorepo在组件库、JS库和脚手架的组合方法。
monorepo只是一种管理代码的思想,并没有绝对的优势,需根据自身业务特性选择是否使用,否则只会增加维护成本。
二更
现在生产项目已经由lerna更换为pnpm实现monorepo,目前没什么问题,而且我感觉pnpm管理方式更简洁,建议还没入坑lerna的同学用pnpm管理。