前端多项目管理工具Lerna

一些业务环境下,前端会涉及多项目的管理,这种情况下往往npm依赖包和git都无法实现共享或统一化管理。因此Lerna应时而生。

A tool for managing JavaScript projects with multiple packages.

背景

随着模块的增多,传统的包管理方式已不能满足多模块的统一管理需求,在此背景下,multirepo 和 monorepo 的管理方式先后出现。

multirepo

多代码仓库(one-repository-per-module),按模块分为多个代码库。

multirepo 管理的缺点:

  • 在 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 以上 multirepo 的问题都可以迎刃而解。Monorepo(monolithic repository) 它是一种管理 organisation 代码的方式,在这种方式下会摒弃原先一个 module 一个 repo 的方式,取而代之的是把所有的 modules 都放在一个 repo 内来管理。

目前诸如 Babel, React, Angular, Ember, Meteor, Jest 等等都采用了 Monorepo 这种方式来进行源码的管理。Lerna 它是基于 Monorepo 理念在工具端的实现。

monorepo 管理的缺点:

  • 单个 repo 体积较大

从源码管理的角度来看,multirepo与monorepo是两种不同的理念,前者允许多元化发展,各个module可以有自己的玩法(构建,依赖管理,单元测试等),后者希望集中管理,减少玩法差异带来的沟通成本

典型案例:

基础

Lerna目前(2020.01.03)版本是3.22.1,已用于babel、create-react-app等各种包中。

安装

Lerna 2.x的版本起,我们可以通过npm命令进行全局安装:

1
npm install --global lerna

安装后执行

1
lerna -v

如能正常看到版本,则为安装成功。

初始化

命令:

1
lerna init

初始化后目录结构:

1
2
3
├─packages/
├─package.json
└─lerna.json

使用命令

init

初始一个lerna项目或更新lerna版本。

可选参数:--independent/-i。使用独立的版本模式。

使用lerna管理项目时,可以选择两种模式。默认的为固定模式(Fixed mode),当使用lerna init命令初始化项目时,就默认为固定模式,也可以使用 lerna init --independent 命令初始化项目,这个时候就为独立模式(Independent mode)。
固定模式中,packages下的所有包共用一个版本号(version),会自动将所有的包绑定到一个版本号上(该版本号也就是lerna.json中的version字段),所以任意一个包发生了更新,这个共用的版本号就会发生改变。而独立模式允许每一个包有一个独立的版本号,在使用lerna publish命令时,可以为每个包单独制定具体的操作,同时可以只更新某一个包的版本号。
独立模式中,lerna.json中的version字段为"independent",在每次publish时,都将得到一个提示符,提示每个已更改的包,以指定是补丁、次要更改、主要更改还是自定义更改。

bootstrap

启动一个lerna项目并安装项目下所有packages的依赖(dependencies),并且链接共享依赖。默认是npm inpm link

如果moduleA依赖core,通过lerna bootstrap命令处理依赖过后,会在moduleA的node_modules下创建软链接指向core目录

此命令非常重要,因为它允许你在require()中使用软件包名称,就好像这些软件包已经存在并在node_modules文件夹中可用。

create <name> [loc]

创建一个包,name包名,loc为位置(可选,默认为packages/)。

比如包根目录的package.json中配置为:

1
2
3
4
5
6
7
{
"workspaces": [
"packages/*",
"packages/test/*"
]
//...
}

创建命令:

1
2
3
4
5
# 创建一个包pkg默认放在workspaces[0]所指位置
lerna create pkg

# 创建一个包pkg2指定放在packages/test文件目录下,注意必须在workspace先写入packages/test/*
lerna create pkg2 packages/test

import <pathToRepo>

将本地路径<pathToRepo>中的包导入具有提交历史记录的package / <directory-name>中。

publish

创建一个新版本的已更新的软件包。提示新版本并更新git和npm上的所有软件包。

可选参数:

  • --npm-tag [tagname]:提交时带上的npm dist-tag
  • --canary/-c:金丝雀发布
  • --skip-git:不执行任何git操作
  • --force-publish [packages]:强制发布指定的软件包(以逗号分隔)或所有使用*的软件包(跳过git diff检查更改的软件包)。

changed

列出下次发版lerna publish要更新的包。

原理是通过执行git diff --name-only v版本号,搜集改动的包。

diff [package?]

自上次发行以来,比较所有软件包或单个软件包。

run [script]

在包含该脚本的每个软件包中运行npm脚本。

ls

列出当前Lerna存储库中的所有公共软件包。

add <package>[@version] [--dev]

该命令用于为packages文件夹下的package安装依赖。如:

  • lerna add babel , 该命令会在package-1和package-2下安装babel
  • lerna add react –scope=package-1 ,该命令会在package-1下安装react
  • lerna add package-2 –scope=package-1,该命令会在package-1下安装package-2

项目包建立软链接,类似npm link。大多数的devDependencies可以通过lerna link convert提升到Lerna repo的根目录。

可选参数:

  • --force-local:链接本地依赖项,无论匹配的版本范围如何。

提升有以下好处:

  • 所有包使用同样的版本
  • 可以通过自动化工具(例如GreenKeeper)使根目录的依赖关系保持最新
  • 依赖性安装时间减少
  • 需要更少的存储空间

请注意,提供npm脚本使用的“二进制(binary)”可执行文件的devDependencies仍需要直接安装在使用它们的每个软件包中。

list

列出当前项目下所有的包。(如果输出结果不符合预期,可以尝试到异常目录下执行yarn init -y命令)

exec -- <command> [..args]

在每个包中执行任意命令。如:

1
lerna exec -- rm -rf node_modules

clean

删除所有包的node_modules目录

配置文件

lerna.json

lerna.json是一个lerna项目的配置文件,默认为:

1
2
3
4
5
6
{
"packages": [
"packages/*"
],
"version": "0.0.0"
}

其中:

  • packages:array,其中每个元素代表可以发布的npm包的目录
  • version:string,该仓库的版本

一个完整的lerna.json demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"version": "1.1.3",
"npmClient": "npm",
"command": {
"publish": {
"ignoreChanges": ["ignored-file", "*.md"],
"message": "chore(release): publish",
"registry": "https://npm.pkg.github.com"
},
"bootstrap": {
"ignore": "component-*",
"npmClientArgs": ["--no-package-lock"]
}
},
"packages": ["packages/*"]
}

其他可选字段:

  • npmClient:stringyarn/npm,用于指定执行命令的客户端
  • command.publish.ignoreChanges:array,提交时的变更忽略文件
  • command.publish.message:string,执行发布的版本更新时的自定义提交消息
  • command.publish.registry:string,使用它来设置要发布到的自定义注册表URL
  • command.bootstrap.ignore:array,启动忽略
  • command.bootstrap.npmClientArgs:array,在lerna bootstrap命令期间,将作为参数直接传递给npm install的字符串数组。
  • command.bootstrap.scope:一组glob,用于限制在运行lerna bootstrap命令时将引导哪些软件包。

设置yarn的workspaces模式

修改顶层的package.json 和 lerna.json

1
2
3
4
5
6
7
8
9
// package.json
"private": true,
"workspaces": [
"packages/*"
],

// lerna.json
"useWorkspaces": true,
"npmClient": "yarn",

插件工具

lerna-changelog

可以通过lerna-changelog自动生成changelog。

安装:

1
npm i -g lerna-changelog

配置(在lerna.json添加对应配置项):

1
2
3
4
5
6
7
8
9
10
"changelog": {
"repo": "test/lernatest", // 必填
"labels": {
"enhancement": ":rocket: Enhancement",
"bug": ":bug: Bug Fix",
"doc": "Refine Doc",
"feat": "New Feature"
},
"cacheDir": ".changelog"
}

要达到“自动”,前提是日常开发维护遵守约定的规范,否则最后工具肯定猜不出来changelog。规范是指:

  • (建议)commit message关联上对应的issue
  • (必须)创建PR时要选择我们预定义的label

因为工具只整理github带有指定label的PR,并把commit message作为changelog项,建议commit message里关联上issue,生成的changelog就能关联到对应issue。

如:

1
git cm -m "feat: changelog, Close #1"

然后提交PR并给贴上label:feat,merge之后,本地pull后就可以生成lerna-changelog。

以这种方式自动整理出changelog,实际上靠的是开发中约束(PR的label规范,commit message作为changelog项的规范),与lerna没有太大关系,只要是monorepo(Issue/PR)都放在一起,就可以按照这个思路获取Issue/PR信息,整理出changelog


相关链接