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

1.monorepo管理

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

目前有很多大型開源項目採用了這種方式,如 BabelReact, Meteor, Ember, Angular,Jest, Umijs, Vue, 還有 create-react-appreact-router 等。幾乎咱們熟知的倉庫,都無一例外的採用了monorepo 的方式,能夠看到這些項目的第一級目錄的內容以腳手架爲主,主要內容都在 packages目錄中、分多個 package 進行管理。node

目錄結構以下:react

├── packages
|   ├── pkg1
|   |   ├── package.json
|   ├── pkg2
|   |   ├── package.json
├── package.json
複製代碼

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

一圖勝千言:typescript

固然到底哪種管理方式更好,仁者見仁,智者見智。前者容許多元化發展(各項目能夠有本身的構建工具、依賴管理策略、單元測試方法),後者但願集中管理,減小項目間的差別帶來的溝通成本。

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

結合shop-service門戶的實際場景和業務須要,自然的 MonoRepo ! 一個理想的開發環境能夠抽象成這樣:npm

「只關心業務代碼,能夠直接跨業務複用而不關心複用方式,調試時全部代碼都在源碼中。」json

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

上圖中提到的利用相關工具就是今天的主角 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 的選擇。以下:

咱們使用 commitizencz-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.jscommitlint 指定相應的規範

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 實踐總結

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

  • 完善的工做流

  • typescript支持

  • 風格統一的編碼

  • 完整的單元測試

  • 一鍵式的發佈機制

  • 完美的更新日誌

……

固然,構建一套完善的倉庫管理機制,可能它的收益不是一些量化的指標能夠衡量出來的,也沒有直接的價值輸出,但它能在平常的工做中極大的提升工做效率,解放生產力,節省大量的人力成本。

相關文章
相關標籤/搜索