1. 背景

对于维护过多个package的同学来说,都会遇到一个选择:这些package是放在一个仓库里维护还是放在多个仓库里单独维护,数量较少的时候,多个仓库维护不会有太大问题,但是当package数量逐渐增多时,一些问题逐渐暴露出来:
一、package之间相互依赖,开发人员需要在本地手动执行npm link,维护版本号的更替;
二、issue难以统一追踪,管理,因为其分散在独立的repo里;
三、每一个package都包含独立的node_modules,而且大部分都包含babel,webpack等开发时依赖,安装耗时冗余并且占用过多空间。

2. 什么是lerna

lerna到底是什么呢?lerna官网上是这样描述的。

用于管理具有多个包的JavaScript项目的工具。 这个介绍可以说很清晰了,引入lerna后,上面提到的问题不仅迎刃而解,更为开发人员提供了一种管理多packages javascript项目的方式。 一、自动解决packages之间的依赖关系。 二、通过git 检测文件改动,自动发布。 三、根据git 提交记录,自动生成CHANGELOG

3 命令

3.1 全局安装lerna

lerna 我们需要全局安装lerna工具。

  1. $ npm i -g lerna
  2. # 或
  3. $ yarn global add lerna

3.2 lerna bootstrap

bootstrap作了如下工作

  • 为每个包安装依赖
  • 链接相互依赖的库到具体的目录
  • 执行 npm run prepublish
  • 执行 npm run prepare

可以通过 — 后添加选项, 设置npm cient的参数

  1. lerna bootstrap -- --production --no-optional

也可以在lerna.json中配置

  1. {
  2. ...
  3. "npmClient": "yarn",
  4. "npmClientArgs": ["--production", "--no-optional"]
  5. }
  1. Options
  2. --hoist
  3. 这个选项,会把共同依赖的库安装到根目录的node_modules下, 统一版本
  4. --ignore
  5. 忽略部分目录, 不进行安装依赖
  6. lerna bootstrap --ignore component-*
  7. 也可以在lerna.json中配置
  8. {
  9. "version": "0.0.0",
  10. "command": {
  11. "bootstrap": {
  12. "ignore": "component-*"
  13. }
  14. }
  15. }
  16. --ignore-scripts
  17. 不执行声明周期脚本命令, 比如 prepare
  18. --registry <url>
  19. 指定registry
  20. --npm-client
  21. 指定安装用的npm client
  22. lerna bootstrap --npm-client=yarn
  23. 也可以在lerna.json中配置
  24. {
  25. ...
  26. "npmClient": "yarn"
  27. }
  28. --use-workspace
  29. 使用yarn workspace 没用过
  30. --no-ci
  31. 默认调用 npm ci 替换 npm install , 使用选项修改设置

3.3 lerna publish

发布新的库版本

  1. options
  2. canary
  3. 可以用来独立发布每个commit,不打tag
  4. --npm-client
  5. 默认npm
  6. --npm-tag
  7. 为发布的版本添加 dist-tag
  8. --no-verify-access
  9. 不进行用户发布的权限校验
  10. --registry
  11. 指定registry http://registry.npmjs.org/
  12. --yes
  13. 用于ci自动输入 yes
  14. --temp-tag
  15. 没啥用
  1. $ lerna publish # 用于发布更新
  2. $ lerna publish --skip-git # 不会创建git commit或tag
  3. $ lerna publish --skip-npm # 不会把包publish到npm上

3.4 lerna init

创建一个新的lerna库或者是更新lerna版本

  • 修改package.json中lerna版本
  • 创建lerna.json
    1. Options
    2. --independent
    3. 独立模式
    4. --exeact
    5. 版本号固定?

    3.5 lerna add

    添加一个包的版本为各个包的依赖 ```bash $ lerna add [@version] [—dev] # 命令签名

例如

$ lerna add module-1 —scope=module-2 # 将 module-1 安装到 module-2 $ lerna add module-1 —scope=module-2 —dev # 将 module-1 安装到 module-2 的 devDependencies 下 $ lerna add module-1 # 将 module-1 安装到除 module-1 以外的所有模块 $ lerna add babel-core # 将 babel-core 安装到所有模块

  1. <a name="iJRe7"></a>
  2. ## 3.6 lerna exec
  3. ```bash
  4. $ lerna exec -- <command> [..args] # 在所有包中运行该命令
  5. # 例如
  6. $ lerna exec --scope=npm-list yarn remove listr # 将 npm-list 包下的 listr 卸载
  7. $ lerna exec -- yarn remove listr # 将所有包下的 listr 卸载

3.7 lerna changed

列出下次发版lerna publish 要更新的包。
原理:
需要先git add,git commit 提交。
然后内部会运行git diff --name-only v版本号,搜集改动的包,就是下次要发布的。并不是网上人说的所有包都是同一个版全发布。

  1. $ lerna updated
  2. # 或
  3. $ lerna diff

