几分钟了解前端 Monorepo - Lerna 的使用

4,513 阅读12分钟

什么是 Monorepo

Monorepo 是一种代码组织思想,它要求我们只用一个代码仓库来管理一个大项目的所有资源、子项目。简单来说以前放在多个 repo 里的代码现在就放在一个 repo 就行了。

而 Multirepo (传统多 repo 项目)会将一个项目按照职责、业务模块进行拆分,然后创建不同的代码仓库进行管理。不同的团队可以专注于负责某一个代码仓库代码提交、编译、发布。

优点

  • 可以看到所有代码,其他项目的新提交发生后也能立刻看到。
  • 便于协同工作,不会被依赖的项目发布而耽误进度,依赖项一提交马上可用。
  • 代码重用能力提升了。内部可以很容易相互引用,一次提交,repo 中依赖于此的项目即刻同步到最新依赖。
  • 公共依赖只要安装一次,Monorepo 中所有模块都共享,也不会有多个项目依赖版本不一致的问题。
  • 统一测试、避免项目依赖导致的跨 repo 测试变得复杂。
  • 统一 CI、CD 流程,一套配置解决所有模块的测试及发布。

缺点

  • 代码规模变得巨大后会造成代码加载,版本控制等的性能问题,可能造成代码编辑器加载不成功,或加载缓慢
  • 代码规模变大,大量提交、分支、tag,版本控制变得困难。比如想要通过提交日志进行 revert 操作,或 blame 等。
  • 由于项目间随意互相引用造成耦合性增加,由于代码放都放一起,可能也会造成项目间职责不明确,代码组织性降低
  • 权限问题,所有代码都可见。

谁在用 Monorepo

  • BabelReactAngularEmberMeteorJest,都是使用单一库进行管理的。
  • 谷歌有着 86 TB 超过 20 亿行的代码,谷歌在 2015 年就开始大规模使用 monorepo。
  • 还有诸如 Facebook、Twitter、Airbnb 等知名国外公司也在使用 monorepo。

如何决定是否是否 Monorepo

  • 微服务共享依赖库代码时,依赖需要统一,并独立拷贝到不同容器时选择 Monorepo。
  • 使用 CLI 工具库开发自动化测试框架时,一个自动化测试框架可以测试所有项目。
  • 在多项目开发过程中,项目职责划分明确,有不同的测试、发布流程时,就不用考虑使用 Monorepo。

git 中使用 Monorepo 的困难

  • git 在每次提交时候都会跟踪整个提交树的状态,由于项目增多,这会让 git 运行变得缓慢。
  • git 的 tag 将变得没有太大作用,比如 repo 中一个安卓项目的发版 tag 对于 repo 中的 web 项目而言是没有实际意义的。
  • 由于 git 底层使用有向无环图实现历史记录。提交的激增导致 git log, git blame 变得缓慢。
  • git 分支也会由于项目激增而激增,这些也会拖慢 git branch 的运行。
  • 大文件存储会影响整个 repo 的性能(这在 git LFS (Large File Storage)中有得到解决)。
  • git clone、git fetch、git push、git status 等都会因为文件数激增变得缓慢。

在前端使用 Lerna 进行 Monorepo 项目管理

通过 git 来管理 monorepo 的确在代码规模变得巨大之后会产生许多困难,但是它并没有阻止前端世界对 monorepo 的向往。由于 monorepo 的代码管理思想,基于 monorepo 的工程管理也出现了许多工具软件,比如 RushNXBitLerna

Lerna 是一个使用 git+ npm 进行 javascript monorepo 项目管理的开源软件。Lerna 的诞生是为了解决 Babel 的多包问题,以优化使用 git 和 npm 管理多包存储库的工作流程。

一个简单的 Lerna 项目文件结构如下:

|--my-lerna-repo
|  |──package.json
|  |──lerna.json
|  └──packages
|     |──package-1
|     |  └──package.json
|     └──package-2
|        └──package.json

关键思想

一个 git repo,多个 js 项目

所有曾经在不同 repo 的 js 项目现在都放在一个 repo 了,如上面文件夹所展示,都放到 packages 目录下了。这些 repo 可以通过 Lerna 提供的能力可以直接进行相互引用。代码提交操作和在一个 repo 中提交没有区别,即一个提交可能包含多个子项目的修改。

共享的 node_modules

通过 Lerna 提供的能力(依赖提升)使得 node_modules 依赖只需要安装一次,所有的子项目的依赖都会安装到项目根目录。

统一版本发布管理

Lerna 提供自动版本号递增、自动发布、修改 changelog、打 release tag的功能。一个发布命令自动发布所有 repo 下产生变化的子项目。也可以通过 Lerna 发布模式可以自主决定是自动发布所有当前已有更新、还是自主选择要发布的产生变化的子项目。

统一触发命令

通过配置根目录 package.json 中的 script 命令,使用 Lerna 运行命令会触发所有子项目中配置的同名命令。假设子项目中 A 项目有一个启动服务器的命令 start,B 项目有一个启动网页的命令 start。那么使用 Lerna 运行 start 命令则会自动运行 A 和 B 项目的指令。配置的其他命令如 test、publish 同理。

几个常用的 Lerna 命令

这里只是简单说下主要功能,详情可以去 Lerna 官网查阅。

  • lerna init 创建一个新的的 lerna repo。生成一个目录,其中包含 lerna.json 和 pacakge.json,以及一个空的 packages 文件夹。
  • lerna clean 在子项目运行 npm install xxx 依赖后会生成 node_modules 文件夹, 在 lerna repo 中只需要子项目的 package.json 中有依赖的声明就行了。在 lerna 中提供共享 node_modules 的能力,所以子项目中不需要有 node_modules, 通过下文的 lerna bootstrap 命令可以统一安装依赖,并实现子项目依赖共享。通过 lerna clean 指令可以一下子删除所有子项目中的 node_modules 文件夹(在下文实践示范中会进行使用演示)
  • lerna bootstrap 这个命令会安装各个子项目 package.json 中声明的依赖,并通过软链链接子项目的相互依赖关系(例如子项目中 A 项目被 B 项目依赖了,那么经过 lerna bootstrap 后会在 repo 根创建一个软链 /node_modules/A 指向 A 项目的文件夹,就像安装了 A 项目一样)。这个操作后 A 项目安装的所有依赖也可以直接被 B 项目直接引用到。这个命令可以指定使用 npm 或 yarn 作为包管理工具。
  • lerna version 这个命令会甄别出自上次发布版本以来现有各子项目的 git 本地提交(统一管理一个待发布版本的提交信息),并针对这些代码更新进行 npm package.json 版本号的递增(子项目有修改才会递增,无则忽略)、修改 changelog、创建 git release tag、创建提交并推送到 git 服务器。
  • lerna publish 运行这个命令会发布所有子项目 git 现在未发布的提交到 npm (这个命令内部同时也会执行 lerna version 一样的操作)。这个命令会触发 npm 发布相关的生命周期函数,可以在 lerna.json 中声明对应的回调以执行需要的的过程。
  • lerna run 类似于 npm run, lerna run 将执行所有子项目中定义在 package.json 中的同名的命令。比如所有子项目都定义了 test 这个命令,lerna run test 将执行所有子项目的测试。

实践示范

这里展示使用 lerna 来创建和管理一个 monorepo 项目,这个项目演示一个传统的前后端分离的项目,包含一个前端 web app,一个后端服务,一个业务通用模块。

创建 lerna monorepo

新建一个工程目录 my-lerna-project, 在该目录执行 lerna init

mkdir my-lerna-project
cd my-lerna-project
lerna init --yes

执行完 lerna init 后 my-lerna-project 下会生成如下结构

my-first-lerna
├── lerna.json
├── package.json
└── packages

打开 lerna.json,内容如下:

{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0"
}

这里 packages 字段表示的含义是所有被 lerna 管理的子 package。这里配置 packages/* 意思是packages 目录下的所有项目(未包含在声明列表里的项目,当运行 lerna 相关指令时,那些项目不受影响,配置的命令也不会得到执行)。

package.json 文件和 npm package.json 没有区别,用于声明整个 repo 的依赖。

创建子项目

首先在 packages 目录下创建一个 react 项目作为我们的前端页面。 通过 create-react-app 脚手架创建好项目后,已经配置好 start、test 等命令了。

cd ./packages
npm init react-app my-web-app

npm start

,验证运行 npm run start 可以运行起来页面。

image.png

接着在 package 目录下创建一个 node expree 项目作为后端服务。通过 create-express-app 脚手架创建好项目后,已经自动配置好了 start、test 等命令了。

cd ./packages
npm init express-app my-backend

,验证运行 npm run start 可以运行起来页面。脚手架示例提供默认相应 GET 请求返回 {hello:'world'}。

image.png

提升 node_modules

到这里为止,我们的 repo 已经有了两个独立的子项目。 image.png 现在我们先使用 lerna clean 来清理子项目中的 node_modules 安装。

lerna clean
lerna notice cli v4.0.0
lerna info Removing the following directories:
lerna info clean packages\my-backend\node_modules
lerna info clean packages\my-web-app\node_modules
? Proceed? Yes
lerna info clean removing 
\my-first-lerna\packages\my-backend\node_modules
lerna info clean removing 
\my-first-lerna\packages\my-web-app\node_modules
lerna success clean finished

lerna bootrap --hoist 来将依赖安装到根目录以达成子项目共享 node_modules。

lerna bootstrap --hoist
lerna notice cli v4.0.0
lerna info Bootstrapping 2 packages
lerna info Installing external dependencies
lerna info hoist Installing hoisted dependencies into root
lerna info hoist Pruning hoisted dependencies
lerna info hoist Finished pruning hoisted dependencies
lerna info hoist Finished bootstrapping root
lerna info Symlinking packages and binaries
lerna success Bootstrapped 2 packages

经过依赖提升后,repo 目录结构如下:如果子项目的依赖包版本有不同,那么相同版本的包会提升到最外层。子项目下的 node_modules 会保留自己独有的依赖版本。并不是所以依赖都会提升到外层,例如 devDependencies 提供的可执行文件仍然会留在子项目的 node_modules 下面,以保证子项目下能正确运行可执行文件。

image.png

可以看到 my-backend 和 my-web-app 中依然有 node_modules 文件夹,但它们实际只包含了可执行文件了,react 和 express 相关的依赖都提到外层 node_modules 了。

image.png

  1. 配置测试、运行命令 通过脚手架搭建的项目已经默认配置了测试和运行命令了,我们将子项目中的 test 命令修改为以下以方便验证:
// my-web-app/package.json
... ,
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "echo tests running in my-web-app",
    "eject": "react-scripts eject"
  },
...

// my-backend/package.json
... ,
  "scripts": {
    "start": "node ./bin/app",
    "test": "echo test running in my-backend-app"
  },
...

现在修改根目录的 package.json, 配置 lerna 的指令

...,
"scripts": {
    "start": "lerna run start",
    "test": "lerna run test"
 },
...,

现在执行 npm run test 会看到两个项目的测试都得到执行了,如下:

 npm test 

> test
> lerna run test

lerna notice cli v4.0.0
lerna info Executing command in 2 packages: "npm run test"
lerna info run Ran npm script 'test' in 'my-backend' in 1.4s:

> my-backend@1.0.0 test
> echo test running in my-backend-app

test running in my-backend-app
lerna info run Ran npm script 'test' in 'my-web-app' in 1.3s:

> my-web-app@0.1.0 test
> echo tests running in my-web-app

tests running in my-web-app
lerna success run Ran npm script 'test' in 2 packages in 1.4s:
lerna success - my-backend
lerna success - my-web-app

运行 npm run start 也是一个道理,前端和后端服务都会启动起来。这样在子项目联调时候就可以一个指令搞定了。

  1. 提交一个版本 我们首先需要将代码变更进行 git 提交,然后使用 lerna 进行统一发布版本管理。
git add .
git commit -m "first commit"

lerna version

运行 lerna version 后命令行会提示新版版本号进行选择(为本次所有的修改自增一个版本号), 选择对应的版本号后,lerna version 命令会将自动创建对应版本版本号的 tag,并自动 push 到 git 远端服务器。

image.png

在 git 远端 web 管理页面可以看出来运行 lerna version 后 packakge.json 中 version 号的递增。

image.png

lerna version 命令会智能的检查出目前发生更改的项目,并为这些有更改的项目统一管理的版本号。例如 A(1.0.1),B(1.0.2)中只有 B 进行了代码修复而更新了代码,那么运行 lerna version 后只有 B 的版本号会递增到 1.0.3, A 则不会发生变化。 如果 A 和 B 同时发生了代码修改,那么 运行 lerna version 之后 A 和 B 都会递增到相同的 1.0.3 版本(在 lerna independent 模式又有所不同,可以参考官网说明)。

可使用 lerna version --converntional-commits ( Conventional Commits 定义) 来发布新版本,这个命令会自动修改 changelog.md 来反映版本变化。如果我们按照 conventional commits 规范来填写提交日志,那么 changelog.md 也会自动产生对应的提交记录。 比如我们进行了如下的提交:

 git commit -m "feat: add new funciton 1"
 git commit -m "fix: fix funciton 2 bug" 
 git commit -m "perf: optimize funciton 3"

那么 changelog.md 也会自动记录如下:

image.png

  1. 发布 npm 包 以上展示了如何通过 lerna 创建和提交 monorepo 项目。如何我们开发的是应用以上命令已经基本够用了,但是如果我们开发的是像 babel 一样的 npm 包,那么我们还需要学会使用 lerna 来进行包的发布。关于如果发布 npm 包可以参考这篇文章 怎么发布一个 npm 包到 npm 官方 registry

如果我们在提交代码后同时想要发布到 npm registry 那么我们可以在 git 提交代码后直接运行 lerna publish, lerna publish 会自动在内部运行 lerna version 进行版本号递增、打 tag 、push 远程 git 服务器等操作。此外,lerna publish 还会在内部自动运行 npm publish, 将新产生的版本推送到 npm registry (这些能工作的前提是我们已经配置好了 npm 和 git 的认证信息)。

总结

简单说了下 monorepo 是个啥,lerna 是个啥,以及 lerna 的基本使用方式。想要在前端使用 monorepo,用 lerna 还是挺方便的。基本操作了解了,就是用的时候遇到问题去扒官方文档了。

参考:

www.perforce.com/blog/vcs/wh… www.atlassian.com/git/tutoria… medium.com/@mattklein1… about.gitlab.com/direction/m… blog.bitsrc.io/11-tools-to…