對於維護過多個package(功能相近)的同窗來講,都會遇到一個選擇題,這些package是放在一個倉庫裏維護仍是放在多個倉庫裏單獨維護。Multirepo 是比較傳統的作法,即每個 package 都單獨用一個倉庫來進行管理。Monorepo 是管理項目代碼的一個方式,指在一個項目倉庫 (repo) 中管理多個模塊/包 (package),不一樣於常見的每一個模塊建一個 repo。前端
目前有很多大型開源項目採用了這種方式,如 Babel
,React
, Meteor
, Ember
, Angular
,Jest
, Umijs
, Vue
, 還有 create-react-app
, react-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 管理工具,功能完整。小程序
Lerna 是一個管理多個 npm 模塊的工具,是 Babel 本身用來維護本身的 Monorepo 並開源出的一個項目。優化維護多包的工做流,解決多個包互相依賴,且發佈須要手動維護多個包的問題。
推薦全局安裝,由於會常常用到 lerna 命令
npm i -g lerna
複製代碼
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"
}
複製代碼
增長兩個 packages
lerna create @mo-demo/cli
lerna create @mo-demo/cli-shared-utils
複製代碼
分別給相應的 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 // 增長內部模塊之間的依賴
複製代碼
lerna publish
複製代碼
上述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
中了。
lerna不負責構建,測試等任務,它提出了一種集中管理package的目錄模式,提供了一套自動化管理程序,讓開發者沒必要再深耕到具體的組件裏維護內容,在項目根目錄就能夠全局掌控,基於 npm scripts,使用者能夠很好地完成組件構建,代碼格式化等操做。接下來咱們就來看看,若是基於 Lerna,並結合其它工具來搭建 Monorepo 項目的最佳實踐。
目前最多見的 monorepo 解決方案是 Lerna 和 yarn 的 workspaces 特性,基於lerna和yarn workspace的monorepo工做流。因爲yarn和lerna在功能上有較多的重疊,咱們採用yarn官方推薦的作法,用yarn來處理依賴問題,用lerna來處理髮布問題。能用yarn作的就用yarn作吧
普通項目: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
複製代碼
在依賴亂掉或者工程混亂的狀況下,清理依賴
普通項目: 直接刪除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操做
複製代碼
普通項目: 經過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
複製代碼
普通項目:創建一個build的npm script,使用yarn build便可完成項目構建
monorepo:區別於普通項目之處在於各個package之間存在相互依賴,如packageB只有在packageA構建完以後才能進行構建,不然就會出錯,這實際上要求咱們以一種拓撲排序的規則進行構建。
咱們能夠本身構建拓撲排序規則,很不幸的是yarn的workspace暫時並未支持按照拓撲排序規則執行命令,雖然該 rfc已經被accepted,可是還沒有實現, 幸運的是lerna支持按照拓撲排序規則執行命令, --sort參數能夠控制以拓撲排序規則執行命令
lerna run --stream --sort build
複製代碼
項目測試完成後,就涉及到版本發佈,版本發佈通常涉及到以下一些步驟
條件驗證: 如驗證測試是否經過,是否存在未提交的代碼,是否在主分支上進行版本發佈操做
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的工做,也能夠單純的只作發佈操做。
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
來根據提示一步一步輸入,來完成代碼的提交。
上面咱們使用了 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 來提交。
除了規範提交信息,代碼自己確定也少了靠規範來統一風格。
安裝
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 文件時,便會自動修正並校驗錯誤。即保證了代碼風格統一,又能提升代碼質量。
有了以前的規範提交,自動生成日誌便水到渠成了。再詳細看下 lerna publish
時作了哪些事情:
找出從上一個版本發佈以來有過變動的 package
提示開發者肯定要發佈的版本號
將全部更新過的的 package 中的package.json的version字段更新
將依賴更新過的 package 的 包中的依賴版本號更新
更新 lerna.json 中的 version 字段
提交上述修改,並打一個 tag
推送到 git 倉庫
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
就行了。
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'
}
複製代碼
到這裏,基本上已經構建了基於lerna
和yarn workspace
的monorepo項目的最佳實踐了,該有的功能都有:
完善的工做流
typescript支持
風格統一的編碼
完整的單元測試
一鍵式的發佈機制
完美的更新日誌
……
固然,構建一套完善的倉庫管理機制,可能它的收益不是一些量化的指標能夠衡量出來的,也沒有直接的價值輸出,但它能在平常的工做中極大的提升工做效率,解放生產力,節省大量的人力成本。