你所知道的 Lerna

3,800 阅读4分钟

转载请保留这部分内容,注明出处。
关注公众号“头号前端”,每周新鲜前端好文推送。

另外,头条号前端团队非常 期待你的加入

前言

将大型代码仓库分割成多个独立版本化的软件包(package)对于代码复用来说非常有用。 虽然拆分子仓库、拆分子 NPM 包(For web)是进行项目隔离的天然方案,但当仓库内容出现关联时,没有任何一种调试方式比源码放在一起更高效。 工程化的最终目的是让业务开发可以 100% 聚焦在业务逻辑上 ,那么这不仅仅是脚手架、框架需要从自动化、设计上解决的问题,这涉及到仓库管理的设计。 一个理想的开发环境可以抽象成这样: “只关心业务代码,可以直接跨业务复用而不关心复用方式,调试时所有代码都在源码中。” 在前端开发环境中,多 Git Repo,多 Npm 则是这个理想的阻力,它们导致复用要关心版本号,调试需要 Npm Link。 当维护的 package 越来越多时,就会遇到以下几个问题:

  • 管理困难: package之间相互依赖,开发人员需要在本地手动执行 npm link ,维护版本号的更替,独立仓库间组件版本号的维护需要手动操作,因为源代码不在一起,所以没有办法整体分析依赖,自动化管理版本号的依赖。

  • 调试困难: issue难以统一追踪,管理,因为其分散在独立的repo里。

  • 占用总空间大: 每一个package都包含独立的 node_modules ,而且大部分都包含 babel , webpack 等开发时依赖,安装耗时冗余并且占用过多空间,比如 React 等大型模块,时间久了可能会占用几十 GB 的额外空间,对于没有外接硬盘的同学来说,定期清理不用的项目下的 node_modules 也是一件麻烦事。

尤其是在开发 UI 组件库,Util 工具库以及插件库等场景

Monorepo

Monorepo is a unified source code repository used by an organisation to host as much of its code as possible.

Monorepo 它是一种管理组织代码的方式,在这种方式下会摒弃原先一个 module 一个 repo 的方式,取而代之的是把所有的 modules 都放在一个 repo 内来管理。可以看看这里 Why is Babel a monorepo?

Monorepo 简单的说,是指将公司的所有代码放到一个 Git / Mercurial / Subversion 的代码仓库中。然后经过合理的划分,分为多个package 的模块。比如一个普通的互联网公司,代码仓库的架构可以这样子:

./frontend/web/vendors
./frontend/web/vendors/vue.js
./frontend/web/ui/registration
./frontend/web/ui/login
./frontend/ios/apps/app1
./frontend/ios/vendors/yoga
./frontend/ios/features/authentication
./frontend/ios/libraries/network
./frontend/android/apps/app1
./frontend/android/vendors/sqldelight
./shared/protobuf_schema
./backend/vendors/envoy_proxy
./backend/messaging/gateway
....

这样子分割,整体上是很大的,但是每个人关注点是分散的,每次修改的应该是不同的几个文件夹内, 比如要修改前端和后端的通信协议,只需要一个 commit 就可以把 protobuf 改掉,同时把使用 protobuf 的后端和三个前端的代码一起改了。

Lerna 它是基于 Monorepo 理念在工具端的最流行的实现之一。

Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm. 引用官方的说法:是一个管理工具,用于管理包含多个软件包(package)的 JavaScript 项目。

Lerna 的基础使用

推荐全局安装,因为会经常用到 lerna 命令 npm i -g lerna

1. 初始化:

$ mkdir lerna-repo && cd $_
$ lerna init

2. lerna init 创建项目

  • --independent(独立模式):每个包都有自己独立的版本号。lerna会配合git,检查文件变动,只发布有改动的package。

  • --fixed(固定模式):默认。所有package 共用一个版本号,比如:babel 任何 package 的 major change 均会导致所有包都会进行 major version的更新。

可以看到以下文件

➜  lerna-repo git:(master) ✗ ls
lerna.json   package.json packages

// package.json
{
  "name": "root",
  "private": true, // 表示私有的,不会被发布,是管理整个项目,与要发布到npm的解耦
  "devDependencies": {
    "lerna": "^3.22.1"
  }
}
 // lerna.json
{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0" // lerna init -i 创建这个参数如后面所示"version": "independent" 
}

3. Lerna create 创建管理包

