今天文章的話題是 monorepo。在進入正文以前,筆者先來歸納下什麼是 monorepo 以及本文會從哪幾個點來聊聊 monorepo。node
monorepo 簡單來講就是將多個項目整合到了一個倉庫裏來管理,不少開源庫都採用了這種代碼管理方式,好比 Vue 3.0:git
從上圖咱們能夠看到 packages 文件夾下存在一堆文件夾,這每一個文件夾都對應一個 npm 包,咱們把這一些 npm 包都管理在一個倉庫下了。github
瞭解 monorepo 的讀者確定聽過 lerna,想必也看過很多 lerna 配置相關的文章。本文不會來聊 lerna 該怎麼怎麼配置,而是主要來聊聊當咱們使用 monorepo 後會引入哪些問題?lerna 這些工具鏈解決了什麼問題以及是如何解決的,總的來講將會從如下幾點來聊聊 monorepo:shell
目前流行的就兩種代碼管理方式,分別爲:npm
接下來聊聊它們各自的優缺點。編程
mono repojson
✅ 只需在一個倉庫中開發,編碼會至關方便。bash
✅ 代碼複用高,方便進行代碼重構。架構
❌ 項目若是變的很龐大,那麼 git clone、安裝依賴、構建都會是一件耗時的事情。編輯器
multi repo
✅ 倉庫體積小,模塊劃分清晰。
❌ 多倉庫來回切換(編輯器及命令行),項目一多真的得暈。若是倉庫之間存在依賴,還得各類 npm link
。
❌ 不利於代碼複用。
mono repo
✅ 工程統一標準化
multi repo
❌ 各個團隊可能各自有一套標準,新建一個倉庫又得從新配置一遍工程及 CI / CD 等內容。
mono repo
✅ 共同依賴能夠提取至 root,版本控制更加容易,依賴管理會變的方便。
multi repo
❌ 依賴重複安裝,多個依賴可能在多個倉庫中存在不一樣的版本,npm link
時不一樣項目的依賴可能會存在衝突問題。
mono repo
❌ 代碼全在一個倉庫,項目一大,幾個 G 的話,用 Git 管理會存在問題。
multi repo
✅ 各個團隊能夠控制代碼權限,也幾乎不會有項目太大的問題。
這部分二者其實都存在問題。
multi repo 的話,若是各個包之間不存在依賴關係倒沒事,一旦存在依賴關係的話,開發者就須要在不一樣的倉庫按照依賴前後順序去修改版本及進行部署。
而對於 mono repo 來講,有工具鏈支持的話,部署會很方便,可是沒有工具鏈的話,存在的問題同樣蛋疼,後續文章中會講到。
看了上文中的對比,相信讀者應該是能認識到 mono repo 在一些痛點上仍是解決得很不錯的,這也是不少開源項目採用它的緣由。可是實際上當咱們引入 mono repo 架構之後,又會帶來一大堆新的問題,無非市面上的工具鏈幫咱們解決了大部分問題,好比 lerna。
接下來筆者就來聊聊 monorepo 在不使用工具鏈的狀況下會存在哪些問題,以及市面上的工具鏈是如何解決問題的。
各個包之間都存在各自的依賴,有些依賴多是多個包都須要的,咱們確定是但願相同的依賴能提高到 root 目錄下安裝,其它的依賴裝哪都行。
此時咱們能夠經過 yarn 來解決問題(npm 7 以前不行),須要在 package.json 中加上 workspaces
字段代表多包目錄,一般爲 packages
。
以後當咱們安裝依賴的時候,yarn 會盡可能把依賴拍平裝在根目錄下,存在版本不一樣狀況的時候會把使用最多的版本安裝在根目錄下,其它的就裝在各自目錄裏。
這種看似正確的作法,可能又會帶來更噁心的問題。
好比說多個 package 都依賴了 React,可是它們版本並不都相同。此時 node_modules 裏可能就會存在這種狀況:根目錄下存在這個 React 的一個版本,包的目錄中又存在另外一個依賴的版本。
由於 node 尋找包的時候都是從最近目錄開始尋找的,此時在開發的過程當中可能就會出現多個 React 實例的問題,熟悉 React 開發的讀者確定知道這就會報錯了。
遇到這種狀況的時候,咱們就得用 resolutions
去解決問題,固然也能夠經過阻止 yarn 提高共同依賴來解決(更麻煩了)。筆者已經不止一次遇到過這種問題,可能是安裝依賴的依賴形成的多版本問題。
在 multi repo 中各類 link 已經夠頭疼了,我可不想在 mono repo 中繼續 link 了。
此時 yarn 又拯救了咱們,在安裝依賴的時候會幫助咱們將各個 package 軟鏈到根目錄中,這樣每一個 package 就能找到另外的 package 以及依賴了。
可是實際上這樣的方式還會帶來一個坑。由於各個 package 都能訪問到拍平在根目錄中的依賴了,所以此時其實咱們無需在 package.json 中聲明 dependencies 就能使用別人的依賴了。這種狀況極可能會形成咱們最終忘了加上 dependencies,一旦部署上線項目就運行不起來了。
以上兩塊主要聊了依賴以及 link 層面的問題,這部分咱們能夠直接經過 yarn 解決,雖然又引入了別的問題。
接下來聊聊 mono repo 在 CI 中會遇到的挑戰,包括了構建、單測、部署環節。
構建是咱們會遇到的第一個問題。這時候可能有些讀者就會說了,構建不就是跑個 build 麼,能有個啥問題。哎,接下來我就跟你聊聊這些問題。
首先由於全部包都存在一個倉庫中了,若是每次執行 CI 的時候把全部包都構建一遍,那麼一旦代碼量變多,每次構建可能都要花上很多的時間。
這時候確定有讀者會想到增量構建,每次只構建修改了代碼的 package,這個確實可以解決問題,核心代碼也很簡單:
git diff --name-only {git tag / commit sha} --{package path}
上述命令的功能是尋找從上次的 git tag 或者初次的 commit 信息中查找某個包是否存在文件變動,而後咱們拿到這些信息只針對變動的包作構建就行。可是注意這個命令的前提是在部署的時候打上 tag,不然就找不到上次部署的節點了。
可是單純這樣的作法是不夠的,由於在 mono repo 中咱們還會遇到多個 package 之間有依賴的場景
在這種狀況下假如此時在 CI 中發現只有 A 包須要構建而且只去構建了 A 包,那麼就會出現問題:在 TS 環境下確定會報錯找不到 D 包的類型。
在這種存在包於包之間有依賴的場景時,咱們須要去構建一個有向無環圖(DAG)來進行拓撲排序,關於這個概念有興趣的讀者能夠自行查閱資料。
總之在這種場景下,咱們須要尋找出各個包之間的依賴關係,而後根據這個關係去構建。好比說 A 包依賴了 D 包,當咱們在構建 A 包以前得先去構建 D 包才成。
以上是沒有工具鏈時可能會出現的問題。若是咱們用上 lerna 的話,內置的一些命令就能夠基本幫助咱們解決問題了:
lerna changed
尋找代碼有變更的包,接下來咱們就能夠本身去進行增量構建了。總結一下構建時咱們會遇到的問題:
單測的問題其實和構建遇到的問題相似。每次把全部用例都跑一遍,可能耗時比構建還長,引入增量單測頗有必要。
這個需求通常來講單測工具都會提供,好比 Jest 經過如下命令咱們就能實現需求了:
jest --coverage --changedSince=master
可是這種單測方式會引來一個小問題:單測覆蓋率是以「測試用例覆蓋的代碼 / 修改過的代碼」來算的,極可能會出現覆蓋率不達標的問題,雖然總體的單測覆蓋率多是達標的。 常寫單測的讀者確定知道有時候一部分代碼就是很難寫單測,出現這種問題也在所不免,可是若是咱們在 CI 中配置了低於覆蓋率就不能經過 CI 的話就會有點蛋疼。
固然這個問題其實仁者見仁智者見智,往好了說也是在提升每次 commit 的代碼質量。
部署是最重要的一環了,這裏會遇到的問題也是最複雜的,固然大部分問題其實以前都解決過了,問題大體可分爲:
首先來看前兩個問題。
第一個問題的解決辦法其實和增量構建那邊作法同樣,經過命令找到修改過代碼的 package 就行。可是光找到須要部署的 package 還不夠,咱們還須要經過拓撲排序看看這個 package 有沒有被別的 package 所依賴。若是被別的 package 所依賴的話,依賴方即便代碼沒有變更也是須要進行部署的,這就是第二個問題的解決方案。
第三個問題解決起來涉及的東西會有點多,筆者以前也給自動化部署系統寫過一篇文章:連接 ,有興趣的讀者能夠一讀。
這裏筆者就簡短地聊聊解決方案。
首先咱們須要引入 commitizen 這個工具。
這個工具能夠幫助咱們提交規範化的 commit 信息:
上圖中最重要的就是 feat、fix
這些信息,咱們須要根據這個 type 信息來計算最終的部署版本號。
接下來在 CI 中咱們須要分析這個規範化的 commit 信息來得出 type。
其實原理很簡單,仍是用到了 git command:
git log -E --format=%H=%B
對於以上 commit,咱們能夠經過執行命令得出如下結果:
固然這樣分析是把當前分支的全部 commit 都分析進去了,大部分發版時候咱們只須要分析上次發版至今的全部變動,所以須要修正 command 爲:
git log 上次的 commit id...HEAD -E --format=%H=%B
最後咱們就能夠經過正則來拿到 type,而後經過 semver 計算出版本號。
固然了,lerna 這些問題也能幫咱們解決的差很少了:
lerna publish --conventional-commits
執行以上代碼就基本解決了部署會遇到的問題,固然若是本身去實現這套內容會方便自定義一些。
總結一下部署環節中咱們可能會遇到的問題:
從上文中讀者們應該也能夠發現這些 monorepo 的工具鏈幫助咱們解決了不少問題,以致於把這些問題都隱藏了起來,致使了不少開發者可能都不瞭解使用 monorepo 到底會帶來哪些問題。
另外這些工具鏈也並非完美的,使用它們之後其實又會帶來一些別的問題。
好比說咱們用 yarn workspaces 解決了 link 以及安裝依賴的問題,可是又帶來了版本間的衝突以及非法訪問依賴的問題,解決這些問題咱們可能又得引入新的包管理器,好比 pnpm 來解決。
總的來講,在編程世界裏還真的沒啥銀彈,看似不錯的工具,在幫助咱們解決了很多問題的同時必然又會引入新的問題,選擇工具無非是在看當下哪一個使用起來成本更低收益更大罷了。
mono repo 並非銀彈,使用這個架構仍是會帶來不少問題,無非市面上的工具鏈幫助咱們解決了大部分問題。文章主要聊了聊在沒有這些工具鏈的時候咱們可能會遇到哪些問題,以及使用這些工具後解決了什麼又帶來了什麼。