基于 lerna 和 yarn workspace 的 monorepo 工作流

基于 lerna 和 yarn workspace 的 monorepo 工作流

由于 yarn 和 lerna 在功能上有较多的重叠,我们采用 yarn 官方推荐的做法,用 yarn 来处理依赖问题,用 lerna 来处理发布问题。能用 yarn 做的就用 yarn 做

可以避免管理很多仓库

使用 monorepo 的原因

我正在开发的项目 A,依赖了已经线上发布的项目 B,但是随着项目 A 的不断开发,又需要不时修改项目 B 的代码(这些修改暂时不必发布线上),如何能够在修改项目 B 代码后及时将改动后在项目 A 中同步? 在项目 A 发布上线后,如何以一种优雅的方式解决项目 A,B 版本升级后的版本同步问题?

为了解决

  1. 多个库之间共享依赖

  2. 库与库之间相互依赖的问题

  3. 测试、构建、lint、commit、发布等脚本共享

  4. 手动改版本号的问题

monorepo 最主要的好处是统一的工作流和Code Sharing。比如我想看一个 pacakge 的代码、了解某段逻辑,不需要找它的 repo,直接就在当前 repo;当某个需求要修改多个 pacakge 时,不需要分别到各自的 repo 进行修改、测试、发版或者 npm link,直接在当前 repo 修改,统一测试、统一发版。

16c6ba2a8a6ce740.png

monorepo 方案的优势

  1. 避免重复安装包,因此减少了磁盘空间的占用,并降低了构建时间;

  2. 内部代码可以彼此相互引用;

  • 代码重用将变得非常容易

  • 依赖管理将变得非常简单:同理,由于项目之间的引用路径内化在同一个仓库之中,我们很容易追踪当某个项目的代码修改后,会影响到其他哪些项目。通过使用一些工具,我们将很容易地做到版本依赖管理和版本号自动升级;lerna

monorepo 方案的劣势

  • 项目粒度的权限管理变得非常复杂:这意味着 A 部门的 a 项目若是不想被 B 部门的开发者看到就很难了。

  • 新员工的学习成本变高:不同于一个项目一个代码仓库这种模式下,组织新人只要熟悉特定代码仓库下的代码逻辑,在 monorepo 策略下,新人可能不得不花更多精力来理清各个代码仓库之间的相互逻辑,当然这个成本可以通过新人文档的方式来解决,但维护文档的新鲜又需要消耗额外的人力;

当多个子项目放在一个代码仓库,并且子项目之间又相互依赖时,我们面临的棘手问题有两个:

  1. 如果我们需要在多个子目录执行相同的命令,我们需要手动进入各个目录,并执行命令; yarn worksapces

  2. 当一个子项目更新后,我们只能手动追踪依赖该项目的其他子项目,并升级其版本。 lerna

依赖管理

monorepo: 各个库之间存在依赖,如 A 依赖于 B,因此我们通常需要将 B link 到 A 的 node_module 里,一旦仓库很多的话,手动的管理这些 link 操作负担很大,因此需要自动化的 link 操作,按照拓扑排序将各个依赖进行 link

解决方式:

通过使用 workspace,yarn install 会自动的帮忙解决安装和 link 问题(https://github.com/lerna/lerna/issues/1308

$ yarn install # 等价于 lerna bootstrap --npm-client yarn --use-workspaces

在某一个 workspace 下执行 yarn install 和在 root 下执行 yarn install 的效果是一样的

依赖清理

在依赖乱掉或者工程混乱的情况下,清理依赖

  • 普通项目:直接删掉 node_modules 和编译之后的产物即可

  • monorepo: 不仅需要删除 root 的 node_modules 的编译产物还需要删除各个 package 里的 node_modules 以及编译产物

解决方式:使用 lerna clean 来删除所有的 node_modules,使用 yarn workspaces run clean 来执行所有 package 的清理工作

$ lerna clean # 清理所有的node_modules
$ yarn workspaces run clean # 执行所有package的clean操作

安装/删除依赖

monorepo: 一般分为三种场景

  1. 给某个 package 安装依赖:yarn workspace packageB add packageA 将 packageA 作为 packageB 的依赖进行安装

  2. 给所有的 package 安装依赖: 使用 yarn workspaces add lodash 给所有的 package 安装依赖

  3. 给 root 安装依赖:一般的公用的开发工具都是安装在 root 里,如 typescript,我们使用 yarn add -W -D typescript 来给 root 安装依赖

对应的三种场景删除依赖如下

yarn workspace packageB remove packageA
yarn workspaces remove lodash
yarn remove -W -D typescript

项目构建

root 目录下的 package.json 中编写脚本

"build": "yarn workspaces run build",

yarn workspaces run xxx

执行每一个 package 下的 xxx 命令

该脚本会执行每一个子目录下的 build 脚本

会按照 root 目录下的 package.json 中

"workspaces": [
    "packages/*"
  ],

的顺序执行各个子项目的 build 脚本(通配符时就按照在项目中的次序)

版本升级及发包

发布 npm 包

发布完 git 后我们还需要将更新的版本发布到 npm 上,以便外部用户使用

我们发现手动的执行这些操作是很麻烦的且及其容易出错,幸运的是 lerna 可以帮助我们解决这些问题

lerna 提供了 publish 和 version 来支持版本的升级和发布

publish 的功能可以即包含 version 的工作,也可以单纯的只做发布操作。

lerna version

lerna version 的作用是进行 version bump,支持手动和自动两种模式

只发布某个 package

不支持,lerna 官方不支持仅发布某个 package,见 https://github.com/lerna/lerna/issues/1691,如果需要,只能自己手动的进入package进行发布,这样lerna自带的各种功能就需要手动完成且可能和lerna的功能相互冲突

由于 lerna 会自动的监测 git 提交记录里是否包含指定 package 的文件修改记录,来确定版本更新,这要求设置好合理的 ignore 规则(否则会造成频繁的,无意义的某个版本更新),好处是其可以自动的帮助 package 之间更新版本

例如如果 ui-form 依赖了 ui-button,如果 ui-button 发生了版本变动,会自动的将 ui-form 的对 ui-button 版本依赖更新为 ui-button 的最新版本。 如果 ui-form 发生了版本变动,对 ui-button 并不会造成影响。

lerna publish

package的源码需要打包后进行发布,需要在lerna publish之前,手动打包已经改动的package,有时候会在publish之后才想起来还没打包,针对这个问题,把build流程和publish发布封装一个npm scripts,打包后自动publish

npm publish之前需要进行build且需要在打包后的目录下运行npm publish

lerna publish from-package

在注册表中不存在版本的最新提交中发布包 ( from-package)。

除了要发布的包列表是通过检查每个 package.json 包并确定注册表中是否不存在任何包版本来确定的。注册表中不存在的任何版本都将被发布。当以前 lerna publish 未能将所有包发布到注册表时,这很有用。

发布 package 中 pkg.json 上的 version 在 registry(高于 latest version)不存在的包

lerna publish 永远不会发布 package.json 中 private 设置为 true 的包

在 lerna 中,如果 workspaces 之前存在依赖的话,在这次发包中,例如 A 这个包依赖了 B,B 在这次发包中版本升级了,那么这里 A 里面依赖的 B 也要更新到对应的版本。

lerna publish 一般有以下几种形式

  1. 发布自上次发布来有更新的包(这里的上次发布也是基于上次执行lerna publish 而言)

  2. 发布在最近 commit 中修改了 package.json 中的 version (且该 version 在 registry 中没有发布过)的包(即 lerna publish from-package)

发布

创建项目

初始化项目 & 初始化 lerna

yarn init
npx lerna init

lerna 配置使用 yarn workspaces, 使用 independent 模式

lerna.json

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

npmClient: 我们显示声明了我们的包客户端(npmClient)为 yarn,并且让 Lerna 追踪我们 workspaces 设置的目录,这样我们就依旧保留了之前 workspaces 的所有特性(子项目引用和通用包提升)。

version: 除此之外一个有趣的改动在于我们将 version 属性指定为一个关键字 independent,这将告诉 lerna 应该将每个子项目的版本号看作是相互独立的。当某个子项目代码更新后,运行 lerna publish 时,Lerna 将监听到代码变化的子项目并以交互式 CLI 方式让开发者决定需要升级的版本号,关联的子项目版本号不会自动升级,反之,当我们填入固定的版本号时,则任一子项目的代码变动,都会导致所有子项目的版本号基于当前指定的版本号升级。

独立模式,每个 package 都可以有自己的版本号。版本号维护在各自 package.json 的 version 中。每次发布前都会提示已经更改的包,以及建议的版本号或者自定义版本号。

lerna publish:发布代码有变动的 package,因此首先您需要在使用 Lerna 前使用 git commit 命令提交代码,好让 Lerna 有一个 baseline;

lerna publish 参数

--npm-tag [tagname] — 使用给定的 npm dist-tag (默认为 latest)发布到 npm。

配置 package.json 使用 yarn workspacess

// package.json
{
  "name": "monorepo-template",
  "private": true, // root禁止发布
  "workspaces": [ // 配置package目录
     "packages/*"
  ]
}

创建 package

注意 package.json 模块的配置

// package.json
{
 "name": "ui",
 "version": "1.0.0",
 "main": "index.js",
 "publishConfig": {
   "access": "publish" // 如果该模块需要发布,对于scope模块,需要设置为publish,否则需要权限验证
  }
}
  1. 创建一个 lib 模块并初始化
cd packages && mkdir lib && cd lib && yarn init -y
  1. 创建一个 ui 模块并初始化 --- 需要发布

使用 lerna create 快速创建 package --- 推荐

npx lerna create ui -y
  1. 创建一个 app 模块
npx lerna create app -y

将 lib 作为 app 的依赖

yarn workspace app add lib@1.0.0 # 这里必须加上版本号,否则报错

一般 root 只包含一些开发工具依赖如 webpack,babel,typescript 等

yarn add -W -D typescript

添加 conventional-commit 支持

lerna 的 version_bump 和 changelog 生成都依赖于 conventional-commit,因此需要保证 commit-msg 符合规范。

添加@commitlint/cli 和@commitlint/config-conventional 以及 husky

yarn add -W -D @commitlint/config-conventional @commitlint/cli lint-staged husky

commitlint.config.js

module.exports = {
  extends: ["@commitlint/config-conventional"]
};

package.json 中配置 commit-msg 的 hooks

"husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  },

添加 git-cz,支持 commit-msg 提示

$ yarn add -W -D commitizen cz-conventional-changelog

配置 commitizen 并添加 commit 为 npm script

// package.json
"scripts": {
   "commit": "git-cz"
 },
 "config": {
    "commitizen": {
      "path": "cz-conventional-changelog"
    }
  }

这样后续 commit,就可以使用 yarn commit 进行 commit,其会自动做出如下提示

发版

package.json 中新增发版脚本

 "release": "lerna version --no-git-tag-version",
    "publish": "lerna publish from-package",
    "publish:next": "lerna publish from-package --dist-tag next"

lerna 常用命令

本项目使用 lerna 来管理多个插件

lerna 常用命令

$ npx lerna init # 初始化
$ npx lerna create @templatejs/parser # 创建一个package
$ npx lerna add yargs --scope=@templatejs/parser # 给package安装依赖
$ npx lerna list # 列出所有的包
$ npx lerna bootstrap # 安装全部依赖
$ npx lerna link # 建立全部软连接
$ npx lerna changed # 列出下次发版lerna publish 要更新的包
$ npx lerna publish # 会打tag,上传git,上传npm

发布步骤

$ yarn release
# git commit
$ yarn publish:next # 测试包
$ yarn publish # 正式包

<del>也可使用下面的命令发布</del>

不打 tag 发布

$ yarn test
$ yarn build
$ npx lerna version --no-git-tag-version # 仅修改version
$ npx lerna publish from-package --dist-tag next # 发布测试包,需要选择对应的 alpha 版本号
$ npx lerna publish from-package # 发布正式包

打 tag 发布

$ yarn test
$ yarn build
$ npx lerna publish --dist-tag next # 发布测试包,需要选择对应的 alpha 版本号
$ npx lerna publish # 发布正式包

如何从 multirepo 迁移至使用 monorepo 策略?

至此,我们学会了如何采用 monorepo 策略组织项目代码的最佳实践,或许您已经开始跃跃欲试想要尝试前文提到的种种技巧。从 0 搭建一个 monorepo 项目,当然没问题!可是如果要基于已有的项目,将其转化为一个使用 monorepo 策略的项目呢?

或许您注意到了,Lerna 为我们提供了 lerna import 命令,用来将我们已有的包导入到 monorepo 仓库,并且还会保留该仓库的所有 commit 信息。然而实际上,该命令仅支持导入本地项目,并且不支持导入项目的分支和标签 。

推荐阅读

为什么使用 monorepo

注意事项

  1. 运行lerna publish命令时需要提交代码,也就是执行 commit & push

  2. package.json 中的

// 发包的地址
"publishConfig": {
    "registry": "xxx.xxx.com"
  },
  1. push 执行lerna publish通过命令行交互的形式选择每一个 package 的版本,然后为你自动修改每个包的 package.json,对于依赖的 package 也会自动更新到对应的版本,并且提交到 git

  2. npx lerna publish from-package 会检查每一个包的 package.json(即使该包没有改动),按 package.json 中的版本进行发包,只有在当前版本不存在于注册表中的时候才可以发布成功

lerna version 会做的事情

1.识别从上次打标记发布以来发生变更的 package

2.版本提示

3.修改 package 的元数据反映新的版本,在根目录和每个 package 中适当运行lifecycle scripts

4.在 git 上提交改变并对该次提交打标记(git commit & git tag)

5.提交到远程仓库(git push)

lerna version --no-git-tag-version 不会执行4 、5

lerna publish / lerna version 是否需要build?

需要!但是不需要切换到打包后的目录,lerna自动帮我们在打包后的目录运行publish

会更新所有包 还是只更新修改过的包?

是的会更新所有包的版本,不论你是否修改

publish结束之后需要我们自己commit & push来将版本号提交到远端

lerna version会将版本修改commit&push

直接调用lerna publish会先进行lerna version的操作

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容