寫在最前
本故事簡要地介紹了 Monorepo 的 What 和 Why,重點篇幅在于搭建一個好用的 Monorepo 工程時應(yīng)該考慮的點刊侯≌掳欤可以作為你在選擇工具時的條件,也可以作為你在搭建 Monorepo 工程時查漏補缺的參考滨彻。希望這對你有所幫助藕届,哪怕只是一點點 ^O^
“在這個 AI 內(nèi)容生成泛濫的時代,依然有一批人"傻傻"堅持原創(chuàng)亭饵,如果您能讀到最后休偶,還請點贊或收藏或關(guān)注支持下我唄,感謝 ( ̄︶ ̄)↗”
What辜羊?- 獨立和關(guān)系
丹尼爾:蛋兄踏兜,好久不見,今天我們來聊聊 Monorepo 吧八秃!
蛋先生:Monorepo碱妆?就是把多個項目放在同一個倉庫里的那種嗎?
丹尼爾:是呀昔驱,感覺上就是把一堆代碼庫簡單地堆在一起
蛋先生:你說得不太準(zhǔn)確疹尾,我覺得 Monorepo 最重要的是倆關(guān)鍵字:獨立和關(guān)系
丹尼爾:怎么說?
蛋先生:獨立是指這些項目本身是完整的骤肛,一般都擁有開發(fā)纳本、測試,發(fā)布等完整的生命周期腋颠,而不是簡單的包含一堆代碼文件的文件夾
丹尼爾:哦繁成,這個我明白了。那關(guān)系呢秕豫?
蛋先生:關(guān)系是指這些項目之間存在一定的關(guān)聯(lián)朴艰,比如它們屬于同一個業(yè)務(wù)領(lǐng)域观蓄,或是有依賴關(guān)系,而不是毫無關(guān)聯(lián)地硬堆在一起
丹尼爾:懂了祠墅!
Why侮穿?- 更好地協(xié)作
丹尼爾:那用這個 Monorepo 有什么好處呢?我以前一個項目一個倉庫不也挺好的嗎毁嗦?
蛋先生:這里科普一下亲茅,一個項目一個倉庫有一個專用的名詞叫 Polyrepo。我認(rèn)為 Monorepo 最關(guān)鍵的好處在于項目與項目之間的協(xié)作
丹尼爾:怎么說呢狗准?
蛋先生:比如共享代碼以減少重復(fù)工作方面克锣。當(dāng)你在開發(fā)應(yīng)用 B 時,如果發(fā)現(xiàn)應(yīng)用 A 中已經(jīng)實現(xiàn)了很多相似的邏輯腔长,那么你需要把共享邏輯抽取到一個獨立的庫 α袭祟,然后修改應(yīng)用 A 和應(yīng)用 B 以依賴于庫 α,因為這一切都在同一個倉庫中完成捞附,非常方便巾乳,操作成本較低
丹尼爾:確實,如果采用 Polyrepo 的方式鸟召,我得新建一個倉庫胆绊,把共享邏輯抽取出來,然后通過本地 link 的方式來開發(fā)調(diào)試欧募。一切就緒后压状,還得發(fā)布到 npm,再在應(yīng)用 A 和應(yīng)用 B 中安裝依賴跟继。而且每次修改都需要重復(fù)這個過程种冬,真是麻煩
蛋先生:再比如庫修改可能導(dǎo)致項目不穩(wěn)定方面。當(dāng)一個被依賴的庫進(jìn)行迭代升級時还栓,特別是有大的變更時碌廓,如果沒有及時溝通以采取相應(yīng)的措施,就會導(dǎo)致各種問題剩盒,潛在的風(fēng)險非常大
丹尼爾:Monorepo 不會有這個問題嗎谷婆?
蛋先生:在 Monorepo 中修改是原子的,即當(dāng)你修改庫 α 時辽聊,同倉庫的應(yīng)用 A 和應(yīng)用 B 都能及時感知到變更纪挎。例如,你刪除了某個接口的入?yún)?shù)跟匆,應(yīng)用 A 和應(yīng)用 B 會立刻報錯异袄,這樣就能及時發(fā)現(xiàn)并解決潛在風(fēng)險
丹尼爾:這樣確實挺棒的
蛋先生:最根本的原因是 Polyrepo 帶來了隔離,而隔離影響了協(xié)作玛臂。Monorepo 的目標(biāo)則是為了更好地協(xié)作烤蜕。就像部門間協(xié)作和部門內(nèi)協(xié)作封孙,顯然同一個部門內(nèi)的協(xié)作效率更高,溝通成本也更低
丹尼爾:一語中的讽营!
How虎忌?- 舒適地開發(fā)
? 初始化階段 - 腳手架
丹尼爾:那采用 Monorepo 的形式來組織項目,我應(yīng)該怎么做呢橱鹏?
蛋先生:我們一起來走一走應(yīng)用開發(fā)的歷程膜蠢,看看需要有哪些工作吧
丹尼爾:好啊
蛋先生:有兩種開局方式。一種是全新開始莉兰,這樣的話你需要一個能生成 Monorepo 大倉的腳手架
丹尼爾:恩挑围,很體貼
蛋先生:不過這種情況發(fā)生的概率較低,通常是一次性的糖荒。更常見的是在已有的 Monorepo 倉庫中增加新項目杉辙。這是經(jīng)常需要做的事情,所以我們可以提供多種腳手架代碼生成器來快速初始化一個項目寂嘉,比如創(chuàng)建 TS 工具庫項目奏瞬、React 應(yīng)用項目,或者是 TS CLI 項目等等
丹尼爾:確實泉孩,常用的項目類型是可以枚舉出來的。有了這些工具并淋,后續(xù)增加項目就輕松多了寓搬,想想就很爽!那另一種開局呢县耽?
? 初始化階段 - 依賴安裝
蛋先生:另一種開局是你準(zhǔn)備在一個已存在的 Monorepo 大倉上進(jìn)行開發(fā)工作句喷。這時,你的第一件事應(yīng)該是安裝依賴兔毙,對嗎唾琼?
丹尼爾:恩,沒錯
蛋先生:不過澎剥,大倉里可能有很多項目锡溯,你總不能一個一個項目進(jìn)行安裝依賴吧,所以需要有一個可以一次性安裝全部項目依賴的能力
丹尼爾:對啊哑姚,我可不想把時間浪費在一個個項目里 cd 來 cd 去的
? 開發(fā)階段 - 任務(wù)編排
蛋先生:無論哪種開局祭饭,接下來都是進(jìn)入到開發(fā)階段了。假設(shè)你在開發(fā)應(yīng)用 A叙量,而應(yīng)用 A 依賴庫 α倡蝙,那么你是不是得先確保庫 α 有可用的構(gòu)建產(chǎn)物?
丹尼爾:是啊绞佩,所以第一步就是得知道應(yīng)用 A 依賴了哪些同倉庫中的其他庫寺鸥,并且提前對它們進(jìn)行構(gòu)建猪钮。但如果依賴關(guān)系比較復(fù)雜,就難搞了
蛋先生:正是如此胆建。所以烤低,我們希望能夠不用手動處理這些依賴,只要對應(yīng)用 A 進(jìn)行構(gòu)建眼坏,就能自動處理它所依賴的所有庫的構(gòu)建
丹尼爾:那就太好了拂玻!
蛋先生:這就需要任務(wù)編排了。我們可以配置任務(wù)之間的協(xié)作關(guān)系宰译,比如在執(zhí)行某個任務(wù)之前檐蚜,需要先執(zhí)行哪些任務(wù),這些任務(wù)是串行還是并行執(zhí)行等等
丹尼爾:哦沿侈,任務(wù)編排還真好用
? 開發(fā)階段 - 一致命令
蛋先生:好了闯第,萬事俱備,你可以開始本地開發(fā)調(diào)試了
丹尼爾:哦缀拭,那我先看看項目的 README咳短,找找本地開發(fā)調(diào)試的指引
蛋先生:不用那么麻煩,直接執(zhí)行 dev 命令吧蛛淋。無論你是在開發(fā)應(yīng)用項目還是庫項目咙好,無論是用 JavaScript 還是 Java,開發(fā)就運行 dev褐荷,構(gòu)建就運行 build勾效,測試就運行 test,等等叛甫。這樣你就不會有任何心智負(fù)擔(dān)
丹尼爾:哈哈层宫,老早就想這樣了
? 開發(fā)階段 - 影響檢測
蛋先生:開發(fā)過程中,你發(fā)現(xiàn)依賴的庫 α 提供的接口有點小問題其监,現(xiàn)在你準(zhǔn)備對應(yīng)用 A 所依賴的庫 α 進(jìn)行修改
丹尼爾:哦萌腿,反正都是在同一個倉庫,修改起來挺方便的
蛋先生:但我們得確保這個改動不會影響到依賴該庫的其他項目抖苦。至少在我們可控的范圍內(nèi)毁菱,比如同一倉庫中依賴該庫的其他項目。所以睛约,我們需要一種自動檢測機(jī)制來識別哪些項目受到了影響鼎俘,然后對這些受影響的項目進(jìn)行單元測試等操作,以確保它們的穩(wěn)定性
? 開發(fā)階段 - 依賴分析
丹尼爾:蛋兄果然很謹(jǐn)慎啊
蛋先生:咳咳~辩涝。其實贸伐,這一切都需要借助依賴分析能力。當(dāng) Monorepo 的規(guī)模越來越大時怔揩,依賴關(guān)系也會變得越來越復(fù)雜捉邢。我們需要通過依賴關(guān)系圖,清晰地了解各項目之間的聯(lián)系和影響伏伐,從而做到對項目狀況了如指掌
? 開發(fā)階段 - 依賴權(quán)限
蛋先生:你現(xiàn)在是庫 α 的主要負(fù)責(zé)人。有一天藐翎,你發(fā)現(xiàn)了一些并不想對外暴露的 API 被倉庫內(nèi)的其他項目使用,結(jié)果你在修改這些 API 時就不得不考慮對這些項目的影響
丹尼爾:啊吝镣,雖然我是聲明了 export,但這只是為了庫內(nèi)部的其他代碼使用末贾≌⒗#可其他項目卻可以通過深層導(dǎo)入來依賴這些 API
蛋先生:嗯,所以我們需要在工程層面上建立機(jī)制拱撵,防止這些 API 被誤依賴
? 開發(fā)階段 - 修改權(quán)限
蛋先生:庫 α 雖說是由你主要負(fù)責(zé)的辉川,但是由于代碼庫是放在一起的,其他擁有大倉權(quán)限的同學(xué)也就有權(quán)限進(jìn)行修改拴测。但是你并不希望他們隨意修改庫 α 的代碼乓旗,至少要經(jīng)過你的同意
丹尼爾:是啊是啊,這真的很重要集索!
蛋先生:所以我們需要引入類似 OWNER 的機(jī)制寸齐,對這些修改權(quán)限進(jìn)行限制浇雹,以確保代碼的穩(wěn)定性和一致性
? CI 階段 - 本地計算緩存
“注:CI 階段的能力谆刨,不僅僅只用于 CI惊畏,開發(fā)階段也是可以享用,只是為了劇情需要這么安排而已”
蛋先生:好了蛹含,項目修改完畢,提交塞颁。CI 開始工作了浦箱,然后你發(fā)現(xiàn)每次 CI 構(gòu)建都非常慢
丹尼爾:嗯,我加點戲哈祠锣。我喝了一杯咖啡酷窥,再回來一看,好家伙伴网,CI 還在跑蓬推。這樣可不行,得優(yōu)化性能了澡腾,不然我快要崩潰了
蛋先生:好吧沸伏,這戲加得... 回到正題糕珊。這是因為該項目直接或間接依賴了同一倉庫中的好幾個其他庫。所以毅糟,每次構(gòu)建實際上都需要構(gòu)建多個項目红选。優(yōu)化性能的思路之一就是減少不必要的計算,增量執(zhí)行就變得非常重要姆另。因此喇肋,我們需要引入本地計算緩存,緩存計算結(jié)果迹辐,避免對沒有修改的庫進(jìn)行重復(fù)構(gòu)建
丹尼爾:本地緩存蝶防,我懂
? CI 階段 - 分布式任務(wù)執(zhí)行
蛋先生:性能優(yōu)化的另一個思路是加速必要的計算
丹尼爾:昨加速捏?
蛋先生:可以采用分布式任務(wù)執(zhí)行右核。將一些可以并發(fā)執(zhí)行的任務(wù)分配到不同的服務(wù)器上并行處理慧脱,實現(xiàn)在更短的時間內(nèi)完成任務(wù)。這樣做雖然會增加一定的成本菱鸥,但對于大型項目來說躏鱼,是非常有效的性能提升方案
丹尼爾:聽上去好高級的樣子
? CI 階段 - 遠(yuǎn)程計算緩存
蛋先生:雖然使用了本地緩存染苛,但每個服務(wù)器都需要先構(gòu)建一次才能生成本地緩存。如果我們把緩存的位置移到遠(yuǎn)程云端躯概,是不是就可以進(jìn)一步優(yōu)化性能呢娶靡?
丹尼爾:Nice! 這樣就可以共享緩存了
? 發(fā)布階段
蛋先生:最后看锉,我們需要把庫 α 發(fā)布到 npm 上去伯铣,因為它提供的功能非常通用,不僅僅局限于當(dāng)前的項目倉庫內(nèi)
丹尼爾:那就趕緊發(fā)布吧焚鲜!
蛋先生:發(fā)布階段,根據(jù)需要郑兴,利用任務(wù)編排就可以了
新的問題
丹尼爾:聽起來 Monorepo 灰常好啊情连,都使用這種方式得了
蛋先生:Monorepo 確實突破了 Polyrepo 的隔離問題览效,但這樣開放的結(jié)構(gòu)也帶來了讀權(quán)限的問題锤灿。如果你的大倉中的部分項目需要由第三方團(tuán)隊來開發(fā),但你又不希望他們能看到其它項目的內(nèi)容螃诅,那么 Monorepo 就無法解決這個問題了
丹尼爾:啊术裸,那怎么辦呢亭枷?
蛋先生:這種情況下叨粘,你可以考慮將這些項目作為 git submodule 分離出去升敲。這樣一來,大倉中的其它項目仍然可以在工作空間內(nèi)直接依賴這些分離出去的 git submodule 項目
丹尼爾:那有啥需要注意的地方嗎苇羡?
蛋先生:要注意的是,git submodule 的項目就不能通過工作空間直接依賴大倉中的其它項目了锦茁,它們需要通過 npm 中央倉庫來進(jìn)行依賴管理
丹尼爾:好咧码俩,這一聊,天色已晚
蛋先生:嗯笨篷,今天就先聊到這里率翅,就此別過吧
丹尼爾:拜拜!
寫在最后
為什么不直接寫一個使用某個工具(比如 Turborepo)來搭建 Monorepo 項目的教程呢腺晾?因為我相信聰明的你悯蝉,只需要閱讀官方文檔鼻由,就可以輕松上手了
不同工具工作方式有所不同蕉世,但都是圍繞 Monorepo 來提供能力的窟感。我們應(yīng)以不變應(yīng)萬變,掌握表面之下的東西哈误,這樣才能更加靈活地應(yīng)對各種變化
“親們蜜自,都到這了重荠,要不虚茶,點贊或收藏或關(guān)注支持下我唄 o( ̄▽ ̄)d”