你知道 monorepo 居然有那麼多坑麼?

前言

今天文章的話題是 monorepo。在進入正文之前,筆者先來概括下什麼是 monorepo 以及本文會從哪幾個點來聊聊 monorepo。

monorepo 簡單來說就是將多個項目整合到了一個倉庫裏來管理,很多開源庫都採用了這種代碼管理方式,比如 Vue 3.0:

從上圖我們可以看到 packages 文件夾下存在一堆文件夾,這每個文件夾都對應一個 npm 包,我們把這一些 npm 包都管理在一個倉庫下了。

瞭解 monorepo 的讀者肯定聽過 lerna,想必也看過不少 lerna 配置相關的文章。本文不會來聊 lerna 該怎麼怎麼配置,而是主要來聊聊當我們使用 monorepo 後會引入哪些問題?lerna 這些工具鏈解決了什麼問題以及是如何解決的,總的來說將會從以下幾點來聊聊 monorepo:

兩種代碼管理的方式及優缺點

目前流行的就兩種代碼管理方式,分別爲:

接下來聊聊它們各自的優缺點。

開發

mono repo

✅ 只需在一個倉庫中開發,編碼會相當方便。

✅ 代碼複用高,方便進行代碼重構。

❌ 項目如果變的很龐大,那麼 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 在不使用工具鏈的情況下會存在哪些問題,以及市面上的工具鏈是如何解決問題的。

monorepo 帶來了什麼問題

安裝依賴

各個包之間都存在各自的依賴,有些依賴可能是多個包都需要的,我們肯定是希望相同的依賴能提升到 root 目錄下安裝,其它的依賴裝哪都行。

此時我們可以通過 yarn 來解決問題(npm 7 之前不行),需要在 package.json 中加上 workspaces 字段表明多包目錄,通常爲 packages

之後當我們安裝依賴的時候,yarn 會盡量把依賴拍平裝在根目錄下,存在版本不同情況的時候會把使用最多的版本安裝在根目錄下,其它的就裝在各自目錄裏。

|   ├── node_modules
|   |   ├── axios@0.21.1
├── packages
|   ├── pkg1
|   |   ├── package.json -> 依賴了 axios 0.21.1
|   ├── pkg2
|   |   ├── package.json -> 依賴了 axios 0.21.1
|   ├── pkg3
|   |   ├── node_modules
|   |   |   ├── axios@0.21.0
|   |   ├── package.json -> 依賴了 axios 0.21.0

這種看似正確的做法,可能又會帶來更噁心的問題。

比如說多個 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 的話,內置的一些命令就可以基本幫助我們解決問題了:

總結一下構建時我們會遇到的問題:

單測

單測的問題其實和構建遇到的問題類似。每次把所有用例都跑一遍,可能耗時比構建還長,引入增量單測很有必要。

這個需求一般來說單測工具都會提供,比如 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

執行以上代碼就基本解決了部署會遇到的問題。但是公司內部的部署系統一般都會自己去實現這部分的功能,畢竟自定義一些功能會更加方便。

總結一下部署環節中我們可能會遇到的問題:

工具鏈帶來的好處及壞處

從上文中讀者們應該也可以發現比如 lerna 這些工具鏈幫助我們解決了很多問題,以至於把問題都隱藏了起來,導致了很多開發者可能都不瞭解使用 monorepo 到底會帶來哪些問題,因爲 monorepo 就是一個完美方案了。

另外這些工具鏈解決問題的方式也並不是完美的,使用它們以後其實又會帶來一些別的問題。

比如說我們用 yarn workspaces 解決了 link 以及安裝依賴的問題,但是又帶來了版本間的衝突以及非法訪問依賴的問題,解決這些問題我們可能又得引入新的包管理器,比如 pnpm 來解決。

總的來說,在編程世界裏還真的沒啥銀彈,看似不錯的工具,在幫助我們解決了不少問題的同時必然又會引入新的問題,選擇工具無非是在看當下哪個使用起來成本更低收益更大罷了。

總結

mono repo 並不是銀彈,使用這個架構還是會帶來很多問題,無非市面上的工具幫助我們解決了大部分。文章主要聊了聊在沒有這些工具的時候我們可能會遇到哪些問題,以及使用這些工具後解決了什麼又帶來了什麼。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/wfAjiHahb-BICrSxOHcLhA