lerna+yarn workspace+monorepo項目的最佳實踐


歡迎關注前端早茶,與廣東靚仔攜手共同進階

前端早茶專注前端,一起結伴同行,緊跟業界發展步伐~

公眾號作者:廣東靚仔

1.monorepo管理

對於維護過多個package(功能相近)的同學來說,都會遇到一個選擇題,這些package是放在一個倉庫里維護還是放在多個倉庫里單獨維護。Multirepo 是比較傳統的做法,即每一個 package 都單獨用一個倉庫來進行管理。Monorepo 是管理項目代碼的一個方式,指在一個項目倉庫 (repo) 中管理多個模塊/包 (package),不同於常見的每個模塊建一個 repo。

目前有不少大型開源項目采用了這種方式,如 Babel,React, Meteor, Ember, Angular,Jest, Umijs, Vue, 還有 create-react-app, react-router 等。幾乎我們熟知的倉庫,都無一例外的采用了monorepo 的方式,可以看到這些項目的第一級目錄的內容以腳手架為主,主要內容都在 packages目錄中、分多個 package 進行管理。

目錄結構如下:

├── packages
 | ├── pkg1
 |  | ├── package.json
 | ├── pkg2
 |  | ├── package.json
├── package.json


monorepo 最主要的好處是統一的工作流和Code Sharing。比如我想看一個 pacakge 的代碼、了解某段邏輯,不需要找它的 repo,直接就在當前 repo;當某個需求要修改多個 pacakge 時,不需要分別到各自的 repo 進行修改、測試、發版或者 npm link,直接在當前 repo 修改,統一測試、統一發版。只要搭建一套腳手架,就能管理(構建、測試、發布)多個 package。

一圖勝千言:


當然到底哪一種管理方式更好,仁者見仁,智者見智。前者允許多元化發展(各項目可以有自己的構建工具、依賴管理策略、單元測試方法),后者希望集中管理,減少項目間的差異帶來的溝通成本。

雖然拆分子倉庫、拆分子 npm 包是進行項目隔離的天然方案,但當倉庫內容出現關聯時,沒有任何一種調試方式比源碼放在一起更高效。

結合shop-service門戶的實際場景和業務需要,天然的 MonoRepo ! 一個理想的開發環境可以抽象成這樣:

“只關心業務代碼,可以直接跨業務復用而不關心復用方式,調試時所有代碼都在源碼中。”

在前端開發環境中,多 Git Repo,多 npm 則是這個理想的阻力,它們導致復用要關心版本號,調試需要 npm link。而這些是 MonoRepo 最大的優勢。

上圖中提到的利用相關工具就是今天的主角 Lerna ! Lerna是業界知名度最高的 Monorepo 管理工具,功能完整。

2. Lerna

Lerna 是一個管理多個 npm 模塊的工具,是 Babel 自己用來維護自己的 Monorepo 並開源出的一個項目。優化維護多包的工作流,解決多個包互相依賴,且發布需要手動維護多個包的問題。

2.1 安裝

推薦全局安裝,因為會經常用到 lerna 命令

npm i -g lerna

2.2 初始化項目

lerna init


其中 package.json & lerna.json 如下:

// package.json
{
"name": "root",
"private": true, // 私有的,不會被發布,是管理整個項目,與要發布到npm的解耦
"devDependencies": {
"lerna": "^3.15.0"
}
}

// lerna.json
{
"packages": [
"packages/*"
],
"version": "0.0.0"
}


2.3 創建npm包

增加兩個 packages

lerna create @mo-demo/cli
lerna create @mo-demo/cli-shared-utils


2.4 增加模塊依賴

分別給相應的 package 增加依賴模塊

lerna add chalk // 為所有 package 增加 chalk 模塊
lerna add semver --scope @mo-demo/cli-shared-utils // 為 @mo-demo/cli-shared-utils 增加 semver 模塊
lerna add @mo-demo/cli-shared-utils --scope @mo-demo/cli // 增加內部模塊之間的依賴


2.5 發布

lerna publish


2.6 依賴包管理

