Monorepo可能沒你想象中那麼香……

全文共2500字,預計學習時長10分鐘

 

圖源:slideshare

 

最近許多流行的開源項目都存在這樣的現象,就是在一個存儲庫中包含多個npm微包,React、Parcel、Babel等等皆是如此。

 

然而Monorepo可能沒你想象中那麼香,筆者認爲,在大多數情況下,這種模式對項目是弊大於利的,它引入了不必要的複雜性,犧牲了作者和開發人員的可用性。

 

 

爲什麼選擇Monorepos?

 

monorepos的概念是簡化依賴項管理。如果項目包含許多包,這些包需要依賴於彼此的特定版本,那麼將它們放在一個地方而不是放在單獨的存儲庫中就可以更容易地管理。

 

同樣,對於一個歷史記錄,這些包將始終具有同步或“原子”提交。爲了讓事情變得更簡單,可以使用自定義腳本自動管理所有包的發佈,這樣一來,沒有相應的包,這個包就不會發布。

 

一個JavaScriptmonorepo項目通常會有這樣的結構:

 

myproject.git/
    packages/
        package-1/
            package.json
        package-2/
            package.json
        package-3/
            package.json
        ...
    scripts/
        common-publishing-script.js

 

這只是一個小例子,你可以看到monorepos可以得到多大的值:

 

· React: 32個包

