基于Lerna+Yarn的Monorepo工具库实战

lerna + yarn workspace 使用总结

「前言」


本文是笔者在学习和应用 lerna + yarn workspace 多包工程化管理模式的过程中,记录的一些使用和问题汇总,作为笔记和分享于大家阅读。
lerna 管理方式属于 Monorepo 模式,这有别于传统的 Multirepo 单仓库应用模式,下面我们先来了解一下两者的区别。

Multirepo

传统的项目开发模式,比如 create-react-app、vue-cli 等框架模板脚手架生成的项目。

- 优点:

    1. 各模块管理自由度高;
    2. 各模块仓库体积一般不会太大;

- 缺点:

    1. 仓库分散不好找,分支管理混乱;
    2. 版本更新频繁,公共模块版本发生变化,需要对所有模块进行依赖更新;

Monorepo

- 优点:

    1. 统一的规范、构建工具;
    2. 方便版本管理和依赖,模块之间的引用调试都变得非常方便;
    3.Multirepo 的缺点就是它的优点。;

- 缺点:

    1. 随着应用扩展,仓库体积将变大。

lerna + yarn workspace 应用场景

  1. 作为业务组件库的工程环境搭建。实现单个业务组件作为单独的 npm 包 进行管理和发布无需将各个业务组件分开建立在多个 Git 中且它们的技术栈、构建工具、规范等都可以保持一致。

  2. 作为日常业务项目工程管理。
    比如有一个低代码业务需求,低代码核心工作区的交互都相同,不同的是业务场景(外层壳子和一些定制功能),低代码相关的模块都可以复用,我们只需在这个仓库内不断去扩展业务需求即可,达到核心代码的复用(当然,可能会想到将低代码核心作为线上包)。

「Lerna」

Lerna 是一个管理工具,用于管理包含多个软件包(package)的 JavaScript 项目,是 Babel 自己用来维护自己的 Monorepo 并开源出的一个项目。

它可以:

  • 统一的一套规范、构建标准;
  • 对相互耦合较大、相互独立的 JS/Git 库进行管理;
  • 统一的工作流和 Code Sharing(代码共享)。

下面我们从以下几个方面来熟悉 Lerna:

  • Lerna 管理模式;
  • Lerna 入门指引;
  • Lerna 管理命令;
  • Lerna 配置文件;
  • Lerna 应用Demo;
  • Lerna 版本发布;
  • Lerna 最佳实践;
  • Lerna 注意事项。

「Lerna 管理模式」

lerna 管理项目可以使用两种模式,默认固定模式,当使用 lerna init -i 命令初始化项目时,此时为独立模式。(模式是用来管理多个 package 发包时的方式)

  1. 固定模式:
    在发包时会检测 packages 下涉及到变更的包,给这些变更的包使用同一版本,未发生变更的包不应用改版本,且不做发布升级;发布时可通过 lerna publish major(大) | minor(中) | patch (小)自定义版本。

  2. 独立模式(常用的模式):
    允许每个包有自己独立的版本号,在 lerna publish 发布时,需要为每个改动的库指定版本号(逐个询问需要升级的版本号)。此模式,lerna.json - version 字段指定为 independent。

如果 packages 下,其中一个包发生改动,另一个包依赖了这个包,即使它没有发生改动,也会被进行发布更新。

「Lerna 入门指引」

  1. 全局安装 Lerna:
npm install --global lerna


  1. 初始化 git 代码仓库:

git init lerna-repo && cd lerna-repo


不要在vscode乱推送,一定要先在github建仓库
git remote set-url origin git@github.com:xxxxx/lernaworker.git
git branch -M master
git push -u origin master

  1. 初始化 Lerna:

lerna init
// lerna info Creating package.json 
// lerna info Creating lerna.json 
// lerna info Creating packages directory 
// lerna success Initialized Lerna files



  1. 此时得到了这样一个仓库目录结构:

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



其中 packages 中保存着每个独立的包模块。

  1. 安装 lerna 到仓库 node_modules 中::

npm install

至此,我们就完成了一个 Lerna 工程的初始化工作,下面我们掌握一些操作命令来管理 Lerna。

「Lerna 管理命令」

  1. lerna init 将一个仓库初始化为 lerna 仓库(默认固定模式)。支持参数:

--independent/-i – 使用独立的版本控制模式

  1. lerna create 「package」 创建一个 package 到项目工程的 packages 下。:

lerna create 「package」
  1. lerna add 「module」:
  • 为每一个 package 安装特定依赖:
lerna add react

  • 为指定的 package 安装特定依赖:
lerna add react-dom packages/package1 
// or 
lerna add react-dom --scope=package1


  • 添加依赖到根目录 node_modules 中:
npm install typescript -D



  • package 之间的相互依赖(会在 package/package.json 下添加该依赖):
lerna add package2 --scope package1 
// or 
lerna add package2 packages/package1

注意 package 安装的相互依赖不会在当前子包产生 node_modules 我们可以在 根目录 lerna link 生成


  1. lerna publish 用于 npm 包版本发布,具体细节可看下文 「Lerna 版本发布」。

  2. lerna bootstrap 用于将 packages 链接在一起(前提是相互依赖的库),并安装 package 下的依赖到 package/node_modules。

注意,它不会安装根目录 package.json 的依赖,如果需要安装根目录依赖,请使用 npm install。

参数:

  • –hoist:依赖提升,把每个 package 下的依赖包都提升到工程根目录(删除包下的 node_modules,将依赖安装在根目录,但依赖注册不会在 package/package.json 内删除,也不会在 root/package.json 内添加此依赖)
  1. lerna clean
    删除各个包下的 node_modules(不会删除根目录 node_modules)。

  2. lerna ls
    列出当前 Lerna 仓库中的所有公共软件包(public packages)。

  3. lerna run 「script」

  • 运行每个包下面的 script(如果某个包没有此 script,将会报错)

lerna run test



  • 运行某个包下面的 script

lerna run test --scope package1



  1. lerna exec 「shell」 允许去执行 shell 脚本

lerna exec webpack

  1. lerna changed
    检查自上次发布以来哪些软件包被修改过。

  2. lerna link
    链接互相引用的库,当 pakcage/package.json 内明确了 packages 下的包时,才会将相关包链接到 package/node_modules 中。

  3. lerna info
    查看 lerna 及运行环境的信息。

「Lerna 配置文件」

在 lerna.json 配置文件内可以指定工作模式、packages 的位置以及一些命令的默认参数定义,如下示例:


{
  "version": "1.0.0",
  "npmClient": "yarn",
  "useWorkspaces": true,
  "packages": [
    "packages/*"
  ],
  "command": {
    "bootstrap": {
      "npmClientArgs": [
        "--no-package-lock"
      ]
    },
    "version": {},
    "publish": {
      "npmClient": "npm",
      "ignoreChanges": [
        "**/*.md",
        "**/test/**"
      ],
      "message": "chore(release): publish",
      "registry": "https://registry.npmjs.org",
      "conventionalCommits": true
    }
  }
}



  • version: 当前仓库的版本,Independent mode 请设置为 independent;

  • packages: 指定包所在的目录,支持指定多个目录;

  • npmClient: 允许指定命令使用的client, 默认是 npm, 可以设置成 yarn;

  • useWorkspaces: 使用 yarn workspaces 管理 Monorepo;

  • command.bootstrap.npmClientArgs: 指定默认传给 lerna bootstrap 命令的参数;

  • command.publish.ignoreChanges: 指定那些目录或者文件的变更不用触发 package 版本的变更;

  • command.publish.message: 执行发布版本更新时的生成的 commit message;

  • command.publish.registry: 指定发布到的 registry url,比如可以发布到指定私服,默认是 npmjs.org;

  • command.publish.conventionalCommits: lerna version 将生成 CHANGELOG.md files(如果设置了这个,lerna 管理模式将直接使用固定模式,version = independent 的配置将失效)。

「Lerna 版本发布」

packages 下的包版本发布需要使用 lerna publish,这个命令组合了这两个命令:lerna versionnpm publish
其中 lerna version 针对 Lerna 的管理模式(固定模式和独立模式),在表现上有所不同。
但主要工作还是在进行 npm publish 之前,去管理哪些包要进行发布,以及发布过程中生成的 Git commit、Git tag 的提交。

  1. 固定模式下的 lerna version:
  • 找出从上一个版本发布以来有过变更的 package;
  • 根据当前 lerna.json 中的版本生成新的版本号;
  • 更新涉及到变更 package 下的 package.json 版本号;
  • 更新 lerna.json 文件中的版本号;
  • 将 version 更新、生成的 CHANGELOG.md 文件带来的变动提交为一次 commit;
  • 基于这次 commit 为所有涉及到更新的 package 打上各自的 tag;
  • 推送 commit 和 tags 到远程仓库。
  1. 独立模式下的 lerna version:
  • 找出从上一个版本发布以来有过变更的 package;
  • 提示开发者为需要更新的 package 选择(一组 Version Select)要发布的版本号;
  • 更新到 package 下的 package.json version 版本号;
  • 如果 packages 下其他包有依赖这个包,那么这些包下的 package.json 中此包版本也会更新;
  • 将 version 的更新变动提交为一次 commit;
  • 基于这次 commit 为所有涉及到更新的 package 打上各自的 tag;
  • 推送 commit 和 tags 到远程仓库。

这里需要注意一下 lerna 查找包变更的逻辑:

在当前分支,找到最近一次 tag,将当前 commit 和 tag 进行比较,看哪些 package 下的文件发生了变更。

lerna publish
lerna publish semver
// semver bump [major | minor | patch | premajor | preminor | prepatch | prerelease]
lerna publish from git
lerna publish from-package

初次使用发布时可能会遇到以下一些问题和注意事项:

  1. 避免开发者自己去打 tag。
    lerna 发布时会自动生成 tag,并且查找更新是基于 tag 来识别的,避免开发者手动打上 tag 后,影响 lerna 查找变更,可能会造成一些变更包没有按照预期发布。

  2. 避免多条分支同时进行。
    在多条分支同时进行时,可能会生成相同的版本号,从而发生版本冲突。解决办法:分支开发者之前应提前约定好版本。

  3. lerna publish 中途发布失败,如何进行重发布。
    有时候发布可能会失败(比如 npm 没有登录、没有使用 npmjs 镜像源),再次运行 lerna publish 时,因为 tag 已经打上了,无法再查找到更新,进行包的发布。

可以采用下面两种发布方式:

  • 运行 lerna publish from-git。会将当前标签中涉及的NPM包再发布一次。(不会再更新package.json,只是执行npm publish);
  • 运行 lerna publish from-package。会将当前所有本地包中的 package.json 和远端 npm 比对,如果 npm 上不存在此包的最新版本,都执行一次 npm publish。

「Lerna 最佳实践」

目前业界使用最多的方案是:lerna + yarm workspace 结合的 Monorepo 方案,两者工作职责划分不同:

  • yarn 处理依赖安装工作(只想做好包管理工具);
  • lerna 处理发布流程。

此处内容可以在下文查看yarn workspace 使用指南。

「Leran 注意事项」

  1. 发布前,提交工作区的变更。 在发布前,需要提交工作区的文件变更,否则终端会收到下面报错信息:

lerna ERR! EUNCOMMIT Working tree has uncommitted changes, please commit or remove the following changes before continuing:

  1. 发布前,需使用 npmjs.org 镜像。 在发布前,如果 npm 设置的镜像源为淘宝镜像,需要切换回 npm 镜像

npm config get registry
npm config set registry https://registry.npmjs.org



  1. 如果要发布一个Scope包:
    Scope 是指具有“组织”的包,比如 Babel 的相关包都是这一格式:@babel/xxx,在发布一个具有 Scope 包时,需要确保Organization(组织)已在 npm 上创建,私有包需要收费,公共包则为免费。

在发布 Scope package 时,需要在 package.json 声明 access publish:

{
  "name": "@feu/tools",
  "publishConfig": {
    "access": "publish"  // 如果该模块需要发布,对于 scope 模块,需要设置为 publish
   }
 }

  1. 发布意外中断,进行重发布: 如果发布因为某些原因中断了,未发布成功,再次执行发布,会得到如下提示:

lerna success No changed packages to publish

但由于包并未成功发布到 npmjs 上,这时可以执行以下命令进行重发布:

lerna publish from-git
// or
lerna publish from-package


  1. independent 模式并未生效: 在 lerna.json 下指定了 version 为 independent,但是发布时却还是固定模式的流程,原因可能是 lerna.json 内配置了 conventionalCommits:
"command": {
  "publish": {
    "conventionalCommits": true
  }
}



可以将其配置移除得到解决。

  1. 固定模式如何自己指定版本: 当我们执行 lerna publish 时,lerna 会自定分配一个版本提供我们使用;但这个版本可能不是我们期望发布的版本;如何自己控制发布的版本呢,在发布时我们可以传递配置:

lerna publish major(大) | minor(中) | patch (小) 
lerna publish patch // 发布小版本


「yarn workspace」

对于Monorepo 的工程,使用最多的方式是 lerna 结合 yarn workspace 一起使用
因为 yarn 在依赖管理上做的非常不错,适合我们业务场景的依赖模块管理。
而 package 的发布工作依旧交由 lerna publish 来运转。

下面我们从以下几个方面来熟悉 yarn workspace:

  • yarn workspace 管理工程;
  • yarn workspace 管理命令;
  • yarn workspace 入门实战。

请移步上一编

「yarn workspace」

yarn workspace 管理工程

初始化工程的步骤和上面 lerna 的方式一样,与 lerna 不同的是,需要做以下配置:

  1. 在 lerna.json 中声明使用 yarn workspace 进行依赖管理:

{
  ...
  "npmClient": "yarn",
  "useWorkspaces": true
}
  1. 在 root/package.json 下必需包含 workspaces 数组,与 lerna.json 下的 packages 保持一致:
{
  "private": true, // 工作空间不需要发布
  ...
  "workspaces": ["packages/*"]
}

  1. yarn workspace 管理命令
    yarn 管理命令大致分为两类(容易混淆,这里先提及一下):
  • 处理工程下指定的包模块时使用:yarn workspace;
  • 处理工程根目录全局或所有包模块时使用:yarn workspaces。
  1. yarn install
    代替 npm install + lerna bootstrap 安装工程依赖。

它与 lerna bootstarp 不同的是:

  • yarn install 会将 package 下的依赖统一安装到根目录之下。这有利于提升依赖的安装效率和不同 package 间的版本复用(有些包是需要私有依赖的,而私有依赖会被多个包安装多次,而提升依赖可以解决这一问题)。
  • yarn install 会自动帮助解决安装(包括根目录下的安装)和 packages link 问题。
  1. yarn add 「module」
  • 为每个 package 都安装指定依赖:

yarn workspaces add react

为指定的 package 安装特定依赖:

yarn workspace package1 add react react-dom --save

注意,package1 一定是 packages/package1/package.json name 字段,有时候 package 的目录名和 name 字段不一致,要以 name 为准。

添加依赖到根目录 node_modules 中:

cd 根目录 yarn add @babel/core -D -W (-W 表示将依赖添加到 workspaces 根目录)

package 之间的相互依赖(会在 package/package.json 下添加该依赖):

yarn workspace package1 add package2

注意,当 package2 没有发布在 npmjs 上时,此时会报错:package2 not found;解决办法:显示指定 package2 的版本: yarn workspace package1 add package2@^1.0.0

在工程根目录下引入 packages/package 包:

yarn add package@^1.0.0 -W

yarn remove「module」

和上面 yarn add 命令格式相同,只需将 add 替换为 remove 即可。

yarn run 「script」

运行工程根目录下 script:

yarn test

运行指定包模块下的 script:

yarn workspace package1 run test

值得注意的是,命令虽然是在根目录下执行,但在执行文件中拿到的 process.cwd() 是 package 下的执行文件所在路径

运行所有 package 下的 script 命令:

yarn workspaces run test

注意,如果某个 package 下没有对应 script,将会终止命令,并报错。若 package 不具备 script,可以定义一个占位 script,类似如下:

"scripts": { "lint": "echo lint successful." }

yarn workspaces info
查看 workspace 依赖树信息。

yarn workspace 入门实战

  1. 新建 yarn workspace 工程:
git init yarn-demo && cd yarn-demo && yarn init -y && yarn add lerna -D && lerna init

  1. 配置 lerna.json 改用 yarn workspaces:

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


  1. 根目录 package.json 必须包含一个 workspaces 数组:

{
  "private": true, // 工作空间不需要发布
  "workspaces": ["packages/*"],
  ...
}



  1. 新建两个 package:

cd packages && mkdir yarn-module1 && cd yarn-module1 && yarn init -y
cd packages && mkdir yarn-module2 && cd yarn-module2 && yarn init -y




  1. 添加几行测试代码:

// yarn-module1/index.js
function yarnModule1() {
  console.log('yarn-module1');
}

module.exports = yarnModule1;

// yarn-module2/index.js
const yarnModule1 = require('yarn-module1');

function yarnModule2() {
  console.log('yarn-module2');
}

yarnModule1();
yarnModule2();

module.exports = yarnModule2;



  1. 为 yarn-module2 添加个 script:
// yarn-module2/package.json
"scripts": {
  "test": "node index.js"
}
  1. 回到根目录执行 script:

   yarn workspace yarn-module2 run test

不出意外,会得到如下错误:

Error: Cannot find module ‘yarn-module1’

  1. 建立 package 之间的关系:

   yarn install

可以看到,根目录下的 node_modules 中已经存在了 yarn-module1 和 yarn-module2 这两个包,与 lerna 的区别在于没有在各自的 package 下创建 node_modules,而是统一链接到根目录。
但,yarn-module2 中依赖的 yarn-module1,应该将其添加到 package.json 中,最好的方式是采用:


   yarn workspace yarn-module2 add yarn-module1@^1.0.0


  1. 再来一次 script:
yarn workspace yarn-module2 run test

输出: 
yarn-module1
yarn-module2
  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值