hardfist / stackoverflow

记录下工作中碰到的坑
81 stars 0 forks source link

基于lerna和yarn workspace的monorepo工作流 #24

Open hardfist opened 5 years ago

hardfist commented 5 years ago

基于lerna和yarn workspace的monorepo工作流

monorepo管理代码有众多好处,但是也带来了很多的技术上的挑战,github上的很多的项目都是使用lerna管理monorepo项目,我们探讨下在gitlab上也通过lerna和yarn workspace结合来管理项目。

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

img

一个非monorepo的普通项目,常见的开发流程如下,对于monorepo项目却可能存在各个问题

搭建环境

主要指安装依赖

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

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

清理环境

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

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

安装|删除依赖

普通项目: 通过yarn add和yarn remove即可简单姐解决依赖库的安装和删除问题

monorepo: 一般分为三种场景

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

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

对于安装local dependency,yarn的实现暂时有bug,第一次安装需要指明版本号,否则会安装失败如下

如果ui-button没有发布到npm则

yarn workspace ui-form add ui-button 会安装失败,但是

yarn workspace ui-form add ui-button@1.0.0会成功 ,详情见 https://github.com/yarnpkg/yarn/issues/3973

项目构建

普通项目:建立一个build的npm script,使用yarn build即可完成项目构建

monorepo:区别于普通项目之处在于各个package之间存在相互依赖,如packageB只有在packageA构建完之后才能进行构建,否则就会出错,这实际上要求我们以一种拓扑排序的规则进行构建。

我们可以自己构建拓扑排序规则,很不幸的是yarn的workspace暂时并未支持按照拓扑排序规则执行命令,虽然该 rfc已经被accepted,但是尚未实现

img

幸运的是lerna支持按照拓扑排序规则执行命令, --sort参数可以控制以拓扑排序规则执行命令

lerna run --stream --sort build

项目测试

普通项目: 建立一个test的npm script即可

monorepo项目:有两种方式

版本升级及发包

项目测试完成后,就涉及到版本发布,版本发布一般涉及到如下一些步骤

条件验证

如验证测试是否通过,是否存在未提交的代码,是否在主分支上进行版本发布操作,以及其他条件

更加严苛的一些验证操作可以通过danger.js,如rxjs https://github.com/ReactiveX/rxjs/blob/master/dangerfile.js

version_bump

发版的时候需要更新版本号,这时候如何更新版本号就是个问题,一般大家都会遵循 semVer语义,如果版本之间的提交记录较少,能够较为容易的手动更新版本好,但这样也存在人为失误的可能,更好的办法是根据git的提交记录自动更新版本号,实际上只要我们的git commit message符合 Conventional commit规范,即可通过工具根据git提交记录,更新版本号,简单的规则如下

生成changelog

为了方便查看每个package每个版本解决了哪些功能,我们需要给每个package都生成一份changelog方便用户查看各个版本的功能变化。同理只要我们的commit记录符合 conventional commit规范,即可通过工具为每个package生成changelog文件

生成git tag:

为了方便后续回滚问题及问题排查通常需要给每个版本创建一个git tag

git 发布版本:

每次发版我们都需要单独生成一个commit记录来标记milestone

发布npm包:

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

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

yarn官方并不打算支持发布流程,只是想做好包管理工具,因此这部分还是需要通过lerna支持

img

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并不会造成影响。

自动选择发布版本

使用--conventional-commits 参数会自动的根据conventional commit规范和git commit message记录帮忙确定更新的版本号。

lerna version --conventional-commits

自动确立了如下版本更新

img

经测试version_bump是依赖于文件检测和subject结合,并不依赖于scope,scope的作用是用来生成changelog的吧,即如果是修改了ui-form的文件,但是commit记录写的是fix(ui-button),lerna是会生成ui-form的版本更新,并不会去更新ui-button的版本

手动选择发布版本

如果git commit message发现不太靠谱,且无法修改的话,那么需要手动的确认新版本,version默认是手动选择版本

lerna version

img

version成功后会自动的推送到主分支,我一般是关闭主分支的推送权限的,这样就会导致推送失败,但是暂时没找到如何禁止推送主分支的好办法,使用--no-push会把tag推送一起禁止掉,好在禁止推送主分支只会报错,但不影响整个流程

lerna version自动生成的提交格式为“ publish xxx",并不符合conventional-commit规范,因此需要加以修改,我们通过message参数可以修改自动生成的提交记录

// lerna.json
{
  "packages": [
    "packages/*"
  ],
  "version": "independent",
  "npmClient": "yarn",
  "command": {
    "publish": {
      "ignoreChanges": ["*.md"],
      "verifyAccess": false,
      "verifyRegistry": false,
      "message":"chore: publish" // 自定义version生成的message记录
    }
  }
}

changelog.md

version完成后会自动生成changelog.md,但是由于lerna是根据什么规则来生成changelog的规则尚不清楚,现在发现A库的changlog里可能包含B的commit记录,具体原因待查

img

lerna publish

git vesion_bump完成后,就可以根据version生成的tag进行npm发包了

lerna publish from-git

这里没使用from-package是因为每次用from-package都会在package.json里生成个gitHead字段,来关联package和git记录,造成文件被修改,需要手动的checkout或者提交掉,暂时没找到方法禁掉这个修改

内网使用lerna进行publish的时候需要配置registry和设置--no-verify-access --no-verify-registry参数

示例

完整的demo地址 https://github.com/hardfist/monorepo-starter

我们通过一个简单的项目来演示上述操作

创建项目

新建项目&&安装lerna&& 初始化lerna

mkdir monorepo-template && cd monorepo-template && yarn init -y && yarn add -D lerna && lerna init && mkdir packages

lerna配置使用yarn workspaces, 使用independent模式(根据需求选择是否使用independent)

// lerna.json
{
  "packages": ["packages/*"], // 配置package目录
  "version": "independent",
  "npmClient": "yarn",
  "useWorkspaces": true // 使用yarn workspaces
}

配置package.json使用yarn workspacess

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

创建package

创建ui-lib 模块

初始化ui-button模块

cd packages && mkdir ui-lib && yarn init -y 

配置

// package.json
{
 "name": "ui-button",
 "version": "1.0.0",
 "main": "index.js",
 "publishConfig": {
   "access": "publish" // 如果该模块需要发布,对于scope模块,需要设置为publish,否则需要权限验证
  }
}

创建ui-app 模块

同上,或者使用lerna create快速创建package

lerna create ui-app -y

将ui-lib作为ui-app的依赖

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

将lodash添加为所有package的依赖(不包含root)

yarn workspaces run add lodash

将typescript设置为root的开发依赖

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

yarn add -W -D typescript jest

构建和测试

为ui-lib和ui-app添加build和test和clean脚本

packages/ui-lib/package.json
{
  "scripts": {
    "build": "tsc",
    "test": "jest",
    "clean": "rimraf lib"
  },
}
packages/ui-app/package.json
{
    "scripts": {
    "test": "echo success", // 如果对应脚本无事可做,可以直接echo success
    "build": "tsc",
    "clean": "rimraf lib"
  },
}

在root里添加build、test和clean脚本

"scripts": {
    "build": "lerna run --stream --sort build", // 按照拓扑依赖进行构建
    "clean": "yarn workspaces run clean", // 彼此独立,可以并行执行
    "test": "yarn workspaces run test" // 彼此独立可以并行执行
  },

添加conventional-commit支持

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

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

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

配置commitlint

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

配置commit-msg的hooks

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

发版

开发测试通过后,每隔一段时间即可发版,我们使用lerna version来做发版

配置发版的message

// lerna.json
  {
  "packages": ["packages/*"],
  "version": "independent",
  "npmClient": "yarn",
  "useWorkspaces": true,
  "command": {
    "publish": {
      "ignoreChanges": ["*.md"], // md文件更新,不触发版本变动
      "verifyAccess": false, // 内网发包需开启
      "verifyRegistry": false, // 内网发包需开启
      "message": "chore: publish" // 修改默认的publish的commit msg
    }
  }
}

``

配置发版的策略,我们积极convention-commit来发版

// package.json
{
  "scripts":
  {
    version: "lerna version --conventional-commits" ## 生成changelog文件以及根据commit来进行版本变动
  }
}

发版

$ yarn run version # 不要使用 yarn version,yarn version 是yarn自动的命令不是npm script

这个会提示用户输入版本,如果不想这个提示可以关闭

// package.json
{
  "scripts":
  {
     version: "lerna version --conventional-commits --yes" ## 生成changelog文件以及根据commit来进行版本变动,不提示用户输入版本
  }
}

``

发包

发版成功后既可以发包,使用lerna publish即可发包

$ lerna publish from-git

常见问题

https://github.com/lerna/lerna/issues/1425 原因是conventional-changelog造成的,讨论见 https://github.com/conventional-changelog/standard-version/issues/163 这可能造成很多的patch version,如果在乎这个可以使用canary release

HaveF commented 5 years ago

从知乎过来看,结果图破了...又回去了 = =;

dbssAlan commented 4 years ago

一个小错误提示:

安装|删除依赖 普通项目: 通过yarn add和yarn remove即可简单姐解决依赖库的安装和删除问题

多写了一个""字

bj75326 commented 3 years ago

菜鸡借道请教一个问题=。= 如下: 我现在用lerna做一套组件开发,其中有一个private:true的package专门用来做组件展示,这个package依赖着其他的组件package并且使用了webpack 的HMR,想问一下有没有什么办法可以在改动其他组件package代码的时候也能触发热更新,以使改动很快的反馈在页面上。

towry commented 3 years ago

菜鸡借道请教一个问题=。= 如下: 我现在用lerna做一套组件开发,其中有一个private:true的package专门用来做组件展示,这个package依赖着其他的组件package并且使用了webpack 的HMR,想问一下有没有什么办法可以在改动其他组件package代码的时候也能触发热更新,以使改动很快的反馈在页面上。

可能需要自己写些工具,开发模式下,link 某个需要更改的组件,修改组件,查看预览展示。然后提交组件的修改,unlink结束。想要完全靠 lerna 等现有库很难满足自己的需求。

towry commented 3 years ago

https://rushjs.io/pages/intro/welcome/

uinz commented 3 years ago

很好的介绍, 希望有更多 lerna 结合 gitlab-ci 或 github-action 的场景介绍

atong666 commented 2 years ago

菜鸡借道请教一个问题=。= 如下: 我现在用lerna做一套组件开发,其中有一个private:true的package专门用来做组件展示,这个package依赖着其他的组件package并且使用了webpack 的HMR,想问一下有没有什么办法可以在改动其他组件package代码的时候也能触发热更新,以使改动很快的反馈在页面上。

可以使用源码依赖的方式