3.8 lerna list

  1. 列举当前lerna 库包含的包
  2. Options
  3. --json
  4. 显示信息为json格式
  5. --all
  6. 显示包含private的包
  7. --parseable
  8. 显示如下格式 <fullpath>:<name>:<version>[:flags..]
  9. --long
  10. 显示更多的扩展信息

3.9 lerna clean

删除各个包下的node_modules

  1. $ lerna clean

3.10 lerna run

运行npm script,可以指定具体的package。

  1. $ lerna run <script> -- [..args] # 在所有包下运行指定
  2. # 例如
  3. $ lerna run test # 运行所有包的 test 命令
  4. $ lerna run build # 运行所有包的 build 命令
  5. $ lerna run --parallel watch # 观看所有包并在更改时发报,流式处理前缀输出
  6. $ lerna run --scope my-component test # 运行 my-component 模块下的 test

3.11 lerna create

  1. $ lerna create <package>

在packages所指目录下创建package包。

3.12 lerna list

列举当前lerna 库包含的包

  1. Options
  2. --json
  3. 显示信息为json格式
  4. --all
  5. 显示包含private的包
  6. --parseable
  7. 显示如下格式 <fullpath>:<name>:<version>[:flags..]
  8. --long
  9. 显示更多的扩展信息

4. lerna.json解析

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

version:当前库的版本
npmClient: 允许指定命令使用的client, 默认是 npm, 可以设置成 yarn
command.publish.ignoreChanges:可以指定那些目录或者文件的变更不会被publish
command.bootstrap.ignore:指定不受 bootstrap 命令影响的包
command.bootstrap.npmClientArgs:指定默认传给 lerna bootstrap 命令的参数
command.bootstrap.scope:指定那些包会受 lerna bootstrap 命令影响
packages:指定包所在的目录

5. 使用lerna的基本工作流

5.1 环境配置

  • Git 在一个lerna工程里,是通过git来进行代码管理的。所以你首先要确保本地有正确的git环境。 如果需要多人协作开发,请先创建正确的git中心仓库的链接。 因此需要你了解基本的git操作,在此不再赘述。
  • npm仓库 无论你管理的package是要发布到官网还是公司的私有服务器上,都需要正确的仓库地址和用户名。 你可运行下方的命令来检查,本地的npm registry地址是否正确。

    1. $ npm config ls

    lerna 我们需要全局安装lerna工具

    1. $ npm i -g lerna
    2. # 或
    3. $ yarn global add lerna

    5.2 初始化一个lerna工程

    在这个例子中,我将在我本地d:/jobs 根目录下初始化一个lerna工程。
    1、在d:/jobs下创建一个空的文件夹,命名为lerna-demo

    1. $ mkdir lerna-demo

    2、初始化 通过cmd进入相关目录,进行初始化

    1. $ cd d:/jobs/lerna-demo
    2. $ lerna init

    执行成功后,目录下将会生成这样的目录结构。

  • packages(目录)

  • lerna.json(配置文件)
  • package.json(工程描述文件)

3、添加一个测试package
默认情况下,package是放在packages目录下的。

  1. # 进入packages目录
  2. cd d:/jobs/lerna-demo/packages
  3. # 创建一个packge目录
  4. mkdir module-1
  5. # 进入module-1 package目录
  6. cd module-1
  7. # 初始化一个package
  8. npm init -y

执行完毕,工程下的目录结构如下:

  1. --packages
  2. --module-1
  3. package.json
  4. --lerna.json
  5. --package.json

4、安装各packages依赖 这一步操作,官网上是这样描述的
在当前的Lerna仓库中引导包。安装所有依赖项并链接任何交叉依赖项。

  1. $ cd d:/lerna-demo
  2. $ lerna bootstrap

在现在的测试package中,module-1是没有任何依赖的,因此为了更加接近真实情况。你可已在module-1的package.json文件中添加一些第三方库的依赖。 这样的话,当你执行完该条命令后,你会发现module-1的依赖已经安装上了。
5、发布 在发布的时候,就需要git工具的配合了。 所以在发布之前,请确认此时该lerna工程是否已经连接到git的远程仓库。你可以执行下面的命令进行查看。

  1. git remote -v
  2. // print log
  3. origin git@github.com:meitianyitan/docm.git (fetch)
  4. origin git@github.com:meitianyitan/docm.git (push)

本篇文章的代码托管在Github上。因此会显示此远程链接信息。 如果你还没有与远程仓库链接,请首先在github创建一个空的仓库,然后根据相关提示信息,进行链接。

  1. $ lerna publish

