monorepo 新浪潮 | introduce lerna
soda-x opened this issue · 22 comments
图片来自 New wave modularity with Lerna, monorepos, and npm organizations
前纪: 大概在 16 年初通过 babel 了解到了 lerna,同时也首次听到了 monorepo 这个概念。当时测试后的结果是,lerna 非常不稳定所以那会儿并没有使用。时隔近一年,在最近尝试中感觉各方面都做的不错了,所以已经开始在项目内使用。大概 google 了下国内似乎并没有文章介绍这一块内容,所以这篇文章的动力遍来源于此。
什么是 monorepo ?
Monorepo is a unified source code repository used by an organisation to host as much of its code as possible.
Monorepo
它是一种管理 organisation 代码的方式,在这种方式下会摒弃原先一个 module 一个 repo 的方式,取而代之的是把所有的 modules 都放在一个 repo 内来管理。
目前诸如 Babel, React, Angular, Ember, Meteor, Jest 等等都采用了 Monorepo 这种方式来进行源码的管理。
什么是 lerna ?
Lerna
它是基于 Monorepo 理念在工具端的实现。
lerna 出现的历史背景
Lerna
出现的历史背景,其实就是 Monorepos
和 Multirepos
在进行项目管理时优与劣。
说说我个人的感触:
Multirepos
缺点:
-
在 Multirepos 方案中我们通常一个项目会有一个 repo 或者说是一个 module 一个 repo,事实上因为项目或者 module 因为功能或者属性或者历史的原因我们不得不拆分到不同的 organisation 中,这导致了后期如果涉及人员交接,或者自己项目管理时就会陷入到不知道哪里去找 repo 的境地。(这个问题对于涉及历史包袱的开发会特别痛苦)
-
issue 不知道往哪里提,导致项目管理混乱。(目前 atool-build、dora 都有这样的困境)
-
版本管理带来的日常开销,首先不得不说采用 semver 后确实给版本管理带来了很多便利之处,但是其偏向于于 patch 版本,当 core module 需要 发布 minor 或者 Major 版本时这就会变成一场灾难。举个例子,dora (插件化 server)的 core 需要变更时,我们得同步所有官方插件,这涉及到了 20 多个仓库,这完全是体力劳动。于此同时,在日常开发中,可能我们一次迭代会涉及多个 repo,一方面需要用 npm link 的方式 hack 到本地仓库,另外一方面,每次都需要手动切换到对应的各个仓库进行 lint test 等操作,要完成这些我们不得不在 terminal 中开启多个 tab,这绝对是个眼力和体力活
-
changelog 梳理又是一场灾难,在 Multirepos 管理项目的情况下,我们需要人工同步所有变动的仓库最终列出一个 changelog。如果全部是由一个人开发还能理得清楚,但实际上一般正常迭代都是多人开发协同开发的模式,这个情况下我们很难统计到仓库依赖的 module 是否有更新,做了什么样的工作。
等等。
使用 monorepo 以上问题都可以迎刃而解。
但使用 monorepo
方案也有相应的缺陷
- 单个 repo 体积较大
- ??
以上只是个人的一些项目中的感触,也来看看 babel 为什么选择 monorepo Why is Babel a monorepo
如何用 lerna 进行项目管理
step 1:
$ npm install --global lerna
step 2:
$ git init monorepo-example
$ cd monorepo-example
step 3:
$ lerna init
运行完后在 terminal 中执行 tree
后我们可以看到此时的目录结构为
$ monorepo-example git:(master) ✗ tree
.
├── lerna.json
└── package.json
step 4:
$ monorepo-example git:(master) ✗ mkdir packages && cd packages
创建 packages
目录,该目录内将会存放之后所有的官方维护的 module
step 5:
$ packages git:(master) ✗ mkdir monorepo-example-module-a && cd monorepo-example-module-a && npm init
$ packages git:(master) ✗ mkdir monorepo-example-module-core && cd monorepo-example-module-core && npm init
新建两个 package,并通过 npm init 来初始化 package.json
此时我们的 packages
目录结构为
➜ packages git:(master) ✗ tree
.
├── monorepo-example-module-a
│ └── package.json
└── monorepo-example-module-core
└── package.json
假设 module-a 依赖 module-core 详细参考 monorepo-example 案例
step 6:
$ monorepo-example git:(master) ✗ lerna bootstrap
Lerna v2.0.0-beta.31
Bootstrapping 2 packages
Preinstalling packages
Installing external dependencies
Symlinking packages and binaries
Postinstalling packages
Prepublishing packages
Successfully bootstrapped 2 packages.
执行 lerna bootstrap
该操作会自动为 module-a 进行 npm install
和 npm link
操作. 如图
是不是非常方便呢!
step 7:
$ monorepo-example git:(master) lerna publish
Lerna v2.0.0-beta.31
Current version: 0.0.0
Checking for updated packages...
? Select a new version (currently 0.0.0) Patch (0.0.1)
Changes:
- monorepo-example-module-a: 0.0.1 => 0.0.1
- monorepo-example-module-core: 1.0.0 => 0.0.1
? Are you sure you want to publish the above changes? Yes
Publishing packages to npm...
npm WARN dist-tag add latest is already set to version 0.0.1
monorepo-example-module-a
npm WARN dist-tag add latest is already set to version 0.0.1
Pushing tags to git...
To git@github.com:pigcan/monorepo-example.git
0f8674c..2ecb064 master -> master
To git@github.com:pigcan/monorepo-example.git
* [new tag] v0.0.1 -> v0.0.1
Successfully published:
- monorepo-example-module-a@0.0.1
- monorepo-example-module-core@0.0.1
执行 lerna publish
回答几个问题便可以把自己的包推送到 npm.
当然实际情况使用中,会更复杂一些,更多的内容就留给大家看官方使用说明了,基本都是简单明了的内容,如果有不清楚的地方欢迎大家提问 官方文档 commands
利用 lerna-changelog 来进行 changelog 的生成
在现实开发中我们经常碰到一个老大难的问题就是 changelog 的梳理,在 lerna 中提供了一个非常有用的 lerna-changelog 的库,在一定的规范开发下会使得这个问题解决起来非常方便,在这边以这个仓库为例我给大家大概讲解下如何使用。
step 1:
$ monorepo-example git:(master) npm install lerna-changelog --save-dev
安装 lerna-changelog 依赖
step 2:
修改 lerna.josn
需要新增相关 lerna-changelog 所需要的配置
+"changelog": {
+ "repo": "pigcan/monorepo-example",
+ "labels": {
+ "tag: bugfix": "Bug fix",
+ "tag: enhancement": "Enhancement"
+ },
+ "cacheDir": ".changelog"
+}
在这边需要特别注意的是 labels 内的内容,labels 的 key 必须在 github 的仓库内定义好
可以通过以下链接 https://github.com/pigcan/monorepo-example/labels 来进行 labels 的创建,接下来我在 github 上分别新增 tag: bugfix
和 tag: enhancement
step 3:
$ export GITHUB_AUTH="..."
GITHUB_AUTH 的 token 字段可以在github 申请 token 获得。
步骤到此结束了,但是这边要想要达到理想的效果必须要遵循一定的开发规则,其实就是需要有好的开发习惯,在此推荐个人的习惯。
如案例所示 https://github.com/pigcan/monorepo-example/issues 把项目碰到的 bug、需要增强的地方等等内容都记入到 issue中,并标记好相应的 label 标签。
创建对应的分支,来解决对应 issue 中记入的内容。
$ monorepo-example git:(bugfix-core) git branch
* bugfix-core
enhance-module-a
master
例如我们在此修复 core 的问题,最终 commit 的日志推荐为
$ git commit -a -m "core-bugfix: the xxx problem had fixed, Close #1"
在 commit 的 message 中要把解决了什么问题和相应的 issue 进行关联,然后一目了然,如果后续需要对该部分代码进行回滚也会变得非常轻松。
一旦 bugfix-core
分支被提交后,我们可以在 github 上以该分支创建一个 pr ,
在创建 pr 时需要注意 ** 一定要选择对应的 label ** 在这个案例中我需要选择,tag: bugfix
具体可以参考 soda-x/monorepo-example#3
一旦分支的代码合并到主干后本地运行
$ monorepo-example git:(master) node_modules/.bin/lerna-changelog
## Unreleased (2017-01-05)
#### Bug fix
* `monorepo-example-module-core`
* [#3](https://github.com/pigcan/monorepo-example/pull/3) core-bugfix: the xxx problem had fixed, Closes [#1](https://github.com/pigcan/monorepo-example/issues/1). ([@pigcan](https://github.com/pigcan))
#### Committers: 1
- pigcan ([pigcan](https://github.com/pigcan))
看是不是已经生成了绝妙的 changelog 日志。
在需要 publish 之前我们运行一次 lerna-changelog
以便拿到日志(publish 后之前的 commit 信息将会被清空)
一旦 publish 后我们便可以创建 release note https://github.com/pigcan/monorepo-example/releases
以下便是最终的效果,是不是很酷很方便呢!
Refs
$ git commit -a -m "core-bugfix: the xxx problem had fixed, Close #1"
这里的 commit 中的 tag 似乎笔误写倒了
@oMaten commit 中的 message 不需要包含 tag,label 的内容即 tag 所 map 的内容最终在 pr 阶段选择 所对应的 label 来生效
@pigcan 好的谢谢~
正在用lerna做组件库,及时雨啊。。
commit message 不用 angular style 了,还真有点不习惯了。
汇总一个仓库时候,会有提交日志混在同一分支线的情况。如果分多个 repos,每个 repos 的分支线都仅仅表示当前模块相关的,请问这样的问题,有什么好避免的想法?
汇总一个仓库,前提是适合放在一起。
前提成立的情况下,还行日志清晰,这个就和自身 git commit message 相关了。可以参看我另外一篇文章,适合一个给自己项目的 adaptor 就可以了。
放到 packages 里面的每一个 mudule,别人怎么使用呢?
比如原来multiRepo的时候,babel,babel-core 和 babel-dore 是单独的repo,我使用的时候是:npm install babel babel-core babel-dore 来进行安装使用。
现在把babel-core和babel-dore都放在babel的packages目录下作为子模块了,安装使用方式还是一样的吗?
谢谢,
如果是一样的方式,其中的逻辑我有点不太明白,想请教一下作者,这里面是什么样的机制?
如果想要在自己的本地仓库或者是私有仓库里面使用,要做哪些配置呢?还是只有npm支持这种机制?
@kybetter 不管是 multiRepo 还是 monoRepo 包本质上还是独立的,只是 monoRepo 中会在 bootstrap 时把这些子包 link 起来。
不需要额外配置什么,跟着我的流程走就可以了。
@pigcan
据我所知,不管是 workspace 还是 link 的方式,也仅仅在开发的时候有用,bootstrap 的时候你也看到了,它会在每个模块下进行依赖安装和 link。
但是这跟我们 build 一个项目没有多大关系啊。build 完以后成啥样了?我现在脑子里是一团糟,绕不出来。
@kybetter 其实我不太理解你的问题
但还是尝试回答下:
bootstrap 时就是把你的依赖给 link 起来,即如果一个 monorepo 中有模块 a,b,c,其中 c 依赖 a 和 b,link 的作用是,a 和 b 更改后并不需要被发布, c 可以随时用最新的 a 和 b。
但是 build,我不清楚这里的 build 的含义是什么,有两种解释
- build 指代码模块 transform
如果作为 babel transform 的话,那么就是因为我们正常情况下引用模块是用的 transform 完的(通常在 lib 下),而本地则是源码,那么就需要单独跑一个 watch 实时把源码文件 src 编译到 lib,但是这种情况下也会出现一个问题即如果我们调试 c ,却发现 a 中有报错,但是 a 是被编译后的代码,这并不利于调试,当然也有办法解决,记得我在一个脚手架中有说明,其中就是借鉴了 cssnano 的设计方式。
- build 指项目构建
如果指的是项目构建(构建到 web 或者 electron 等),那么你在采用 monoRepo 时就是混用了项目和模块,建议分开,项目即项目,模块即模块;当然一定要合在一起也可以,那么项目构建应该和 monoRepo 的流程没啥关系。这个时候的 build 更多意思是构建一份可在目标容器执行的代码。
@pigcan 非常感谢,通过学习我知道 monoRepo 和它的管理方式了,我之前提的问题可能是把 multiRepo 的概念混合进来了,所以造成了理解偏差。
请问下,lerna下项目和模块怎么管理呢,是把项目和模块放在lerna下座位不同的package吗,我这样理解对吗。
@qq13836848 en
感谢
请教个问题:如何发布组件库到git,而不是npm仓库?
前提描述
公司只有git
仓库可用。
我使用lerna开发组件库,仓库名为my-component
。
lerna.json
配置如下:
{
"packages": [
"packages/*"
],
"useWorkspaces": true,
"npmClient": "yarn",
"version": "0.0.1",
"publishConfig": {
"directory": "dist"
}
}
根目录的package.json
配置中增加:
"private": true,
"workspaces": [
"packages/**"
]
packages
中每个组件(项目)的package.json
增加:
"private": true
问题描述:
lerna publish
命令可以设置发布组件打包后的dist
到npm
或者私人搭建的npm
仓库。但是公司代码是不可能发布到npm
的,而且也没有搭建私人npm
仓库。这就造成我的业务代码也只能放在packages
中开发,这样才能import
我开发的组件。
疑问?
能否通过lerna publish
发布到本地,我把这些文件(只有打包后的dist文件)上传到公司git
,再通过yarn add xxxx/my-component-release.git
下载到对应的业务代码项目中。主要就是想通过yarn add xxxx/my-component-release.git
来添加私有依赖包
我现在的解决方案
通过git subtree
拉取my-component
,然后根目录的package.json
配置:
"private": true,
"workspaces": [
"my-component/**"
]
这样我就可以在业务项目中使用自己开发的组件库了。
lerna publish
可能有默认的行为(比如打 tag,发包等),但是这些行为可以通过配置项去除。
如果你想要建立在现有工作流方式上做,建议自己写发布脚本,但是在发布脚本内,可以调用 lerna 来管理流程,但是最终比如到发布环节,直接走你自己的流程就好了。
step 3:
$ export GITHUB_AUTH="..."
这步骤啥意思呢,我已经拿到了github 的 token,哪里配置 export
@ZengTianShengZ 命令行直接输 export
在执行node_modules/.bin/lerna-changelog的时候报错Could not infer "repo" from the "package.json" file.,但是我配置了"repo": "AlenOne/lerna"