使用 lerna create@demo/core 按照命令行提示就能创建一个包

➜  lerna-repo git:(master) ✗ lerna create @demo/core
lerna notice cli v3.22.1
lerna WARN ENOREMOTE No git remote found, skipping repository property
package name: (@demo/core) 
version: (0.0.0) 
description: 
keywords: 
homepage: 
license: (ISC) MIT
entry point: (lib/core.js) 
git repository: 
About to write to /Users/username/lerna-repo/packages/core/package.json:

{
  "name": "@demo/core",
  "version": "0.0.0",
  "description": "> TODO: description",
  "author": "sundaojiao <[sundaojiao@bytedance.com](mailto:sundaojiao@bytedance.com)>",
  "homepage": "",
  "license": "MIT",
  "main": "lib/core.js",
  "directories": {
    "lib": "lib",
    "test": "__tests__"
  },
  "files": [
    "lib"
  ],
  "publishConfig": {
    "registry": "[http://bnpm.byted.org](http://bnpm.byted.org/)"
  },
  "scripts": {
    "test": "echo \"Error: run tests from root\" && exit 1"
  }
}


Is this OK? (yes) y
lerna success create New package @demo/core created at ./packages/core

同理创建了 lerna create@demo/plugin 可以获取到如下结构:

4. Lerna add 添加依赖

lerna add eslint                           // 为所有 package 增加 eslint 模块
lerna add react --scope @demo/core       // 为 @demo/core 增加 react 模块
lerna add @demo/core --scope @demo/plugin  // 增加内部模块之间的依赖,把 @demo/core 添加到 @demo/plugin 的依赖中

结果如下

5. lerna bootstrap :安装所有依赖项并链接任何交叉依赖项

为所有包安装依赖,一般从远程 monorepo 仓库克隆代码之后就要经过这步

  • --hoist这个选项,会把共同依赖的库安装到根目录的node_modules下, 统一版本

  • --npm-client指定安装用的npm client,如下

lerna bootstrap --npm-client=yarn

这些常用的参数都可以在 lerna.json中进行配置

{
  "packages": [
    "packages/*"
  ],
  "command": {
    "bootstrap": {
      "ignore": "component-*", // 忽略部分目录,不进行安装依赖
      "npmClient": "yarn",
      "hoist": true
    }
  },
  "version": "0.0.0"
}

6. lerna clean 删除各个包下的node_modules

7. Lerna version

具体识别出修改的包 --> 创建新的版本号 --> 修改package.json --> 提交修改 打上版本的tag --> 推送到git上。

  • --conventional-commits

使用了这个选项, lerna会收集日志, 自动生成 CHANGELOG

  • --changelog-preset

修改changelog生成插件, 默认是 angular

  • --exact 将会锁定包之间的依赖关系,而不是使用 semver (^)向后兼容的形式

8. lerna publish 发布版本

主要进行一下步骤

  • 检查从上一个 git tag 之后是否有提交,没有提交就会显示 No changed packages to publish 的信息,然后退出

  • 检查依赖了修改过的包的包,并更新依赖信息

  • 提交相应版本的 git tag ,这个需要在命令行中确认

  • 发布修改的包及依赖它们的包

一些参数配置

  • --npm-tag 为发布的版本添加 dist-tag,在发非 master 分支时经常使用的,发布 prerelease 版本

  • from-git 直接发布当前的 commit,用于发布失败的时候,已经自动生成 tag 的

和 yarn workspace 结合

当然,如果对于 lerna bootstrap,add 觉得有成本的,可以使用 yarn workspace,对于从远程仓库克隆的包,直接运行 yarn 即可(

yarn install # 等价于 lerna bootstrap --npm-client yarn --use-workspaces --hoist

),就能自动建立链接,npm 5 以前,安装速度差异较大,所以选用 yarn。

优势
  • 不用设置 hoist,所有的项目依赖将被安装在一起,这样可以让 Yarn 来更好地优化它们

  • Yarn 自动将使用一个单一的 lock 文件,而不是每个包都有一个,这意味着拥有更少的冲突和更容易的进行代码检查。

开启 yarn workspace,需要配置根目录下的 package.json 以及 lerna.json

// package.json
{
  "private": true,
  "workspaces": ["packages/*"]
}

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

然后添加依赖等就可以用yanr workspace 的相关命令进行了

  • 将@demo/core作为@demo/plugin的依赖
