Skip to main content

Lerna + Yarn workspace: 管理 Monorepo

常见的前端项目,如果需要复用,可以把功能组件抽离到 utils/components/ 位置;如果有一部分功能组件可以在其他项目复用,那可以把这部分抽离成一个包,托管到 公共仓库 npm、Github,或私有仓库。

但是这些方法对于一个大型工程可能不是一个很好的解决方案,单独分离出一个复用包作为项目的主要缺点是无法保证实时和私有需求

例:当我修改完一个复用包时,需要重新发布。同时主项目要更新它的最新版本,重新拉取依赖。这些都要手动解决。

什么是 Monorepo

大型项目像后端 Maven、Gradle 那样存在多个子项目,复用包、组件也会作为一个子项目存在,供其他子项目使用,它们组织在一起作为整个项目,称为 Monorepo。大概有这种结构:

.git/
packages/
  icons/        项目图标库
  common/       项目通用库
  api/          项目协议和接口库
  console/      前端项目: 控制台项目,需要 icons、common、api
  web/          前端项目: web项目,需要 icons、common、api
  mobile/       前端项目: App端项目,需要 icons、common、api
  tools/        开发过程辅助工具
package.json

Monorepo 的好处是可以统一管理这些可复用但又局限于项目内的子包,同时保证子包的实时更新能立即作用于整个项目,常用的包管理工具会以符号链接的方式(类似快捷方式),把主项目和子包连接起来,对主项目来说,引用子包只是引用了 node_modules 里面的依赖。

Lerna 配合 Yarn 管理 Monorepo

Lerna 是一个 Monorepo 管理工具,可以把子项目连接起来;自动生成子项目的版本,发布公开的包。

Yarn 本身是个包管理工具,Yarn workspace 命令可以给子项目添加、移除依赖。

安装 Lerna、初始化 Monorepo

初始化工程,并在项目安装 Lerna:

yarn init
yarn add -D lerna

接着初始化一个 Monorepo 项目:

yarn lerna init

初始化完毕,项目根目录可以看到配置文件 lerna.json:

Lerna 要用包管理工具给子包安装依赖,默认是 npm:

{
  "$schema": "node_modules/lerna/schemas/lerna-schema.json",
  "useNx": true,
  "useWorkspaces": true,
  "version": "0.0.0",
+ "npmClient": "yarn"
}

配置、识别子包

package.json 定义的 workspaces 告诉 Lerna 和 Yarn,子包存放在哪里。

{
  ...
  "workspaces": [
    "packages/*"
  ]
}

Yarn workspace 增删子包的依赖

针对一个子包安装依赖要指明子包的名称,子包的名称定义在子包的 package.json name 字段。

现有一个私有子项目 icons

packages/
  icons/
    package.json -> { "name": "icons", "private": true }
lerna.json
package.json

根目录,给子包 icons 安装 lodash 依赖:

yarn workspace add lodash

根目录,给子包 icons 移除 lodash 依赖:

yarn workspace remove lodash

链接依赖: 子项目依赖子包

现有一个 web 需要依赖 icons,引用里面的图标资源。

packages/
  icons/
    package.json -> { "name": "icons", "private": true }
  web/
    package.json -> { "name": "web", "private": true, "dependencies": {} }
lerna.json
package.json

webpackage.json 手动添加:

// packages/web/package.json
{
  "name": "web",
  "private": true,
  "dependencies": {
+   "icons": "*"
  }
}

然后,根目录让 Lerna 执行 bootstrap。把 icons 链接成一个依赖,提供给 web

yarn lerna bootstrap
# 执行完后,web 可以直接以 node_modules 方式引用 icons 的资源了
# e.g. import MyIcon from 'icons';

运行构建、执行脚本

现增加一个子项目 console,也要引用 icons 资源。

同时 web 和 icons 开发到一段进度,加了 build 脚本简化构建。

packages/
  icons/
    package.json -> { "name": "icons", "private": true, "scripts": { "build": "..." } }
  web/
    package.json -> { "name": "web", "private": true, "scripts": { "build": "..." }, "dependencies": { "icons": "*" } }
  console/
    package.json -> { "name": "console", "private": true, "scripts": { "build": "..." }, "dependencies": { "icons": "*" } }
lerna.json
package.json

Lerna 可以执行子项目定义的脚本,如果一些子项目没有这个脚本,那么 Lerna 会跳过:

yarn lerna run build
# Lerna 会到 icons、console、web 目录,执行 build 脚本

让 Lerna 只执行某个子项目的脚本:

yarn lerna run build --scope=icons
# Lerna 只会到 icons 执行 build 脚本

如果子项目又依赖另一个子包,那么这个子包也会被执行脚本:

yarn lerna run build --scope=console
# Lerna 只会到 console 执行 build 脚本
# 但是 console 依赖了 icons,所以会到 icons 也执行一遍 build

Lerna 也支持并行构建:

yarn lerna run build --parallel
# Lerna 会并行执行 icons、web、console 的 build

子项目和独立项目

Monorepo 包含子项目,当这个 Monorepo 完成开发、修改后,会以整个 git 项目上传到版本控制。

一个 Monorepo 的版本会存在很多个子项目的修改。所以当私有子项目需要被另一个完整项目引用,那可以把这个子项目独立出一个完整项目作为版本控制,方便版本追溯和跨项目引用。

如果一部分复用代码的作用非常有限,那么建议还是局限到成一个 Monorepo 内,以子项目的形式提供依赖。

总结起来,Monorepo 类似 Java 后端项目,内部项目会互相依赖。Monorepo 的特性会在前端微应用体现出来(管理、部署多个微应用)。

附录

Lerna (https://lerna.js.org/)

Flutter Monorepo 管理工具 Melos (https://melos.invertase.dev/)