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

效果如下:

初探Lerna的更多相关文章

  1. 初探领域驱动设计(2)Repository在DDD中的应用

    概述 上一篇我们算是粗略的介绍了一下DDD,我们提到了实体.值类型和领域服务,也稍微讲到了DDD中的分层结构.但这只能算是一个很简单的介绍,并且我们在上篇的末尾还留下了一些问题,其中大家讨论比较多的, ...

  2. CSharpGL(8)使用3D纹理渲染体数据 (Volume Rendering) 初探

    CSharpGL(8)使用3D纹理渲染体数据 (Volume Rendering) 初探 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码 ...

  3. 从273二手车的M站点初探js模块化编程

    前言 这几天在看273M站点时被他们的页面交互方式所吸引,他们的首页是采用三次加载+分页的方式.也就说分为大分页和小分页两种交互.大分页就是通过分页按钮来操作,小分页是通过下拉(向下滑动)时异步加载数 ...

  4. JavaScript学习(一) —— 环境搭建与JavaScript初探

    1.开发环境搭建 本系列教程的开发工具,我们采用HBuilder. 可以去网上下载最新的版本,然后解压一下就能直接用了.学习JavaScript,环境搭建是非常简单的,或者说,只要你有一个浏览器,一个 ...

  5. .NET文件并发与RabbitMQ(初探RabbitMQ)

    本文版权归博客园和作者吴双本人共同所有.欢迎转载,转载和爬虫请注明原文地址:http://www.cnblogs.com/tdws/p/5860668.html 想必MQ这两个字母对于各位前辈们和老司 ...

  6. React Native初探

    前言 很久之前就想研究React Native了,但是一直没有落地的机会,我一直认为一个技术要有落地的场景才有研究的意义,刚好最近迎来了新的APP,在可控的范围内,我们可以在上面做任何想做的事情. P ...

  7. 【手把手教你全文检索】Apache Lucene初探

    PS: 苦学一周全文检索,由原来的搜索小白,到初次涉猎,感觉每门技术都博大精深,其中精髓亦是不可一日而语.那小博猪就简单介绍一下这一周的学习历程,仅供各位程序猿们参考,这其中不涉及任何私密话题,因此也 ...

  8. Key/Value之王Memcached初探:三、Memcached解决Session的分布式存储场景的应用

    一.高可用的Session服务器场景简介 1.1 应用服务器的无状态特性 应用层服务器(这里一般指Web服务器)处理网站应用的业务逻辑,应用的一个最显著的特点是:应用的无状态性. PS:提到无状态特性 ...

  9. NoSQL初探之人人都爱Redis:(3)使用Redis作为消息队列服务场景应用案例

    一.消息队列场景简介 “消息”是在两台计算机间传送的数据单位.消息可以非常简单,例如只包含文本字符串:也可以更复杂,可能包含嵌入对象.消息被发送到队列中,“消息队列”是在消息的传输过程中保存消息的容器 ...

随机推荐

  1. Java学习的第三十八天

    例3.4. package bgio; public class cjava { public static void main(String[]args) { int i=1; int sum=0; ...

  2. Linux和MySQL的安装与基本操作

  3. IO流读写数据简单示例

    常用的字节输入流有:InputStream ,FileInputStream,BufferedInputStream 常用的字节输出流有:OutputStream,FileOutputStream,B ...

  4. JS小案例:循环间隔重复变色

    在A.B.C三个区块中,有且仅有一个红色,要求红色每隔一秒即进入下一个区块,变色过程不断循环往复. 参考代码: <!DOCTYPE html> <html lang="zh ...

  5. select模型(一 改进客户端)

    一.改程序使用select来改进客户端对标准输入和套接字输入的处理,否则关闭服务器之后循环中的内容都要被gets阻塞.原程序中https://www.cnblogs.com/wsw-seu/p/841 ...

  6. mysql之binlog和各类日志介绍

    1.错误日志 错误日志作用: 记录MySQL的启动.停止信息以及在MySQL运行过程中的错误信息. 参数log_error(默认开启)  修改后重启生效 log_error=[path/[file_n ...

  7. 磁盘构造/msdos分区(fdisk)格式化(mkfs)和挂载

    分区不是必要的,分区是与系统盘分开,防止数据丢失. 磁盘使用流程:查看磁盘(fdisk -l)---分区---格式化(创建文件系统)----挂载(自动挂载) 分区表类型:msdos(一般是系统分区)  ...

  8. 慢话crush-各种crush组合

    前言 ceph已经是一个比较成熟的开源的分布式存储了,从功能角度上来说,目前的功能基本能够覆盖大部分场景,而社区的工作基本上是在加入企业级的功能和易用性还有性能等方面在发力在,不管你是新手还是老手,都 ...

  9. Ubuntu 12.10设置root用户登录图形界面

    Ubuntu 12.04默认是不允许root登录的,在登录窗口只能看到普通用户和访客登录.以普通身份登陆Ubuntu后我们需要做一些修改,普通用户登录后,修改系统配置文件需要切换到超级用户模式,在终端 ...

  10. js-根据日期获取本年所有周日

    /** * 方法 描述 Date() 返回当日的日期和时间. getDate() 从 Date 对象返回一个月中的某一天 (1 ~ 31). getDay() 从 Date 对象返回一周中的某一天 ( ...