lerna 和 yarn 实现 monorepo

4,629 阅读7分钟

环境要求

  • yarn v1.x
  • Node.js >= v10.12.0
  • lerna 3.x

什么是 monorepo?

monorepo 是指一种把多个项目的源代码放在同一个代码仓库里面管理的一种方法。与 monorepo 相对的是 multirepo,它的思想是按模块分成多个仓库。当前有很多流行的开源项目使用 monorepo 管理代码,如 BabelVue3.0

monorepo 的优缺点

优点:

  • 代码复用更容易;
  • 精简依赖管理,去除多个包的重复依赖;
  • 方便大规模的重构,可以同时重构多个包然后统一发布,避免不同包管理的问题;
  • 跨团队合作更容易,跨团队开发时只需提交代码即可合作,而不需要发布到包管理中心;
  • 方便统一管理lint、test、build 和 release;
  • 统一的地方处理 issue;
  • 方便统一生成 ChangeLog。

缺点:

  • 丢失版本信息,统一发布版本的 monorepo,会丢失各自项目的版本信息,尽管也可以单独发布版本;
  • 缺乏针对包的权限控制(Git),只要需要访问一个项目的,就必须要给开发者授权整个仓库,可能存在安全问题;
  • 消耗更多硬件资源:当你只需要开发某个项目时,必须要把整个仓库代码全部下载,会占用更多的网络和本地存储空间。

如何实现 monorepo?

目前比较主流的方案是采用 yarn workspaces + lerna 来实现。

yarn workspaces

yarn workspaces 可以将多个包的 node_modules 整合成一个, 只需要执行 yarn install 就可将所有包的依赖安装。

下面以一个例子来说明:

root/
  workspace-a/
    package.json
  workspace-b/
    package.json
  node_modules/
  package.json

根目录 package.json 文件中声明了 workspaces:

{
  "private": true,
  "workspaces": ["workspace-a", "workspace-b"]
}

注意,添加 private: true 是防止工作区根目录被发布到 npm,workspaces 是一个数组,其中包含所有“工作空间”的路径,也可支持通配符,如 "workspaces": ["packages/*"]

workspace-aworkspace-bpackage. json 分别如下:

workspace-a/package.json:

{
  "name": "workspace-a",
  "version": "1.0.0",

  "dependencies": {
    "cross-env": "5.0.5"
  }
}

workspace-b/package.json:

{
  "name": "workspace-b",
  "version": "1.0.0",

  "dependencies": {
    "cross-env": "5.0.5",
    "workspace-a": "1.0.0"
  }
}

最后在根目录运行 yarn install 即可安装所有依赖,安装后会有一个类似这样的目录结构:

/package.json
/yarn.lock

/node_modules
/node_modules/cross-env
/node_modules/workspace-a -> /workspace-a

/workspace-a/package.json
/workspace-b/package.json

workspace-b 依赖 workspace-a,将直接引用当前项目中内部的文件,而不是从 npm 去取。且不会有各自的 node_modules/,统一在根目录下的 node_modules

lerna

A tool for managing JavaScript projects with multiple packages. 一个用来管理有多个包的 JS 项目的工具。

以上是 lerna 的官方介绍。 lerna 用来管理多个包,优化维护多包的工作流,解决多个包互相依赖及发布的问题。目前已被许多著名的开源项目使用,如 babelcreate-react-appmint-uireact-router

一个 lerna 项目的结构大致如下:

lerna-repo/
  packages/
    package-1/
      package.json
    package-2/
      package.json
  package.json
  lerna.json

packages/ 目录下存放所有的包,lerna.json 是它的配置文件,后面会介绍。

lerna 如何工作

lerna 管理包有2种模式 fixed 和 independent。

Fixed/Locked mode 这是默认的模式,Fixed 模式下所有的包共用一个版本号,这个版本号保存在 lerna.jsonversion 字段里面。所以当你运行 lerna publish,修改过的包会自动更新 package.json 里的 version 并发布到 npm,未修改的包则不会更新。还有一个问题值得注意,当 lerna.jsonversion 做了主版本号的改动时,所有的包都会更新版本并发布。

注意:如果你的主版本号是 0,所有的修改都会当成是 breaking,表示做了不向下兼容的大改动,这会导致所有包的版本都会更新。

Independent mode lerna init --independent 用来初始化一个 independent 模式的项目,你也可以手动修改 lerna.json 里的 versionindependent 使用该模式。

independent 模式允许每个包自行更新版本号,当你运行 lerna publish 时,需要逐个选择修改过的包的版本。

lerna 常用命令

lerna init

创建一个新的 lerna 仓库或更新已有仓库为新版本的 lerna,其中的选项 --independent/-i 用来生成 independent 模式的项目。

lerna bootstrap

此命令会做以下几个事情:

  1. npm install 为所有的包安装依赖。
  2. 为互相依赖的包创建软链接。
  3. 在所有 bootstrap 包(不包括 command.bootstrap.ignore 中忽略的包)中执行 npm run prepublish(如果传了参数 --ignore-prepublish 将跳过此步骤)。
  4. 在所有 bootstrap 包(不包括 command.bootstrap.ignore 中忽略的包)中执行 npm run prepare

lerna publish

发布所有修改过的包,会在终端提示(prompt)选择一个新版本,并会更新所有改动到 Git 和 npm.

lerna run [script]

在所有包中执行特定的 npm script

lerna ls

列出当前仓库中的所有公共包(public packages),private: true 的包不会列出。

lerna.json

lerna.json 内容大致如下

{
  "version": "1.1.3",
  "npmClient": "npm",
  "command": {
    "publish": {
      "ignoreChanges": ["ignored-file", "*.md"],
      "message": "chore(release): publish",
      "registry": "https://npm.pkg.github.com"
    },
    "bootstrap": {
      "ignore": "component-*",
      "npmClientArgs": ["--no-package-lock"]
    }
  },
  "packages": ["packages/*"]
}
  • version: 当前仓库的版本。
  • npmClient: 使用的 npm 客户端,默认是 "npm",可选值还有 "yarn"。
  • command.publish.ignoreChanges: 是个数组,在这个数组里面的文件变动,不会触发版本更新。
  • command.publish.message: 自定义发布新版本时的 git commit 信息。
  • command.publish.registry: 设置私有仓库,默认是发布到 npmjs.org
  • command.bootstrap.ignore: 设置在这里的目录将不会参与 lerna bootstrap
  • command.bootstrap.npmClientArgs: 执行 lerna bootstrap 时会将此数组的所有值当作参数传给 npm install
  • command.bootstrap.scope: 限制 lerna bootstrap 在哪些包起作用。
  • packages: 用以指明所有包的路径。

项目实践

搭建

开启 yarn workspaces:

yarn config set workspaces-experimental true

创建项目:

mkdir lerna-demo && cd lerna-demo
yarn init

全局安装 lerna:

npm install --global lerna

lerna 初始化:

lerna init

就会在当前目录下生成以下文件:

lerna-demo/
  packages/
  package.json
  lerna.json

配置根目录下的 lerna.json 使用 yarn 客户端并使用 workspaces:

// lerna.json
{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0",
+ "npmClient": "yarn",
+ "useWorkspaces": true
}

package.json 设置 privatetrue,防止根目录被发布到 npm,还要设置 workspaces 目录:

// package.json
{
  "name": "lerna-demo",
+ "private": true,
+ "workspaces": [
+    "packages/*"
+ ]
}

新增包

新增包需要在 packages/ 下创建目录:

cd packages
mkdir wdm-lerna-demo-core && cd wdm-lerna-demo-core

使用 yarn 初始化:

yarn init

新包的初始化版本应当是 0.0.0,因为是用 lerna 更新版本,发布时会在此基础上新增。

同样操作再创建包 wdm-lerna-demo-ui,最后目录结构大致如下:

lerna-demo/
  packages/
    wdm-lerna-demo-core/
      package.json
    wdm-lerna-demo-ui/
      package.json
  package.json
  lerna.json

安装公共依赖

如果是所有的包公用的依赖,可以像这样安装:

lerna add lodash

执行完上面命令会在所有包的 dependencies 中加入依赖,如果是 dev 的公用依赖,最好是加到根目录的 devDependencies

yarn add eslint --dev -W

-W 选项显式指明在 workspace 的根目录执行,避免在根目录误操作 yarn。

安装本地包依赖

此示例中我们设定 ui 依赖 core

lerna add wdm-lerna-demo-core --scope=wdm-lerna-demo-ui

执行完后 wdm-lerna-demo-ui 会增加新的 dependencies:

// package.json
{
  "name": "wdm-lerna-demo-ui",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "wdm-lerna-demo-core": "^1.0.0"
  }
}

然后就可以在 ui 中使用 core:

import myCore from 'wdm-lerna-demo-core'

这里是通过软链接到 wdm-lerna-demo-core/ 目录,并不是使用从 npm 下载的包。所以,在 core 中的修改可以实时地在 ui 中反映。

移除依赖

如果你想要移除一个被所有包依赖的公共包,可以这样操作:

lerna exec -- yarn remove lodash
# lerna exec -- yarn remove lodash --network-timeout=1000000 # 如果提示网络有问题用此命令

lerna exec -- <command> [..args] 表示在所有包中执行该 command.

发布到 npm

首先检查是否已登录到 npm:

# 打印出用户名表示已登录
npm whoami

如果未登录,则执行以下命令按提示操作:

npm login

登录后即可发布:

lerna publish

这里的 lerna publish 会做以下几件事情:

  • 让你选择如何更新版本,是 major、minor 或是 beta。
  • 更新版本到有改动的包,即修改 package.jsonversion
  • 如果有某些包依赖刚才更新的包,自动更新 dependencies 的版本号。
  • 把改动提交到 git 并生成以版本号命名的 git commit 和 tag。
  • 发布刚才改动的并且是 public 的包到 npm。

至此,一个 lerna 项目搭建完成,此处源代码见 lerna-demo