(https://github.com/facebook/react/tree/master/packages)

 

· Parcel: 81個包

(https://github.com/parcel-bundler/parcel/tree/v2/packages)

 

· Babel: 138個包

(https://github.com/babel/babel/tree/master/packages)

 

這很荒謬,下面筆者將解釋爲什麼要反對monorepos概念,以及爲什麼這是一種反模式。

 

 

掩蓋monolith

 

將代碼分解成多個包有幾個好處,無論是庫、微服務還是微前端,都顯著地提高了構建速度,可以進行獨立部署,並在多個團隊之間並行化開發,所有這些都通過一個大家可以依賴的約定API進行集成。

 

但是,如果所有這些都託管在同一個存儲庫中,很多好處就不復存在了。

 

 

圖源:unsplash

 

雖然最初看起來monorepos並沒有與monolith相同的問題,並且還可以單獨維護包,但是當進一步檢查這些存儲庫時,monolith變得非常明顯。通常有一個複雜的依賴關係樹,其中所有的包都傾向於相互依賴才能發揮功能。

 

如果對其中一個包進行更改,可能會對使用該包的包產生連鎖反應,而這些包本身必須更新和發佈。通常在monorepos中,包的功能是非常特殊的,那麼問題就變成了如果它是緊密耦合的,爲什麼還要有一個單獨的包呢?可以獨立使用這些包嗎?或者與monorepo中其他包的特定版本綁定?它可能更容易卸下僞裝,就和momolith一樣。

 

只有一個包使用了Parcel包,就是它本身。

 

 

包的開銷

 

當查看node_modules目錄時,即使對於一個相對基本的應用程序,也可能有數百甚至數千個包。通常這樣的包只包含幾行代碼,並附帶LICENCE、README和package.json文件。

 

這是一筆令人難以置信的開銷和浪費。包會消耗更多的硬盤空間,增加安裝時間,並且在功能上變得更加模糊,以至於有些名稱就直接描述了它們的功能。

 

節點項目中非常常見的依賴項。需要更少的這種類型的包。

 

Monorepos放大了這個問題。它們常常不必要地將功能分割到一個單獨的包中。如果一個包的惟一實際使用者是monorepo,並且不能實際地看到普通用戶在這個存儲庫中的138個其他包中安裝那個包,可能並沒有必要將它作爲一個單獨的包。理想情況下,最好讓用戶安裝一個包含所有內容的包,並減少開銷。

 

 

開發人員的困惑

 

許多monorepos將包發佈到npm上,這可能會導致一些問題。第一個問題是,如果希望開發人員安裝其中的一些軟件包,版本號可能會混淆。如果包是緊密耦合的,那麼弄清楚包與包的搭配使用可能會令人沮喪。

 

一些monorepos通過保持版本號的同步來解決這個問題,但是如果正在這樣做,就再次引發了爲什麼值得創建單獨的包的問題。

 

一個沒有文檔的公開的Babel包。

 

另一個問題是,發佈單獨的包會暴露私有功能。儘管希望用戶不要使用未歸檔的功能,但是如果有方法訪問它,用戶就會使用它。這迫使開發人員在特定的實現細節上保持向後兼容性。

 

如果要大量修改軟件包,則可能僅由於某些人可能依賴未公開API中存在的那個軟件包而不得不增加主版本號。

 

圖源:unsplash

 

 

跟蹤Git歷史

 

Git存儲庫中的歷史提交可能非常重要,特別是如果需要了解包是如何隨時間變化的,以及是否需要還原一些已經做出的更改。有些人會認爲monorepos的一個優點是可以同時恢復所有包,這樣它們就具有相同的兼容性。

 

這是一個很好的觀點,但是它只簡化了版本控制的一個方面,而犧牲了其他方面。

 

對筆者來說,大多數情況下想還原單個包,或者檢查對該包所做的更改。在monorepo的環境中,這可能變得更具挑戰性。必須開始對搜索應用過濾器,但是考慮到monorepo中的包是緊密耦合的,仍然需要查看在數百個不相關的包中對其他相關包所做的更改。

 

圖源:unsplash

 

值得注意的是,Git的設計並不適合在monorepo級別上工作。存儲庫中的文件和提交越多,使用Git執行任何基本命令的速度就越慢。Atlassian提供了關於這個主題的一些技術細節。

 

 

現在有ESM模塊

 

monorepos之前存在並擁有多個微包的原因之一是爲了改進綁定,確保沒有使用的功能不會綁定到應用程序中。

 

Lodash這樣的庫很好地推廣了這種模式。如果只想使用一小段Lodash代碼,可以單獨導入該包以排除其餘的Lodash代碼。

 

然而,隨着tree-shaking在捆綁程序中變得常見,它們開始被棄用。由於現在ESM支持的無處不在,包括NodeJS,所以沒有理由再使用單獨的包來減少包的大小。

 

 

私有嵌套包

 

儘管如此,仍然有理由考慮在存儲庫中使用一個單獨的包。它可以幫助開發人員簡化導入和捆綁程序,而不需要在任何地方發佈這些包。PreactCompat就是一個很好的例子。

 

如果有用戶可以導入的可選文件,但又不希望用戶必須引用特定的JavaScript文件,希望捆綁程序自動爲環境選擇正確的格式,那麼使用單獨的package.json就可以了。

 

 

在上面的例子中,捆綁程序可以使用簡化的路徑,而不是直接指向文件,還可以根據包元數據決定是否使用UMD或ESM版本的文件。

 

 

結論

 

就像monorepos過度工程化並將太多的特性分離到包中一樣,將代碼分割到太多的存儲庫中也是如此。當一種模式比另一種模式更有意義時,那還有什麼好討論的呢?

 

需要進行成本效益分析,並將該特性作爲一個單獨的包放在一個存儲庫中,而不是將其作爲一個可以導入的單獨文件,或者完全放在一個單獨的存儲庫中,這樣做的好處是什麼?總是需要考慮維護開銷?

 

圖源:unsplash

 

就筆者個人而言,基於上面列出的所有原因,monprepos並不是前進的道路,相反它們應該被避免。也希望能幫助你們避免掉“坑”。

 

留言 點贊 關注

我們一起分享AI學習與發展的乾貨
歡迎關注全平臺AI垂類自媒體 “讀芯術”

(添加小編微信:dxsxbb,加入讀者圈,一起討論最新鮮的人工智能科技哦~)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章