初探Lerna
1、简介
首先是关于Monorepo(一篇不错的介绍Monorepo的文章),它是管理项目代码的一种方式,主要手段是通过在一个项目仓库中管理多个模块/仓库包。而Multirepo是传统的仓库管理方法,也是公司目前所用的方法,即所有的项目包都是独立仓库部署和管理。两种方式对比如下:
两种方式进行对比的话,千人千面。前者允许多元化发展(各项目可以有自己的构建工具、依赖管理策略、单元测试方法),后者希望集中管理,减少项目间的差异带来的沟通成本。
虽然拆分子仓库、拆分子 npm 包是进行项目隔离的天然方案,但当仓库内容出现关联时,没有任何一种调试方式比源码放在一起更高效。
在前端开发环境中,多 Git Repo,多 npm 则是这个理想的阻力,它们导致复用要关心版本号,调试需要 npm link。而这些是 MonoRepo 最大的优势。
上图中提到的利用相关工具就是今天的主角 Lerna ! Lerna是业界知名度最高的 Monorepo 管理工具,功能完整。而且目前很多大型开源项目都采用了这种方式,比如Babel、React、Meteor、Ember、Jest、Vue等等,当我们查看他们的源码时,可以发现他们的目录都是将主要内容放在packages目录中,分多个模块进行管理。
2、使用
2.1、全局安装Lerna
npm install lerna -g
或者
yarn add lerna -D
安装完成后,在控制台输入lerna -v,只要可以显示版本号,就意味着我们安装成功了。控制台输出如下图:
2.2、初始化
初始化之前的步骤无非两种,第一种是创建一个lerna-repo目录,第二种是在git上创建一个项目lerna-repo并clone到本地,然后本地进入该目录并执行
npx lerna init
一开始我们都是使用lerna的默认模式我们的目录结构会变成如下:
顾名思义,lerna.json就是Lerna的配置文件,里面可以进行不同的业务或者不同的场景的配置话定义,我们可以查看初始化的lerna.json的文件,如下图:
首先就是packages属性,它的类型是一个数组,在这里可以配置可以发布的npm包的目录,可以根据不同的场景进行定义发布的npm包所在的不同的文件夹。其次的属性version就是当前lerna项目的版本号。
2.3、引入npm包/生成一个npm包
这里不得不介绍一下引入npm包的强大之处,它可以把你引入的包的git commit记录完整的引入。我们一般都是先将要引入的包下载到本地,命令如下:
lerna import ../userlogin
如果是新项目,需要新生成一个npm包的话,可以使用如下命令lerna create <包名> [目录]:
lerna create base packages/npm
ps:此处npm为原有目录;
引入一个npm包其实还可以通过进入当前packages文件夹下,通过git clone的方式进行引入,这种优点是可以自由的在某一个项目中进行切换分支,如果是import的方式引入的npm包,目前我是没找到自由切换单项目分支的方式,欢迎大家补充。
create时,按照lerna的提示输入name、version等属性就好了,如果为了省事可以一路回车走到最后,它就会以默认值的形式生成一个新的npm包。
无论是引入的npm包,还是生成的npm包,默认目录都是在当前项目根目录下的packages文件夹下,生成的目录如下:
此处会有几种常见的错误展示,第一种就是由于你是新建的lerna项目,没有进行git commit,这时候进行lerna import会报错如下:
Error: Command failed: git rev-parse HEAD
lerna ERR! fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree.
lerna ERR! Use '--' to separate paths from revisions, like this:
lerna ERR! 'git <command> [<revision>...] -- [<file>...]'
lerna ERR!
lerna ERR! HEAD
lerna ERR!
lerna ERR! at makeError (/usr/local/lib/node_modules/lerna/node_modules/execa/index.js:174:9)
lerna ERR! at Function.module.exports.sync (/usr/local/lib/node_modules/lerna/node_modules/execa/index.js:338:15)
lerna ERR! at Object.execSync (/usr/local/lib/node_modules/lerna/node_modules/@lerna/child-process/index.js:22:16)
lerna ERR! at ImportCommand.getCurrentSHA (/usr/local/lib/node_modules/lerna/node_modules/@lerna/import/index.js:129:34)
lerna ERR! at ImportCommand.initialize (/usr/local/lib/node_modules/lerna/node_modules/@lerna/import/index.js:98:31)
lerna ERR! at Promise.resolve.then (/usr/local/lib/node_modules/lerna/node_modules/@lerna/command/index.js:266:24)
lerna ERR! at <anonymous>
lerna ERR! lerna Command failed: git rev-parse HEAD
lerna ERR! lerna fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree.
lerna ERR! lerna Use '--' to separate paths from revisions, like this:
lerna ERR! lerna 'git <command> [<revision>...] -- [<file>...]'
lerna ERR! lerna
lerna ERR! lerna HEAD
如果你没有初始化git项目的话,会有如下报错:
cli v3.8.0
lerna ERR! Error: Command failed: git log --format=%h
lerna ERR! fatal: Not a git repository (or any of the parent directories): .git
lerna ERR!
lerna ERR!
lerna ERR! at makeError (/usr/local/lib/node_modules/lerna/node_modules/execa/index.js:174:9)
lerna ERR! at Function.module.exports.sync (/usr/local/lib/node_modules/lerna/node_modules/execa/index.js:338:15)
lerna ERR! at Object.execSync (/usr/local/lib/node_modules/lerna/node_modules/@lerna/child-process/index.js:22:16)
lerna ERR! at ImportCommand.externalExecSync (/usr/local/lib/node_modules/lerna/node_modules/@lerna/import/index.js:137:34)
lerna ERR! at ImportCommand.initialize (/usr/local/lib/node_modules/lerna/node_modules/@lerna/import/index.js:82:25)
lerna ERR! at Promise.resolve.then (/usr/local/lib/node_modules/lerna/node_modules/@lerna/command/index.js:266:24)
lerna ERR! at <anonymous>
lerna ERR! lerna Command failed: git log --format=%h
lerna ERR! lerna fatal: Not a git repository (or any of the parent directories): .git
lerna ERR! lerna
但是如果原项目是具有合并提交冲突的存储库时,import命令便无法尝试应用所有提交。此时可以使用 --flatten命令标志来请求导入固定的历史记录,也就是将每次合并提交作为单独的更改,引入合并。报错如下:
lerna ERR! import Rolling back to previous HEAD (commit e232dec13e7a62943567257eff8573db2eb1a19e)
lerna ERR! EIMPORT Failed to apply commit 2aafdfd.
lerna ERR! EIMPORT Command failed: git am -3 --keep-non-patch
lerna ERR! EIMPORT Can't find Husky, skipping applypatch-msg hook
lerna ERR! EIMPORT You can reinstall it using 'npm install husky --save-dev' or delete this hook
lerna ERR! EIMPORT error: Failed to merge in the changes.
lerna ERR! EIMPORT hint: Use 'git am --show-current-patch' to see the failed patch
lerna ERR! EIMPORT Applying: feat(component): add chromeDownload component
lerna ERR! EIMPORT Using index info to reconstruct a base tree...
lerna ERR! EIMPORT M packages/dt-react-component/.storybook/config.js
lerna ERR! EIMPORT Falling back to patching base and 3-way merge...
lerna ERR! EIMPORT Auto-merging packages/dt-react-component/.storybook/config.js
lerna ERR! EIMPORT CONFLICT (content): Merge conflict in packages/dt-react-component/.storybook/config.js
lerna ERR! EIMPORT Patch failed at 0001 feat(component): add chromeDownload component
lerna ERR! EIMPORT When you have resolved this problem, run "git am --continue".
lerna ERR! EIMPORT If you prefer to skip this patch, run "git am --skip" instead.
lerna ERR! EIMPORT To restore the original branch and stop patching, run "git am --abort".
lerna ERR! EIMPORT
lerna ERR! EIMPORT
lerna ERR! EIMPORT You may try again with --flatten to import flat history.
命令如下:
$ lerna import ../base --flatten
2.4、显示当前列表
lerna list
2.5、 为项目包添加依赖
命令格式:
lerna add 包名 [--scope=特定的某个包]
例子:
lerna add component --scope=base
如上面例子代码,执行成功这段代码后,我们可以在base项目中引用component的包了,而且会自动把component包的版本号更新到base包的package.json中。此处需要注意,如果我们add的包不在我们的packages目录下,它就会自动从npm的仓库上进行下载,并且因为加了scope,所以只会给base包进行安装依赖,如果不加scope这段代码的话,会自动给packages目录下所有的包更新安装component依赖。如果原项目中在package.json中有当前npm包的配置,那么这行命令会将原配置删除,并新增当前最新版本的npm包,保证其的版本实时性。
2.6、为每个包安装依赖
lerna bootstrap --scope=特定的某个包
这行代码功能和npm install或者yarn差不多,如果不加后缀scope,lerna会自动把当前工程下的所有包的依赖都安装好。
请注意,Lerna在这有一个非常强大的功能,
2.7、删除某个包的依赖
lerna clean --scope=特定的某个包
同安装依赖,这行代码就是删除某个包的依赖,跟rm -rf node_modules功能一致,将某个项目的node_modules删除,如果不加scope,就会默认删除全部项目的依赖,如下图,会出现提示:
2.8、运行项目中的script命令
lerna run 命令 --scope=特定的某个包
这里可以运行start等约定好的script命令,同理,如果没有scope后缀,lerna会运行每个包的该script命令,截图如下:
2.9、查看可以发布的包
lerna changed
该命令是查看当前可以发布的包,显示自上次relase tag以来有修改的包,输入后展示图会与下图类似:
如图展示,当前有三个包可以进行发布,但是如果你发布完以后再执行该命令,此处就会展示No changed packages found。
2.10、发布某个项目
lerna publish --dist-tag=tag名
输入该命令后,会展示如下图:
在上图中,控制台会让用户选择要发布的版本的版本号,最后一个选项为自定义选项。--dist-tag=tag名为发布某一个分支的包,主要用于测试或者多版本并存的情况。发布模式如下图:
2.11、查看diff
lerna diff
作用类似git diff,主要用于显示自上次relase tag以来有修改的包的差异。
2.12、链接引用
lerna link
链接项目引用的库。主要用于项目包建立软链,类似 npm link。
2.13、exec
在每个包目录下执行任何命令。
$ lerna exec -- <command> [..args] # runs the command in all packages
$ lerna exec -- rm -rf ./node_modules
$ lerna exec -- protractor conf.js
$ lerna exec -- npm view \$LERNA_PACKAGE_NAME
$ lerna exec -- node \$LERNA_ROOT_PATH/scripts/some-script.js
3、模式
Lerna提供了两种管理项目的方式,一种是固定模式(Fixed mode),另一种是独立模式(Independent mode),两者区分如下:
固定模式下,packages文件夹下的所有包将会公用一个版本号version(这个version就是lerna.json中的version字段),会自动将所有的包绑定到一个版本号上面,当任意一个npm包发生了更新,整个公用版本号就会发生变化。
独立模式下,每个npm包都具有一个独立的版本号,在使用发布命令lerna publish命令时,可以为每个包单独的进行发布操作,同时只更新当前包的版本号,不会影响其余包的版本。在此模式下,lerna.json的version字段改为independent即可。
lerna version 会检测从上一个版本发布以来的变动,但有一些文件的提交,我们不希望触发版本的变动,譬如 .md 文件的修改,并没有实际引起 package 逻辑的变化,不应该触发版本的变更。可以通过ignoreChanges配置排除,如下:
{
"packages": [
"packages/*"
],
"command": {
"bootstrap": {
"hoist": true
},
"version": {
"conventionalCommits": true
}
},
"ignoreChanges": [
"**/*.md"
],
"version": "0.0.1-alpha.1"
}
效果如下: