目录

3.使用Lerna管理你的代码仓库

目录

随着前端开发逐渐向着工程化、模块化、组件化的方向演变。越来越多的项目和工程都会采用拆分成独立的几个 package 的方法来共享模块、组件。

在以前开发 package 的时候,还是采用单一组件包相互独立的形式,这种形式就意味着每个包必须手动去变更版本号、去发版。如果遇到相互依赖的 package 还需要依次进行升级,其中的过程可谓是复杂而且不稳定。特别是当我们的目标是构建一个组件库时,会把组件库拆分成多个部分互相依赖,这样的话,维护直接的依赖关系肯定是一个重要的部分。

例如:你目前有一个项目,其中依赖了两个 package:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0645d16f4da74d5f9d26b1d9bfe224aa~tplv-k3u1fbpfcp-zoom-1.image

// 项目 package.json
{
   "name": "proj",
   "version": "0.0.1",
   "dependencies": {
      "apis": "0.0.1",
      "logs": "0.0.1"
   }
}

// 包 apis package.json
{
   "name": "apis",
   "version": "0.0.1",
   "dependencies": {
      "utils": "0.0.1",
   }
}

// 包 logs package.json
{
   "name": "logs",
   "version": "0.0.1",
   "dependencies": {
      "utils": "0.0.1",
   }
}

假如你改动了utils package 某个函数的逻辑,为了代码的准确性,我们必须按照以下步骤进行 package 的维护:

  1. 改动 utils 的版本号 0.0.1 -> 0.0.2, 并进行发版。
  1. 修改 apis 包的依赖的 utils 的版本号,并改动自身版本号至 0.0.2, 并发版。
  1. 修改 logs 包的依赖的 utils 的版本号,并改动自身版本号至 0.0.2, 并发版。
  1. 修改主项目的 apis 和 logs 的版本号,并发版。

整整需要四步才能整个对项目进行升级,而此处只是简单举例,假如项目更加复杂,依赖情况并不清晰,所造成的工作量还会成倍上升。

显然,这种 package 相互独立,用多个代码库维护一个依赖关系网的情况,导致了一些跨越多个代码仓库的更改变的极其不稳定且不可追踪。并不符合前端工程化的发展趋势,于是,一个新的概念 Monorepos (单一代码库) 诞生了 。

monorepos

单一代码库(monorepos)的概念就是将所有的代码集合进一个仓库管理,与之相对的就是我们上文提到的另一种结局方案:多代码库(multirepos)。

那么 monorepos 相对 multirepos 有什么优势和劣势呢?我们首先说下 monorepos 的优势:

  • 统一便捷性:所有相关的 package 包都会放在一个 repo 仓库里面。减少了下载不同 repo 的代码的繁琐工作,相关代码清晰明了集合在一起。
  • 依赖关系集中化:因为代码都会集合到一个仓库里面,相互之间的依赖关系更好的维护管理,寻找依赖也比较方便。
  • 代码标准一致性:可以采用统一的一套代码风格 lint 标准、Git commit 标准来约束所有 package 的代码风格,做到一致统一。
  • 统一的 CI/CD 流程:可以采用一套 CI/CD 流程来统一部署或者发版,减少了人为控制的不定因素,更加稳定可靠。

当然 monorepos 并不是没有缺点,如果把所有代码集合到一起,代码的体积和维护成本也会随之上升。每次拉取代码都不得付出更高的成本。

而且,对于 monorepos 的权限管理也是一个更有挑战性的事情,因为对于 monorepos 项目,git工具并没有针对这样的情况进行目录的权限划分,很容易造成一些安全隐患。并且对 code review 的工作也不是特别友好。

但是瑕不掩瑜,monorepos 依旧可以给我们管理多个 package 带来便捷和高效。 Google 和 Facebook 或者一些大型的框架或者组件项目依然对使用 monorepos 来管理模块 package 情有独钟。

monorepos 既然这么符合我们前端发展的趋势,那我们该如何把 monorepos 的理念带入到我们的具体工程中呢?这就不得不提到实现 monorepos 的工具链。目前用于在一个项目代码仓库里管理多个包的工具链也是遍地开花,其中我选两个比较有代表性的来举例说明。它们分别是 yarn workspaces 和 Lerna。

yarn 想必大家都不陌生了,著名的包管理工具,而 yarn workspaces 是近两年 yarn 增加的一项新功能,它通过对 monorepos 的核心理念和功能进行抽象和实现。

但是 Lerna 相对于 yarn workspaces 更进一步,提供了相应的脚本实现了复杂的包管理、发布、依赖自动更新等功能,可以说更加符合我们构建一个复杂项目的基本需求。所以我们在开发组件库这种需要管理多个 package 的时候,采用 lerna 来进行包管理再合适不过了。

Lerna

官网上对于 lerna 的定义是:

A tool for managing JavaScript projects with multiple packages.

简单的翻译一下就是:用来管理 js 多 package 项目的工具,上文也简单实现 monorepos 不错的工具链,那么它带来的特性如何?要如何使用这些提升我们的效率?我们接下往下看。

首先我们简单来看下 lerna 的一些特性:

  • 自动维护 packages 直接的依赖关系:这意味着对于多个包互相依赖的情况,无需手动升级各个包的版本,如文章开头我们举的例子。使用 lerna 后,可以简化掉 1.2.3 这三个步骤,只需执行下发布命令,就可以自动分析依赖,升级版本号。
  • Git 仓库检测 change, 自动分析发版:lerna 可以根据 Git change 记录,来自动分析你哪个 package 有改动,需要升级。我们只需关注我们的具体改动,剩下的发版,都可以直接交给 lerna 来处理。
  • 关联 commit 说明,生成 changelog:lerna 可以分析你的 commit 记录,并自动构建更新 changelog 文件,这对于一个大的组件库,生成记录是比较有用的。
  • 提供丰富的 指令集: lerna 提供了丰富的脚本指令集,可以用来执行 package 的脚本、增加依赖包、根据配置更新版本号、发布等等功能。为我们开发提供比较大的便利。

了解过以上特性后,我们来开始构建一个 Web Components 组件库,来看下怎么使用 lerna 来解决我们管理多个 package 包的问题。

  1. 首先我们来使用 lerna 初始化我们组件库的项目:
// 安装 lerna
npm install lerna -g

// 创建根目录 *给项目起个 帅气的名字 
mkdir sten-design
cd sten-design

// 用 lerna 初始化项目
lerna init

// logout
➜  sten-design lerna init
lerna notice cli v4.0.0
lerna info Initializing Git repository
lerna info Creating package.json
lerna info Creating lerna.json
lerna info Creating packages directory
lerna success Initialized Lerna files

可以看到初始化后目录为:

├── lerna.json
├── package.json
└── packages

其中 packages 文件夹就是用来存放子 package 的文件夹,是一个扁平结构。lerna.json 是用来声明 lerna相关配置,package.json 则作为最外层项目的包配置声明。

我们改动下 lerna.json 和 package.json:

// lerna.json
{
  "packages": [
    "packages/*"
  ],
  "npmClient": "pnpm",
  "useWorkspaces": true,
  "version": "independent"
}

// package.json
{
  "name": "root",
  "private": true,
  "devDependencies": {
    "lerna": "^4.0.0"
  },
  "workspaces": [
    "packages/*"
  ]
}
  1. 我们在packages 里面新建几个文件夹,用来作为模块来互相依赖,并用 npm init 初始化一下,现在结构如下:
├── lerna.json
├── package.json
└── packages
    ├── sten-components
    │   └── package.json
    ├── sten-icons
    │   └── package.json
    └── sten-themes
        └── package.json

我们先新建三个package,分别是components、 icons、themes,分别用来存放组件、icons、主题相关的代码。并且components 依赖 icons、themes。

lerna add "@sten-design/icons" --scope="@sten-design/components"

lerna add "@sten-design/themes" --scope="@sten-design/components"

{
  "name": "@sten-design/components",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "dependencies": {
     "@sten-design/icons": "1.0.0",
     "@sten-design/theme": "1.0.0",
  },
  "author": "",
  "license": "ISC"
}

