从lerna谈monorepo的应用场景
目前一个前端项目最通用的开发流程都是从npm init
或者cli
开始,完成于npm publish
,未来的迭代都以这样一个包为原子单位。但当某些包之间的关联越来越紧密并耦合时,粒度或许就有更多的划分思路了。这篇文章就从lerna
谈谈monorepo
这个思想,供合适的场景考虑。
基本概念
我们平时使用的组织管理代码的形式叫做multirepo
,就是将模块划分到多个仓库,对应到前端的具体表现就是npm
包管理。这样的好处很多,首先就是可以很自然的以仓库划分开发团队,让不同团队采用最适合自己的技术栈,并基于npm
提供的包管理能力实现一套闭环工作流。这样其实是很好的,要不然npm
也不会如此流行,但有不少场景下或许会有更合适的方案,毕竟单纯的以包为单位来隔离还是不够灵活。比如多个包紧密依赖并频繁变动时,就该聚合并由一个团队维护了。
应运而生的,就是monorepo
的管理形式了,简单来说就是聚合所有的npm
包到一个仓库中进行管理。把所有鸡蛋放到一个篮子里之后,取用当然是更加方便,但对篮子的要求当然要更高并且维护也要更小心。对应的就是需要更精密的构建流程,更严格的版本控制。
两种方案没有优劣,相比于方案本身,更重要的能是根据合适的场景做出选择。所以在谈到具体案例之前,先了解两者的具体优劣势是很重要的。
优劣分析
在本身就是单包项目的情况下自不必说,主要是分析存在多包有明显依赖的场景。
首先是multirepo
的问题:
- 开发环境下调试困难
首先就是多仓库工作台的问题,需要打开多个页面来回切换。虽然有些编辑器提供了多仓库的管理模式,但对应环境配置也是很麻烦的事且不能保证团队所有成员一致。再就是如何实现一个仓库中的代码变动,另外一个仓库中能立刻响应。现在虽然有npm link
的解决方案,但这样又会遇到环境相关的上下文问题。
- 分支以及版本管理复杂
很明显的仓库变多,相互依赖增加的情况下,各自每更新一个不能保证全部同步的版本就要新切出一个分支,分支复杂度以及版本管理就会成为灾难。
- 代码分散带来的维护成本增加
相互依赖后由于通常是一起使用,很容易出现产生bug后不知道是具体到哪个仓库的问题,进而导致issue
分布不合理。
- 三方包重复安装带来的额外空间占用
多仓相互依赖的情况下,大部分用到的三方包版本应该是一致的并可以共用的,重复安装就有额外空间占用的问题。
采用monorepo
方案的话,以上问题都能得到很好的解决,但同时也会引入新的问题:
- 更复杂的部署流程
因为单一仓库间有了依赖,而依赖的模块可能只是本地的关联,并不一定发布到了npm
上,所以在部署服务器上install
时就会出现问题,需要在install
的前后节点做更多的处理这类情况的操作。
- 配置文件的上下游兼容
monorepo
的代码结构下,相当于在单仓到CI中新增了一个仓库节点,所以很多CI能处理的配置检查需要做多一步的考量。比如eslint
,husky
这些配置文件除了配置在仓库最上层之外,还要合理的对接到各个子仓库并支持这些子仓库单独的配置。
- 代码安全
集中化之后也就意味着所有开发人员都能看到代码并能随意改动其他团队仓库的代码,不能控制代码权限就必然对开发人员有更高的要求。
- 分支管理复杂
同样的,monorepo
依然有分支复杂的问题。但相比于multirepo
的分支依赖复杂,这里更倾向于分支内容复杂。毕竟需要单分支处理多仓库,信息量自然增加,需要一套更完善的分支管理方案规约各仓库间的整套开发流程。
lerna
因为monorepo
本身是一种模式,那么自然就可以抽离出一套通用化方案应对大多数情况。lerna就是这样一个工具,给我们提供了monorepo
下包管理的能力以及常用功能,比如包之间的本地相互引用,统一发布以及版本号管理等等。在这里我不打算介绍它的API,官方文档已经有很详细的介绍,不过目录结构还是有必要说明一下。
lerna-repo/
package.json
lerna.json
packages/
最上层会用lerna.json
配置一些相关参数,比如各个包是独立版本还是统一版本等等。package.json
就是下层packages
项目中通用的包。在这一层除了这些必要文件之外,还可以根据情况增加一些通用配置文件,比如editconfig
,eslint
相关文件。如果需要和子包交互,lerna
提供了相关API直接声明在package.json
的scripts
字段使用即可。
概括的说,lerna
做的事就是一层胶水,将单仓管理和多仓对应的单仓连接起来。问题自然是有的,就是胶水本身的能力在某些场景下不够用,需要找到其他的方案代替。目前来说需要注意如下几点:
- 类似于
pre-commit
的钩子命令需要使用lerna run --concurrency 1 --stream
参数流式调用,因为很多命令没有考虑多仓同时执行的情况,并行调用会出问题。 - 子包如果是以
@
开头且需要发布到公共仓库,必须先用npm publish
加公共权限发布参数,因为lerna publish
并不支持权限参数 - 本地依赖的包如果没有发布到
npm
,在部署服务器上就会安装失败,需要在远程部署前就考虑好本地包的安装路径映射
相信这样的场景随着使用深入,应该还有不少,也是考虑lerna
选型时需要注意的点。另外一种可选方案是yarn workspace,因为我没有具体使用过这里就不瞎说了,但这两种方案都属于同一维度的工具,思想是一样的。
应用场景
如果已经明晰了monorepo
的优劣以及lerna
的使用,下一步就可以针对具体场景决定是否启用了。目前来说,比较推荐的使用场景有如下几类。
- 一主包n从包的模式。非常典型的明星项目有
react
,babel
,jest
等,就是围绕着一个核心包构建的生态系统。这样的项目自然是相互依赖关系比较复杂的,使用monorepo
是很合适的方案。 - 某些子模块需要单独打包的项目,最典型的就是按需加载的组件库,具体案例有饿了么的移动端组件库mint-ui。如果使用
multirepo
就需要借助打包工具配置多入口,以及增加各种繁琐的配置文件,monorepo
就可以借助lerna
默认分包的能力自然又灵活的控制各个按需加载模块的打包。
小结
以上大概就是使用monorepo
需要了解的一些基础知识,现代化前端项目越来越重也是趋势,monorepo
还是有不小的应用空间。回顾整个前端发展,似乎就是不停的与分离和结合打交道,前后端的分离,前端微服务化的拆分,和中间层的结合到和serverless
的结合。虽然形式多种多样但总的思想其实一直没变,就是如何隔离和聚合软件工程各个模块的关键部分,随着时代和工具发展,这个隔离点也自然是在不断变化,所以monorepo
也只是这浪潮中极小也关键的一部分,总之用起来就对了。
-- EOF --