基于lerna的monorepo仓库管理

2,611 阅读7分钟

前言

在日常开发往往需要把公共代码封装复用,在项目比较少的时候可以通过复制代码到不同项目共享,但面对多个项目时,这种复制行为会导致维护问题。

为了解决这个问题,常见的办法是封装一个npm包发布到仓库或者通过git安装,但随着拆分颗粒越来越小,如果还是每个包独立一个仓库,就会产生新的维护问题,例如一个包被多个包依赖,当更新一个包时,需要通知其他包逐个更新,这会产生大量工作。

社区最近流行一种叫monorepo的方式管理仓库,就是把所有代码都存储到一个仓库,例如Vue3、babel、Taro都用这种方式,这种方式到底如何组织代码,又要用什么工具。下文将通过对比monorepo和multirepo的优缺点、lerna基本使用方法让大家初步了解单仓库设计。

monorepo和multirepo

多仓库管理Multirepo

Multirepo(multiple repository),即传统的多仓库管理方式,每一个 package 都单独用一个仓库来管理,结构如下图

1dbdcd87-c3e5-4e6f-920c-67b62408e4d7.png

优点:

  1. 各模块独立管理,可自行选择构建工具,编码规范、单元测试等配套设施。

缺点:

  1. 仓库分散不方便管理,当需要修改不同模块代码,需切换不同仓库。
  2. 版本更新繁琐,当一个模块更新版本后,其依赖模块都需要逐一更新。
  3. 构建脚本和配置无法共享

单仓库管理Monorepo

Monorepo 的全称是 monolithic repository,即单体式仓库,把所有相关的 package 都放在一个仓库内进行管理,目前 React、Babel、Umi、Taro、 Vue3 都在使用,结构如下图。

image.png

优点:

  1. 统一构建工具和构建脚本、复用lint规则之类
  2. 代码都在一个仓库,方便跨应用代码管理
  3. 方便版本管理

缺点:

  1. 单个仓库管理所有代码,仓库过大,无法做权限管理
  2. 引入新技术,增加开发门槛

vue的单仓库目录结构

如下图vue3源码就是基于monorepo的思想组织代码,他把每个模块独立成一个子包,每个子包都是独立且具备完整功能的模块,我们可以安装单独一个子包,也可以整个仓库安装。

image.png

monorepo解决方案

目前monorepo管理并没有一个标准,不同仓库有不同的实现方式,例如vue3是通过自定义脚本管理仓库,但是如果你不想折腾也可以用社区开源方案,目前比较知名的方案有lerna和rush.js。

lerna

成熟的monorepo管理工具,经过多年的迭代和大型开源项目的实践,有较高的稳定性和功能完整性,他提供例如初始化,版本依赖更新,版本发布,changelog生成等功能。

rush.js

微软开源的monorepo管理方案,它默认基于单仓库管理代码,适合大型项目使用,他解决了依赖版本混乱,幽灵依赖等问题。

Lerna管理仓库

下面演示使用lerna命令创建monorepo仓库,包之间创建关联和版本更新

初始化仓库

lerna init

执行命令会创建一个monorepo仓库,命令执行内容如下:

  1. 添加lernapackage.josndevDependency
  2. 创建lerna.json并初始化version属性

image.png

lerna提供两种版本管理方式,第一种为所有子项目使用相同版本号,第二种是子项目维护各自版本。lerna默认使用第一种,如果要子项目各自维护版本,可执行lerna init --independent初始化。

添加子项目

现在packages目录是空的,可通过lerna create <name>创建项目

# 创建两个子项目
lerna craete tiangong-utils
lerna create tiangong-hooks

创建后目录如下图,其中子项目和普通项目没什么差别

image.png

添加依赖

把远程或本地npm包安装到所有子项目,可使用lerna add <包名>,他和npm install <包名>最大的区别是会对所有子项目添加依赖。

# 给所有子项目安装lodash
lerna add lodash

执行后,tiangong-utilstiangong-hooks都增加lodash依赖,如果你想只对单个项目增加依赖,可增加scope参数,lerna add <包名> --scope=本地项目名

image.png

安装本地依赖

假如tiangong-hooks要依赖tiangong-utils,可执行lerna add tiangong-utils --scope=tiangong-hooks

image.png

由于已经作为依赖安装,hooks项目可直接通过包名引入

import utils from 'tiangong-utils'

使用workspace优化node_module

现在每个子项目的node_module都在自己的目录下,如下图

我们可以通过yarn或者npm7的workspace把node_module提取到最外层

image.png

方法一:在执行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的目录结构

image.png

各子项目功能介绍

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管理。