这样我们的组件库就有一个比较原始的结构。

  1. 接下来我们执行 lerna 脚本 lerna bootstrap ,此脚本作用是:分析仓库内的依赖,引导当前 lerna 仓库中的包。安装所有依赖项并连接所有的交叉依赖。
➜  sten-design git:(master) ✗ lerna bootstrap
lerna notice cli v4.0.0
lerna info Bootstrapping 3 packages
lerna info Symlinking packages and binaries
lerna success Bootstrapped 3 packages

➜  sten-design git:(master) ✗ lerna list
lerna notice cli v4.0.0
@sten-design/components
@sten-design/icons
@sten-design/themes
lerna success found 3 packages

执行后可以看出,lerna 已经识别了三个 package 并记录了依赖关系。

我们来关联 git 仓库后执行一次 lerna publish 来模拟发布,会有以下信息:

lerna publish

lerna notice cli v4.0.0
lerna info versioning independent
lerna info Assuming all packages changed
? Select a new version for @sten-design/components (currently 1.0.0) Patch (1.0.1)
? Select a new version for @sten-design/icons (currently 1.0.0) Patch (1.0.1)
? Select a new version for @sten-design/themes (currently 1.0.0) Patch (1.0.1)

Changes:
 - @sten-design/components: 1.0.0 => 1.0.1
 - @sten-design/icons: 1.0.0 => 1.0.1
 - @sten-design/themes: 1.0.0 => 1.0.1
 
 // 会有一个list供你选择你要发布的版本,一路 yes 后,lerna 会自动把版本号升级
  1. 那么接下来我们试一下,如果我们改动了其中一个包,lerna 会不会识别我们的变动,并自动升级package.json 中的 version。

首先我们来改动 themes 中的代码:

├── lerna.json
├── package.json
└── packages
    ├── sten-components
    │   ├── node_modules
    │   └── package.json
    ├── sten-icons
    │   └── package.json
    └── sten-themes
        ├── index.js // 增加 index.js 并对外导出变量
        └── package.json

当我们再次执行 lerna publish 的时候可以发现:

? Select a new version for @sten-design/components (currently 1.0.1) Patch (1.0.2)
? Select a new version for @sten-design/themes (currently 1.0.1) Patch (1.0.2)

Changes:
 - @sten-design/components: 1.0.1 => 1.0.2
 - @sten-design/themes: 1.0.1 => 1.0.2

? Are you sure you want to publish these packages? (ynH)

这次只会展示出 themes 和 components 包需要升级,因为我们只改动了 themes 包,而且 components 包因为依赖 themes 包,所以也会被找出,并更新 package.json 相应的 themes 的版本并把自己进行升级。

  1. 我们来尝试下生成 changelog 日志,来执行下 lerna version ,这时候会发现在目录中多出了 CHANGELOG.md 来记录我们的 commit。
├── sten-components
│   ├── CHANGELOG.md
│   ├── node_modules
│   └── package.json
├── sten-icons
│   └── package.json
└── sten-themes
    ├── CHANGELOG.md
    ├── index.js
    └── package.json
    
// CHANGELOG.md
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## [ 1.0 . 4 ]( https://github.com/fanshyiis/sten-design/compare/@sten-design/components@ 1.0 . 3 ...@sten-design/components@ 1.0 . 4 ) ( 2022 - 01 - 23 )
**Note:** Version bump only for package @sten-design/components

以上使用的体验可以看到,lerna 在解决多个仓库 package 相互依赖的问题上有了比较合理高效的解决方案,它会自动分析依赖,决定哪些依赖需要升级,并自动帮我们升级好,做好changelog。并内置一些脚本指令,帮助我们发版。可以说规范了整个的流程,提升了我们的开发效率,并降低了很多由手动发版造成的错误。

那么以上就是 menorepos 和 lerna 的介绍,还有用 lerna 来管理我们组件库 menorepos 的基本实现。我们选定以 lerna 为管理工具从0开始建设我们的仓库,在接下来的课程中,我们会在此的基础上继续添砖加瓦,构建我们的组件库。大家拭目以待吧。