yarn workspace @demo/plugin add @demo/core/1.0.0 # 这里必须加上版本号,否则报错
  • 仅安装在根目录
yarn add -W -D typescript jest

其他一些常用命令

// workspace 不受 Yarn Workspace 管理,只需在此 workspace 目录下添加 .yarnrc 文件
yarn config set workspaces-experimental true

// 指定 workspace 执行 command
yarn workspace <workspace_name> <command>

// 每个 workspace 下执行 <command>
yarn workspaces <command>

// 显示当前各 workspace 之间的依赖关系树
yarn workspaces info [--json]

Yarn 2.X 版本对于 workspace 又有了较大的提升

Lerna 实践

在具体实践中,得保证有完善的工作流,风格统一的结构,自动的更新日志

1. 风格统一的 commit 约束

lerna 的 version_bump 和 changelog 生成都依赖于 conventional-commit,因此需要保证 commit-msg 符合规范。 使用 commitizen 和 cz-lerna-changelog 来规范提交

yarn add -W -D commitizen  cz-lerna-changelog @commitlint/cli @commitlint/config-conventional husky standard lint-staged

安装完成后,在根 package.json 中增加 config 字段,把 cz-lerna-changelog 配置给 commitizen

{
    "name": "root",
    "private": true,
    "scripts": {
        "gc": "git-cz"  // 命令行工具
    },
    "config": {
        "commitizen": {
          "path": "./node_modules/cz-lerna-changelog"
        }
    },
    "husky": {
        "hooks": {
          "pre-commit": "lint-staged",
          "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
        }
    },
    "lint-staged": {
        "*.js": [
          "standard --fix",
          "git add"
        ]
    },
  }

同时在根目录下面创建 commitlint.config.js

// 在工程根目录为 commitlint 增加配置文件 commitlint.config.js 为commitlint 指定相应的规范
// commmitlint.config.js
module.exports = {
  extends: [
    "@commitlint/config-conventional"
  ]
};

2. 自动更新的 changelog

在根目录可以使用convention-commit 来发版,利用了上面 lerna version 的命令行

// package.json
{
  "scripts":
  {
    version: "lerna version --conventional-commits" ## 生成changelog文件以及根据commit来进行版本变动
  }
}

不过一般直接运行 publish,可以在 lerna.json 进行如下配置

"command": {
  "version": {
    "conventionalCommits": true
  },
  "publish": {
    "conventionalCommits": true, // 生成changelog文件
    "exact": true // 准确的依赖项
  }
},
"ignoreChanges": [
  "**/*.md"
],

Lerna 的使用痛点

由于源码在一起,仓库变更非常常见,存储空间也变得很大,甚至几 GB,CI 测试运行时间也会变长。即便如此,团队中任何人都不想回到 git submodules 多仓库的方式。

Lerna 使用中遇到的一些问题

1. 发布失败之后解决

  • 在你这次失败之后使用 from-git 参数,即 lerna publish from-git

  • 可以手动回退 git 到 release 之前的版本,并删除相应的 git tag ,如下:

git reset --hard HEAD~1 && git tag -d $(git log --date-order --tags --simplify-by-decoration --pretty=format:'%d' | head -1 | tr -d '()' | sed 's/,* tag://g')

2. 依赖包之间锁定版本的问题

  • 建议导入包的时候不是包含的,统一有业务方全部引入避免这个问题

总结

monorepo 感觉更适合内聚性比较强的工具类、框架类工程,例如react相关、 Angular相关,一般发布会有一系列的依赖同步更新;同时这种工程发展是可控的,不会随意膨胀,所以库的体积也是可控的。 业务类的工程代码管理感觉不适合用 monorepo, 一是业务代码膨胀很快,不可控,体积会变很大,迟早要拆; 二是用了 monorepo 自动化发布不好搞,业务一般是要求能独立发布,互不影响,多工程好搞自动化,只要关心repo地址就行,剩下的事情外部工具直接执行repo内的ci文件就行,monorepo 的话是不是要写很多文件夹的逻辑; 三是业务类的工程往往和组织架构相关,不同的一群人各做各的业务,monorepo 不好做细粒度的权限管理,目前 github gitlab 的权限管理只能到仓库级还不能到目录级;

参考

  1. github.com/lerna/lerna…

  2. zhuanlan.zhihu.com/p/77577415

  3. zhuanlan.zhihu.com/p/65533186