初探Lerna

1、简介

首先是关于Monorepo(一篇不错的介绍Monorepo的文章),它是管理项目代码的一种方式,主要手段是通过在一个项目仓库中管理多个模块/仓库包。而Multirepo是传统的仓库管理方法,也是公司目前所用的方法,即所有的项目包都是独立仓库部署和管理。两种方式对比如下:

image.png

两种方式进行对比的话,千人千面。前者允许多元化发展(各项目可以有自己的构建工具、依赖管理策略、单元测试方法),后者希望集中管理,减少项目间的差异带来的沟通成本。

虽然拆分子仓库、拆分子 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,只要可以显示版本号,就意味着我们安装成功了。控制台输出如下图:

image.png

2.2、初始化

初始化之前的步骤无非两种,第一种是创建一个lerna-repo目录,第二种是在git上创建一个项目lerna-repo并clone到本地,然后本地进入该目录并执行

npx lerna init

一开始我们都是使用lerna的默认模式我们的目录结构会变成如下:

image.png

顾名思义,lerna.json就是Lerna的配置文件,里面可以进行不同的业务或者不同的场景的配置话定义,我们可以查看初始化的lerna.json的文件,如下图:

image.png

首先就是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文件夹下,生成的目录如下:

image.png

此处会有几种常见的错误展示,第一种就是由于你是新建的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,就会默认删除全部项目的依赖,如下图,会出现提示:

image.png

2.8、运行项目中的script命令

lerna run 命令 --scope=特定的某个包

这里可以运行start等约定好的script命令,同理,如果没有scope后缀,lerna会运行每个包的该script命令,截图如下:

image.png

2.9、查看可以发布的包

lerna changed

该命令是查看当前可以发布的包,显示自上次relase tag以来有修改的包,输入后展示图会与下图类似:

image.png

如图展示,当前有三个包可以进行发布,但是如果你发布完以后再执行该命令,此处就会展示No changed packages found。

2.10、发布某个项目

lerna publish --dist-tag=tag名

输入该命令后,会展示如下图:

image.png

在上图中,控制台会让用户选择要发布的版本的版本号,最后一个选项为自定义选项。--dist-tag=tag名为发布某一个分支的包,主要用于测试或者多版本并存的情况。发布模式如下图:

image.png

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"
}

效果如下:

image.png

posted @ 2020-11-24 17:53  JeromeZhang  阅读(964)  评论(0编辑  收藏  举报