执行这条命令,你就可以根据cmd中的提示,一步步的发布packges了。
实际上在执行该条命令的时候,lerna会做很多的工作。

  1. - Run the equivalent of `lerna updated` to determine which packages need to be published.
  2. - If necessary, increment the `version` key in `lerna.json`.
  3. - Update the `package.json` of all updated packages to their new versions.
  4. - Update all dependencies of the updated packages with the new versions, specified with a [caret (^)](https://docs.npmjs.com/files/package.json#dependencies).
  5. - Create a new git commit and tag for the new version.
  6. - Publish updated packages to npm.

到这里为止,就是一个最简单的lerna的工作流了。但是lerna还有更多的功能等待你去发掘。
lerna有两种工作模式,Independent mode和Fixed/Locked mode,在这里介绍可能会对初学者造成困扰,但因为实在太重要了,还是有必要提一下的。
lerna的默认模式是Fixed/Locked mode,在这种模式下,实际上lerna是把工程当作一个整体来对待。每次发布packges,都是全量发布,无论是否修改。但是在Independent mode下,lerna会配合Git,检查文件变动,只发布有改动的packge。

6. 使用lerna提升开发流程体验

接下来,我们从一个demo出发,了解基于lerna的开发流程。

6.1 初始化项目

img1.jpg
我们需要维护一个UI组件库,其包含2个组件,分别为House(房子)和Window(窗户)组件,其中House组件依赖于Window组件。
我们使用lerna初始化整个项目,并且在packages里新建了2个package,执行命令进行初始化:

  1. $ lerna init

初始化化后目录结构如下所示:

  1. .
  2. ├── lerna.json
  3. ├── package.json
  4. └── packages
  5. ├── house
  6. ├── index.js
  7. ├── node_modules
  8. └── package.json
  9. └── window
  10. ├── index.js
  11. ├── node_modules
  12. └── package.json

6.2 增加依赖

img2.jpg
接下来,我们来为组件增加些依赖,首先House组件不能只由Window构成,还需要添加一些外部依赖(在这里我们假定为lodash)。我们执行:

  1. $ lerna add lodash --scope=house

这句话会将lodash增添到House的dependencies属性里,这会儿你可以去看看package.json是不是发生变更了。
我们还需要将Window添加到House的依赖里,执行:

  1. $ lerna add window --scope=house

就是这么简单,它会自动检测到window隶属于当前项目,直接采用symlink的方式关联过去。

symlink:符号链接,也就是平常所说的建立超链接,此时House的node_modules里的Window直接链接至项目里的Window组件,而不会再重新拉取一份,这个对本地开发是非常有用的。

6.3 发布到npm

img3.jpg
接下来我们只需要简单地执行lerna publish,确认升级的版本号,就可以批量将所有的package发布到远程。

默认情况下会推送到系统目前npm对应的registry里,实际项目里可以根据配置leran.json切换所使用的npm客户端。

6.4 更新模块

img4.jpg
接下来,我们变更了Window组件,执行一下lerna updated,便可以得知有哪些组件发生了变更。

  1. lerna updated
  2. lerna info version 2.9.1
  3. lerna info Checking for updated packages...
  4. lerna info Comparing with v1.0.9.
  5. lerna info Checking for prereleased packages...
  6. lerna info result
  7. - jx-house
  8. - jx-window

我们可以看到,虽然我们只变更了window组件,但是lerna能够帮助我们检查到所有依赖于它的组件,对于没有关联的组件,是不会出现在更新列表里的,这个对于相比之前人工维护版本依赖的更新,是非常稳健的。

6.5 集中版本号或独立版本号

截止目前,我们已经成功发布了2个package,现在再新增一个Tree组件,它和其他2个package保持独立,随后我们执行lerna publish,它会提示Tree组件的版本号将会从0.0.0升级至1.0.0,但是事实上Tree组件仅仅是刚创建的,这点不利于版本号的语义化,lerna已经考虑到了这一点,它包含2种版本号管理机制。

  • fixed模式下,模块发布新版本时,都会升级到leran.json里编写的version字段
  • independent模式下,模块发布新版本时,会逐个询问需要升级的版本号,基准版本为它自身的package.json,这样就避免了上述问题。

如果需要各个组件维护自身的版本号,那么就使用independent模式,只需要去配置leran.json即可。

6.6 总结

img5.jpg
lerna不负责构建,测试等任务,它提出了一种集中管理package的目录模式,提供了一套自动化管理程序,让开发者不必再深耕到具体的组件里维护内容,在项目根目录就可以全局掌控,基于npm scripts,可以很好地完成组件构建,代码格式化等操作,并在最后一公里,用lerna变更package版本,将其上传至远端。

7. lerna最佳实践

为了能够使lerna发挥最大的作用,根据这段时间使用lerna 的经验,总结出一个最佳实践。下面是一些特性。

  • 采用Independent模式
  • 根据Git提交信息,自动生成changelog
  • eslint规则检查
  • prettier自动格式化代码
  • 提交代码,代码检查hook
  • 遵循semver版本规范
    大家应该也可以看出来,在开发这种工程的过程的,最为重要的一点就是规范。因为应用场景各种各样,你必须保证发布的packge是规范的,代码是规范的,一切都是有迹可循的。这点我认为是非常重要的。

    8. 工具整合

    在这里引入的工具都是为了解决一个问题,就是工程和代码的规范问题。

  • husky

  • lint-staged
  • prettier
  • eslint

    9. yarn workspaces

命令
在根目录安装npm包,以danger为例:

  1. $ yarn add danger --dev -W

10.参考

lerna管理前端packages的最佳实践