上述1-5步已經包含了 Lerna 整個生命周期的過程了,但當我們維護這個項目時,新拉下來倉庫的代碼后,需要為各個 package 安裝依賴包。

我們在第4步 lerna add 時也發現了,為某個 package 安裝的包被放到了這個 package 目錄下的 node_modules 目錄下。這樣對於多個 package 都依賴的包,會被多個 package 安裝多次,並且每個 package 下都維護 node_modules ,也不清爽。於是我們使用 --hoist 來把每個 package 下的依賴包都提升到工程根目錄,來降低安裝以及管理的成本。

lerna bootstrap --hoist

為了省去每次都輸入 --hoist 參數的麻煩,可以在 lerna.json 配置:

{
  "packages": [
    "packages/*"
  ],
  "command": {
    "bootstrap": {
      "hoist": true
    }
  },
  "version": "0.0.1-alpha.0"
}


配置好后,對於之前依賴包已經被安裝到各個 package 下的情況,我們只需要清理一下安裝的依賴即可:

lerna clean



然后執行 lerna bootstrap 即可看到 package 的依賴都被安裝到根目錄下的 node_modules 中了。

3. Lerna + Monorepo 最佳實踐

lerna不負責構建,測試等任務,它提出了一種集中管理package的目錄模式,提供了一套自動化管理程序,讓開發者不必再深耕到具體的組件里維護內容,在項目根目錄就可以全局掌控,基於 npm scripts,使用者可以很好地完成組件構建,代碼格式化等操作。接下來我們就來看看,如果基於 Lerna,並結合其它工具來搭建 Monorepo 項目的最佳實踐。

目前最常見的 monorepo 解決方案是 Lerna 和 yarn 的 workspaces 特性,基於lerna和yarn workspace的monorepo工作流。由於yarn和lerna在功能上有較多的重疊,我們采用yarn官方推薦的做法,用yarn來處理依賴問題,用lerna來處理發布問題。能用yarn做的就用yarn做吧

3.1 yarn workspace

3.1.1 搭建環境
普通項目:clone下來后通過yarn install,即可搭建完項目,有時需要配合postinstall hooks,來進行自動編譯,或者其他設置。

monorepo: 各個庫之間存在依賴,如A依賴於B,因此我們通常需要將B link到A的node_module里,一旦倉庫很多的話,手動的管理這些link操作負擔很大,因此需要自動化的link操作,按照拓撲排序將各個依賴進行link

解決方式:通過使用workspace,yarn install會自動的幫忙解決安裝和link問題

yarn install # 等價於 lerna bootstrap --npm-client yarn --use-workspaces


3.1.2 清理環境
在依賴亂掉或者工程混亂的情況下,清理依賴

普通項目: 直接刪除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操作

3.1.3 安裝|刪除依賴
普通項目: 通過yarn add和yarn remove即可簡單姐解決依賴庫的安裝和刪除問題

monorepo: 一般分為三種場景

給某個package安裝依賴:

yarn workspace packageB add packageA 

將packageA作為packageB的依賴進行安裝

給所有的package安裝依賴: 使用yarn workspaces add lodash 給所有的package安裝依賴

給root 安裝依賴:一般的公用的開發工具都是安裝在root里,如typescript,我們使用yarn add -W -D typescript來給root安裝依賴

對應的三種場景刪除依賴如下

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


3.1.4 項目構建
普通項目:建立一個build的npm script,使用yarn build即可完成項目構建

monorepo:區別於普通項目之處在於各個package之間存在相互依賴,如packageB只有在packageA構建完之后才能進行構建,否則就會出錯,這實際上要求我們以一種拓撲排序的規則進行構建。

我們可以自己構建拓撲排序規則,很不幸的是yarn的workspace暫時並未支持按照拓撲排序規則執行命令,雖然該 rfc已經被accepted,但是尚未實現, 幸運的是lerna支持按照拓撲排序規則執行命令, --sort參數可以控制以拓撲排序規則執行命令

lerna run --stream --sort build



3.1.5 版本升級及發包
項目測試完成后,就涉及到版本發布,版本發布一般涉及到如下一些步驟

條件驗證: 如驗證測試是否通過,是否存在未提交的代碼,是否在主分支上進行版本發布操作

version_bump:發版的時候需要更新版本號,這時候如何更新版本號就是個問題,一般大家都會遵循 semVer語義,

生成changelog: 為了方便查看每個package每個版本解決了哪些功能,我們需要給每個package都生成一份changelog方便用戶查看各個版本的功能變化。

生成git tag:為了方便后續回滾問題及問題排查通常需要給每個版本創建一個git tag

git 發布版本:每次發版我們都需要單獨生成一個commit記錄來標記milestone

發布npm包:發布完git后我們還需要將更新的版本發布到npm上,以便外部用戶使用

我們發現手動的執行這些操作是很麻煩的且及其容易出錯,幸運的是lerna可以幫助我們解決這些問題

yarn官方並不打算支持發布流程,只是想做好包管理工具,因此這部分還是需要通過lerna支持

lerna提供了publish和version來支持版本的升級和發布, publish的功能可以即包含version的工作,也可以單純的只做發布操作。

3.2 優雅的提交

3.2.1 commitizen && cz-lerna-changelog
commitizen 是用來格式化 git commit message 的工具,它提供了一種問詢式的方式去獲取所需的提交信息。

cz-lerna-changelog 是專門為 Lerna 項目量身定制的提交規范,在問詢的過程,會有類似影響哪些 package 的選擇。如下:


我們使用 commitizen 和 cz-lerna-changelog 來規范提交,為后面自動生成日志作好准備。

因為這是整個工程的開發依賴,所以在根目錄安裝:

yarn add -D commitizen
yarn add -D cz-lerna-changelog


安裝完成后,在 package.json 中增加 config 字段,把 cz-lerna-changelog 配置給 commitizen。同時因為commitizen不是全局安全的,所以需要添加 scripts 腳本來執行 git-cz

{
  "name": "root",
  "private": true,
  "scripts": {
    "commit": "git-cz"
  },
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-lerna-changelog"
    }
  },
  "devDependencies": {
      "commitizen": "^3.1.1",
      "cz-lerna-changelog": "^2.0.2",
      "lerna": "^3.15.0"
   }
}


之后在常規的開發中就可以使用 yarn run commit 來根據提示一步一步輸入,來完成代碼的提交。

3.2.2 commitlint && husky
上面我們使用了 commitizen 來規范提交,但這個要靠開發自覺使用yarn run commit 。萬一忘記了,或者直接使用 git commit 提交怎么辦?答案就是在提交時對提交信息進行校驗,如果不符合要求就不讓提交,並提示。校驗的工作由 commitlint 來完成,校驗的時機則由 husky 來指定。husky 繼承了 Git 下所有的鈎子,在觸發鈎子的時候,husky 可以阻止不合法的 commit,push 等等。

安裝 commitlint 以及要遵守的規范

yarn add -D @commitlint/cli @commitlint/config-conventional


在工程根目錄為 commitlint 增加配置文件 commitlint.config.js 為commitlint 指定相應的規范

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


安裝 husky

yarn add -D husky
在 package.json 中增加如下配置

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


"commit-msg"是git提交時校驗提交信息的鈎子,當觸發時便會使用 commitlit 來校驗。安裝配置完成后,想通過 git commit 或者其它第三方工具提交時,只要提交信息不符合規范就無法提交。從而約束開發者使用 yarn run commit 來提交。

3.2.3 eslint && lint-staged

除了規范提交信息,代碼本身肯定也少了靠規范來統一風格。

安裝

yarn add -D standard lint-staged
eslint就是完整的一套 JavaScript(typescript) 代碼規范,自帶 linter & 代碼自動修正。自動格式化代碼並修正,提前發現風格以及程序問題, 同時也支持typescript的代碼規范校驗,eslintrc.json配置:

{
  "extends": [
    "yayajing",
    "plugin:@typescript-eslint/recommended"
  ],
  "parser": "typescript-eslint-parser",
  "plugins": ["@typescript-eslint"],
   "rules": {
      "eqeqeq":"off",
      "@typescript-eslint/explicit-function-return-type": "off",
     "no-template-curly-in-string": "off"
  }
}



lint-staged staged 是 Git 里的概念,表示暫存區,lint-staged 表示只檢查並矯正暫存區中的文件。一來提高校驗效率,二來可以為老的項目帶去巨大的方便。

package.json配置

// package.json
{
"name": "root",
"private": true,
"scripts": {
"c": "git-cz"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-lerna-changelog"
}
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.ts": [
"eslint --fix",
"git add"
]
},
"devDependencies": {
"@commitlint/cli": "^8.1.0",
"@commitlint/config-conventional": "^8.1.0",
"commitizen": "^3.1.1",
"cz-lerna-changelog": "^2.0.2",
"husky": "^3.0.0",
"lerna": "^3.15.0",
"lint-staged": "^9.2.0"
}
}



安裝完成后,在 package.json 增加 lint-staged 配置,如上所示表示對暫存區中的 js 文件執行 eslint --fix 校驗並自動修復。那什么時候去校驗呢,就又用到了上面安裝的 husky ,husky的配置中增加pre-commit的鈎子用來執行 lint-staged 的校驗操作。

此時提交 ts 文件時,便會自動修正並校驗錯誤。即保證了代碼風格統一,又能提高代碼質量。

3.3 發布自動生成日志

有了之前的規范提交,自動生成日志便水到渠成了。再詳細看下 lerna publish 時做了哪些事情:

3.3.1 lerna version 更新版本
找出從上一個版本發布以來有過變更的 package

提示開發者確定要發布的版本號

將所有更新過的的 package 中的package.json的version字段更新

將依賴更新過的 package 的 包中的依賴版本號更新

更新 lerna.json 中的 version 字段

提交上述修改,並打一個 tag

推送到 git 倉庫

 

3.3.2 使用 npm publish 將新版本推送到 npm

CHANGELOG 很明顯是和 version 一一對應的,所以需要在 lerna version 中想辦法,查看 lerna version 命令的詳細說明后,會看到一個配置參數 --conventional-commits。沒錯,只要我們按規范提交后,在 lerna version 的過程中會便會自動生成當前這個版本的 CHANGELOG。為了方便,不用每次輸入參數,可以配置在 lerna.json中,如下:

{
"packages": [
"packages/*"
],
"command": {
"bootstrap": {
"hoist": true
},
"version": {
"conventionalCommits": true
}
},
"ignoreChanges": [
"**/*.md"
],
"version": "0.0.1-alpha.1"
}


lerna version 會檢測從上一個版本發布以來的變動,但有一些文件的提交,我們不希望觸發版本的變動,譬如 .md 文件的修改,並沒有實際引起 package 邏輯的變化,不應該觸發版本的變更。可以通過 ignoreChanges 配置排除。如上。


實際 lerna version很少直接使用,因為它包含在 lerna publish 中了,直接使用 lerna publish就好了。

3.4 完善的測試用例

monorepo項目:測試有兩種方式

使用統一的jest測試配置這樣方便全局的跑jest即可,好處是可以方便統計所有代碼的測試覆蓋率,壞處是如果package比較異構(如小程序,前端,node 服務端等),統一的測試配置不太好編寫

每個package單獨支持test命令,使用yarn workspace run test,壞處是不好統一收集所有代碼的測試覆蓋率

如果采用jest編寫測試用例,支持typescript的話,需要初始化配置jest.config.js:

module.exports = {
preset: 'ts-jest',
moduleFileExtensions: ['ts'],
testEnvironment: 'node'
}

4 實踐總結

到這里,基本上已經構建了基於lerna和yarn workspace的monorepo項目的最佳實踐了,該有的功能都有:

完善的工作流

typescript支持

風格統一的編碼

完整的單元測試

一鍵式的發布機制

完美的更新日志

 

關注我,一起攜手進階

前端技術公眾號

如果這篇文章有幫動到你,歡迎關注前端早茶,與廣東靚仔攜手共同進階~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM