本週精讀的文章是 The many Benefits of Using a Monorepo。html
如今介紹 Monorepo 的文章不少,能夠分爲以下幾類:直接介紹 Lerna API 的;介紹如何從獨立倉庫遷移到 Lerna 的;經過舉例子說明 Monorepo 重要性的。前端
本文屬於第三種,從 Android 與 IOS 的開發故事說明了 Monorepo 的重要性。node
筆者之因此選擇這篇文章,不是由於其故事寫的好,而是承認這種具備普適性的解決思路。畢竟 Lerna 做爲 Monorepo 的實現之一也並不盡善盡美,而不一樣場景對 Monorepo 依賴的緣由、功能也有所不一樣,因此但願借這篇文章,從理論上解釋清楚爲何會產生 Monorepo,以及 Monorepo 能夠解決哪些問題,這樣在工做遇到問題時,才能想清楚本身要的是什麼。android
做者的一個項目是 PDF 服務,簡稱 PSPDFKit,須要同時兼顧 Android 與 IOS 平臺,項目的發展經歷了以下幾個階段。webpack
在 2011 到 2013 年間,PSPDFKit 僅支持 IOS 平臺,但最終項目須要支持 Android,所以開了一個新倉庫放置 Android 代碼。Android 倉庫的代碼不只在 UI 上不一樣,同時解析 PDF 文檔的核心代碼也不一樣,這是由於 IOS 平臺上使用內置 PDF 渲染引擎同時作了一些業務拓展,但使用的 OC 代碼沒法在 Android 使用。ios
最終新建了兩個倉庫 PSPDFKit-Android
與 Core
。git
倉庫 Core 中代碼依賴 Android 平臺 JNI 的支持,因此並不能實現 Core 一處修改,兩處都生效的願望,而咱們又但願兩邊功能始終兼容,且減小分支過多帶來的潛在的衝突,所以花了好久才意識到應該將這兩個倉庫合併起來。github
因爲 Android 的整套流程本身控制的,所以老是能夠快速修復用戶提出的 BUG,然而 IOS 提供的 CGPDF 總會趕上各類問題。因此在 2014 年,咱們開啓了一個龐大的項目,重寫 IOS 的 Core 庫。有三中方式可供選擇:web
PSPDFKit-Android
。PSPDFKit-Android
提取到 Core
倉庫中並分別維護。通過討論,最終做者的團隊選擇了第三種方案,所以目錄結構相似以下:npm
- ios-platform
- android-platform
- core
複製代碼
Web 與後臺服務代碼一直是一個特例,咱們認爲這些內容相對獨立,因此沒有將其代碼放置到 Monorepo 中。
直到一年後,開始探索 WebAssembly 時,PSPDFKit-web 模塊就出現了,由於能夠利用 WebAssembly 將 Core 的代碼編譯並在 Web 平臺使用,所以 Core 倉庫與 Web 倉庫的關係變得很是緊密,最終,咱們將 Web、Server 也都遷移到 Monorepo 中了。
Monorepo 瑕不掩瑜,但做者仍是列舉了一些缺陷。
因爲源碼在一塊兒,倉庫變動很是常見,存儲空間也變得很大,甚至幾 GB,CI 測試運行時間也會變長。即使如此,團隊中任何人都不想回到 git submodules 多倉庫的方式。
總的來講,雖然拆分子倉庫、拆分子 NPM 包(For web)是進行項目隔離的自然方案,但當倉庫內容出現關聯時,沒有任何一種調試方式比源碼放在一塊兒更高效。
工程化的最終目的是讓業務開發能夠 100% 聚焦在業務邏輯上,那麼這不只僅是腳手架、框架須要從自動化、設計上解決的問題,這涉及到倉庫管理的設計。
一個理想的開發環境能夠抽象成這樣:
「只關心業務代碼,能夠直接跨業務複用而不關心複用方式,調試時全部代碼都在源碼中。」
在前端開發環境中,多 Git Repo,多 Npm 則是這個理想的阻力,它們致使複用要關心版本號,調試須要 Npm Link。
另外對於多倉庫的缺點,文中還有一些沒有提到的因素,這裏一併列舉出來:
管理、調試困難
多個 git 倉庫管理起來自然是麻煩的。對於功能相似的模塊,若是拆成了多個倉庫,不管對於多人協做仍是獨立開發,都須要打開多個倉庫頁面。
雖然 vscode 經過 Workspaces 解決多倉庫管理的問題,但在多人協做的場景下,沒法保證每一個人的環境配置一致。
對於共用的包經過 Npm 安裝,若是不能接受調試編譯後的代碼,或每次 npm link 一下,就沒有辦法調試依賴的子包。
分支管理混亂
假如一個倉庫提供給 A、B 兩個項目用,而 B 項目優先開發了功能 b,沒法與 A 項目兼容,此時就要在這個倉庫開一個 feature/b
的分支支持這個功能,而且在將來合併到主幹同步到項目 A。
一旦須要開分支的組件變多了,且之間出來依賴關聯,分支管理複雜度就會呈指數上升。
依賴關係複雜
獨立倉庫間組件版本號的維護須要手動操做,由於源代碼不在一塊兒,因此沒有辦法總體分析依賴,自動化管理版本號的依賴。
三方依賴版本可能不一致
一個獨立的包擁有一套獨立的開發環境,難以保證子模塊的版本和主項目徹底一直,就存在運行結果不一致的風險。
佔用總空間大
正常狀況下,一個公司的業務項目只有一個主幹,多 git repo 的方式浪費了大量存儲空間重複安裝好比 React 等大型模塊,時間久了可能會佔用幾十 GB 的額外空間,對於沒有外接硬盤的同窗來講,按期清理不用的項目下 node_modules
也是一件麻煩事。
不利於團隊協做
一個大項目可能會用到數百個二方包,不一樣二方包的維護頻率不一樣,權限不一樣,倉庫位置也不一樣,主倉庫對它們的依賴方式也不一樣。
一旦其中一個包進行了非正常改動,就會影響到整個項目,而咱們精力有限,只盯着主倉庫,每每會栽在不起眼的二方包發佈上。
因此對於一個很是複雜,又具備技術挑戰的大型系統在協做人員多的狀況下出現問題的機率很是大,須要經過 Review 制度避免錯誤的發生,那麼將全部相關的源碼聚合在一個倉庫下,是更好管理的。
參考 Lerna 的規範,以 packages
做爲子模塊根文件夾,筆者設計一個理想的 monorepo 結構:
.
├── packages
│ ├─ module-a
│ │ ├─ src # 模塊 a 的源碼
│ │ └─ package.json # 自動生成的,僅模塊 a 的依賴
│ └─ module-b
│ ├─ src # 模塊 b 的源碼
│ └─ package.json # 自動生成的,僅模塊 b 的依賴
├── tsconfig.json # 配置文件,對整個項目生效
├── .eslintrc # 配置文件,對整個項目生效
├── node_modules # 整個項目只有一個外層 node_modules
└── package.json # 包含整個項目全部依賴
複製代碼
全部全局配置文件只有一個,這樣不會致使 IDE 遇到子文件夾中的配置文件,致使全局配置失效或異常。node_modules
也只有一個,既保證了項目依賴的一致性,又避免了依賴被重複安裝,節省空間的同時還提升了安裝速度。
兄弟模塊之間經過模塊 package.json
定義的 name
相互引用,保證模塊之間的獨立性,但又不須要真正發佈或安裝這個模塊,經過 tsconfig.json
的 paths
與 webpack
的 alias
共同實現虛擬模塊路徑的效果。
再結合 Lerna 根據聯動發佈功能,使每一個子模塊均可以獨立發佈。
Lerna 是業界知名度最高的 Monorepo 管理工具,功能完整。但因爲通用性要求很是高,須要支持任意項目間 Monorepo 的組合,所以在 packages
文件夾下的配置文件仍是與獨立倉庫保持一致,這樣在 TS 環境下會形成配置截斷的問題。同時包之間的引用也經過更通用的 symlink 完成,這致使了仍是要在子模塊目錄存在 node_modules
文件夾,並且效果依賴項目初始化命令。
若是加一些限定條件,好比基於 Webpack + Typescript 環境的 Monorepo,能夠換一套思路,利用這些工具自身運行時功能,減小更多模版代碼或配置文件,進一步提高 Monorepo 的效果。
對於別名映射,對 symlink 與 alias 進行對比:
node_modules
文件夾。可見若是限定了構建器,別名映射能夠作得更輕量,且無需初始化。
今天的問題是,你的項目須要使用 Monorepo 嗎?你對 Monorepo 有其餘要求嗎?
若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公衆號
special Sponsors
版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)