奇技指南
本文来自奇舞周刊,作者刘观宇,360 奇舞团高级前端工程师、技术经理,W3C CSS 工作组成员。
多包合作的烦恼
在开发需要多个密切协作的软件包时候,我们往往将独立的功能块进行划分,使得各个功能独立的模块分别完成,以减少相互影响,完成有效的多人合作。但是,在模块协作时,经常会遇到一些问题:
依赖处理繁琐。
依赖的模块,尚处在开发之中,通行的npm install、yarn等无法从安装源中获得。
被依赖的模块版本升级,模块其他版本需要手动管理相关的版本。
有循环依赖的风险
对于多个模块的大型项目的协作管理,一般地有multirepo、monorepo和submodules等多种方式:multirepo是将多个模块分别分为多个仓库,早期的Babel(Babel6以前)使用的就是这种方式;submodules是借助git的实现,在.gitmodules中写明引用的仓库,在主仓库中只保留必要的索引;monorepo则是将相关的模块用单一的仓库统一管理。
上述的方式各有优劣。从目前前端工程的代码管理来看,monorepo被很多超级repo选中。Babel、vue-cli、create-react-app都采用这种模式。
Babel的重要贡献者Jamie Kyle1,在为 Babel 6 工作的过程中发现所有东西都拆分成漂亮的小插件包,但同时也就需要管理数十个软件包。因此,多包存储库管理工具 Lerna 应运而生。为让项目更好用,他对项目进行了多次重写,试图让架构更完善。下图是Jamie Kyle的靓照@_@
Lerna也是Babel官方现在使用的多包管理工具。
什么是Lerna
Lerna官网2对此给出了官方的解释:Lerna是一个管理包含多个软件包的JavaScript项目的工具。它可以:
解决包之间的依赖关系。
通过git仓库检测改动,自动同步。
根据相关的git提交的commit,生成CHANGELOG。
Lerna是一个命令行工具,可以将其安装在系统全局。简单的命令说明,可以使用:lerna -h查看命令帮助。
两种模式
Lerna分为两种模式:fixed模式和independent模式。两种模式的区别在于:前者强制所有的包都使用在根目录lerna.json中指定的版本号。而后者各个软件包,可以自己指定版本号。
默认的,lerna使用的是fixed模式。笔者认为,这种模式下,所有的相关软件包,最好以几乎一致的发布周期发布,如Babel这种。并且软件内部应该被使用者更多以“黑盒”方式对待。这是fixed模式最适应的方式。
而需要暴露内部包的细节,或者迭代频率显著不一致的包,建议采用independent模式。
指定为independent模式,可以在lerna init时加入--independent,或者将lerna.json的version字段指定为independent。
Lerna配置
lerna.json通常位于项目的根目录下,定义了lerna运行的主要行为。当在根目录下运行lerna init或lerna init --independent时,会自动生成。以下是一个典型的配置:
{
"version": "1.1.3",
"npmClient": "npm",
"command": {
"publish": {
"ignoreChanges": ["ignored-file", "*.md"],
"message": "chore(release): publish"
},
"bootstrap": {
"ignore": "component-*",
"npmClientArgs": ["--no-package-lock"]
}
},
"packages": ["packages/*"]
}
上面的配置文件中:
version指定的是所有包的统一版本号;对于independent模式,这个字段请指定为independent;
npmClient指定的是npm的客户端。默认的,lerna将使用npm。读者也可依所需将程序设置为yarn,甚至cnpm等等。
command字段,可以对publish和bootstrap命令进行参数传递和命令定制。如:command.publish.ignoreChanges,用来设置一些忽略的文件,以避免无关文件的提交对于版本号的变更,如README.md等等。command.bootstrap.npmClientArgs指定在bootstrap命令时,传递的默认参数,比如我们会常常使用--no-package-lock来禁止package-lock.json或yarn.lock等等。
packages字段指定包所在的目录。
Lerna命令
初始一个多包的工程
lerna init
上述命令会初始化一个多包工程。初始化之后会在根目录生成packages目录、lerna.json,如果使用independent模式,请使用命令:lerna init --independent
创建子包
lerna create
在packages所指目录下创建package包。
添加包
lerna add
上述命令会添加一个包package指明的软件包。
指定--dev是添加在devDependencies中。
指定--exact,则将用精确匹配的版本添加包。
指定--scope将只在此指明的模块中安装这个软件包,否则将在所有packages目录中的包中安装。
对于packages目录下的子包,将通过设立systemlink来解决依赖。
对于npm镜像中存在的包,将安装镜像中的包。
运行命令
运行命令分为两种:任意命令和npm scripts定义的命令。
对于任意命令使用,lerna exec;对于npm scripts定义的命令使用lerna run
以lerna exec为例:
lerna exec [--concurrency number] [--stream] [--parallel] --
特别地,lerna exec -- rm -rf ./node_modules将删除所有包中的依赖。lerna exec -- npm uninstall
lerna exec 和 lerna run 如需要每个子模块相继的执行并按顺序输出,可以指定--concurrency 1。
对于指定了--stream的命令,将把所有子进程的输出立即回显此举可能造成子进程显示顺序交叉,为了分辨输出来源,每个输出,会带上包名;指定了--parallel的命令,则会在scope指定的范围内,并行地执行相关地命令。
lerna run与上述命令不一样的情况在于,lerna run build将在每一个包中scripts字段中执行定义的build命令。
安装所有依赖
lerna bootstrap
上述命令安装所有的依赖、将所有的相关链接做好,同时在所有的包中运行npm run prepublish。随后,在所有包中运行npm run prepare。此时,所有的依赖均已完备。
发布
lerna publish 发布所有的包。
清理
lerna clean 删除所有的node_modules
一些优化
合并公共依赖
我们在开发过程中,经常发现包依赖类似。这样,我们发现运行lerna bootstrap之后,会重复安装依赖包,这样会造成空间的浪费和效率的降低。为此,我们可以把同样的依赖包在根目录安装一次即可。此时,可以使用lerna bootstrap --hoist命令,则公用的依赖,只会在顶层目录安装一次。
发布带有scope公有包
带有scope的包,需要发布时候,如果是公有的包,需要在npm publish时候使用npm publish --access public。为了能够成功publish,并使用lerna流程,请在每个子包的package.json中加入:
"publishConfig": {
"access": "public"
}
检测循环依赖
lerna本身内置了检测循环依赖的功能,如果出现循环依赖。会在bootstrap时候给出提示:
此时,请依照提示去掉循环依赖,以保证软件包的正常运行。
文内链接
https://github.com/jamiebuilds
https://lerna.js.org/
界世的你当不
只做你的肩膀
无
360官方技术公众号
技术干货|一手资讯|精彩活动
空·
我知道你在看哟