該用 Monorepo 嗎?比較 Monolith vs Multi-Repo vs Monorepo
28 Jan 2023專案隨著開發時間愈長,伴隨而來的除了體積和複雜度增加之外,也產生難以擴充、缺乏彈性以及打包和部署時間長、效率差等問題。這時候就會開始考慮切分專案,在這裡來聊聊三種管理專案的架構 - Monolith、Multi-Repo 與 Monorepo,這篇文章會談到過去在建置專案時遇到的問題,以及根據不同情境而選用的解法,並在最後推薦工具與提供利用 Module Federation 達成 Micro Frontends 的範例來快速上手和總結比較。
Monolith
首先來看的是 Monolith。過去在建置專案時,大多選用 Single-Repo Monolith 的架構,意即將所有的功能都統一放在同一個 repository (簡稱 repo) 底下。如下圖所示,在 WebGether 這個產品的 repo 底下,包含 News、Mall 和 Chat 功能。
檔案配置示意如下。
├── assets
├── components
│ ├── Button.js
│ ├── Modal.js
│ └── ...
├── node_modules
├── pages
│ ├── Chat
│ │ ├── Contact.js
│ │ ├── Message.js
│ │ └── ...
│ ├── Mall
│ │ ├── Cart.js
│ │ ├── Product.js
│ │ └── ...
│ ├── News
│ │ ├── Article.js
│ │ ├── Topic.js
│ │ └── ...
├── utils
├── package.json
├── jest.config.js
├── webpack.config.js
├── yarn.lock
└── README.md
這在產品開發初期或非大型規模專案,的確是個簡單方便的好方法。在團隊與產品不斷擴張的狀況下,這個 repo 變得超級肥大,導致打包和部署時間長、效率差;又因為不同團隊可能會用到不同的環境建置與技術線 (tech stack) 而造成相依管理 (dependency management) 困難、難以擴充、缺乏彈性。
例如:WebGether 原本統一使用 React v15 作為前端開發框架,並且用 Redux 作為狀態管理工具,但某天 News 團隊希望改用潮到出水的 Vue,Mall 團隊希望能用 useReducer + useContext 取代 Redux,Chat 團隊無法接受 React 升版 v16.8 導致某最愛的 package 失效。
這時候就要考慮分家了,將不同功能切分為不同專案,切分方式可分為兩種 - Multi-Repo 與 Monorepo。
Multi-Repo
Multi-Repo (或稱 Polyrepo、Many-repo) 是指將個別功能放在不同 repo 底下,如前面的例子,在拆分 News、Mall 和 Chat 後,這三個功能便擁有各自獨立的 repo,同時也能擁有各自的環境和工具的設定檔與技術線。
Chat 的檔案配置示意如下。
├── assets
├── components
│ ├── Button.js
│ ├── Modal.js
│ └── ...
├── node_modules
├── pages
│ ├── Contact.js
│ ├── Message.js
│ └── ...
├── utils
├── package.json
├── jest.config.js
├── webpack.config.js
├── yarn.lock
└── README.md
Mall 的檔案配置示意如下。
├── assets
├── components
│ ├── Button.js
│ ├── Modal.js
│ └── ...
├── node_modules
├── pages
│ ├── Cart.js
│ ├── Product.js
│ └── ...
├── utils
├── package.json
├── jest.config.js
├── webpack.config.js
├── yarn.lock
└── README.md
News 的檔案配置示意如下。
├── assets
├── components
│ ├── Button.js
│ ├── Modal.js
│ └── ...
├── node_modules
├── pages
│ ├── Article.js
│ ├── Topic.js
│ └── ...
├── utils
├── package.json
├── jest.config.js
├── webpack.config.js
├── yarn.lock
└── README.md
於是 News 果真改用 Vue 來做前端開發的框架,Mall 不再需要遷就 Chat 的守舊而能快樂使用 React Hooks 以及 Chat 可以繼續使用他們最愛的恐龍級 package,皆大歡喜 ヽ(●´∀`●)ノ
由此看來,使用 Multi-Repo 帶來的優點是…專案體積小、高效率開發、技術線獨立、相依管理簡單、高彈性,因此權責切分乾淨、不同框架時更顯優點。
但缺點就是優點的反面,像是…
- 不易共享,像是環境與設定檔重複配置 (CI/CD、webpack、test suite 等)、資源難以共用。舉例來說,某天 News 團隊開發了一個很棒的 sticker footer 元件,過去在 Monolith 時各團隊只要引入這個元件即可共用,但現在可能要請 News 團隊將這個元件打包成 NPM package 或抽到共用的元件的 repo 再用 Git Submodule 再引用的方式才能共用,共用的困難度增加。關於選 NPM package 還是 Git Submodule,個人經驗來說差異為打包、發佈和更新上的不同,端看團隊用起來誰比較順手就好,這個討論串也許能提供一些參考。
- 當資源更新時,個別 repo 難以被通知而及時更新。
- 修 bug 或是 i18n 字串更新時若涉及多個 repo,處理與測試的困難度也會變高。
- 建構、測試與發佈流程上甚至是 rollback 都較困難。
與 Multi-Repo 這個概念的相似解法,可參考 Git Submodule 或 Git Subtree。Git Submodule 是指建立 main repo 與 sub repo 的 HEAD commit 連結,而 Git Subtree 是指將 main repo 包含 commit log 全部 copy 到新的 repo 中。
看完 Multi-Repo,有沒有什麼能同時兼顧彈性與共用的解法呢?接下來看 Monorepo。
Monorepo
檔案配置示意如下。
├── shared
│ ├── assets
│ ├── components
│ │ ├── Button.js
│ │ ├── Modal.js
│ │ └── ...
│ ├── utils
│ └── ...
├── apps
│ ├── chat
│ │ ├── components
│ │ ├── pages
│ │ │ ├── Contact.js
│ │ │ ├── Message.js
│ │ ├── utils
│ │ └── ...
│ ├── chat-e2e
│ ├── mall
│ │ ├── components
│ │ ├── pages
│ │ │ ├── Cart.js
│ │ │ ├── Product.js
│ │ ├── utils
│ │ └── ...
│ ├── mall-e2e
│ ├── news
│ │ ├── components
│ │ ├── pages
│ │ │ ├── Article.js
│ │ │ ├── Topic.js
│ │ ├── utils
│ │ └── ...
│ └── news-e2e
├── node_modules
├── package.json
├── jest.config.js
├── webpack.config.js
├── yarn.lock
└── README.md
Monorepo 將個別功能放在同一 repo 的不同 package 底下,如前面的例子,在拆分 News、Mall 和 Chat 後,這三個功能雖然仍在同一個 repo 底下,但放置於不同的 pacakge 資料夾。
這麼做的優點是…
- 能共享資源,在同框架時更顯優勢。
- 當資源更新時,個別 app 能夠及時被通知而更新。
- 能共享配置,像是共享環境設定和 config 檔,不需每個 app 再自建一套。這在建構、測試與發佈流程上甚至是 rollback 是很方便的。
就個人開發經驗來說,會依據專案的團隊成員組成、實作細節與維運的複雜度來決定要選用 Multi-Repo 還是 Monorepo 的架構。簡單來說,需要高度共用、緊密合作的狀況下會選用 Monorep;但若功能差異大、彼此沒什麼交集、不想被彼此影響,就會選用 Multi-Repo。看來 Monorepo 在易於分享配置與資源的同時,也能擁有各自想要的彈性,可說是共享與獨立兼顧。
但缺點就是…
- 跟 Monolith 同樣有 repo 龐大的問題,這在 codebase 很大的狀況下很可能造成 Git 效能差 (細節和解法參考這裡)、SourceTree 當掉;雖然說開發人員可自行設定 filter 來偵測 repo 更新和決定部署哪個 package,但開發平台像是 Netlify 不會判斷是哪個 app 有更新,只要 repo 有 change 就會 trigger deploy,repo 愈大開發與部署成本愈高,對這個議題和解法有興趣的可參考那裏。
- 是否共用要切分清楚,由於太容易共用了,在開發上需要制定明確規範,例如:利用 linter or dependency graph 限制共用元件或工具必須放在 shared package,不屬於自身 package 的 app 不能共用。這在開發和 PR review 時需要多多留心 codebase 的更新狀況和是否修改在適當的範圍之內。同樣的,對於專屬自身 app 想要安裝使用的東西就放在自家 package.json 裡面,記得注意 package version hoist 的狀況。
- 在開發人員眾多時難以控管檔案權限,無法針對 package 來限制誰能瀏覽或編輯,同時也會反應到開 issue、回覆 PR 和通知過於紊亂的問題上。
順道一提,有些專案是將前後端一同整合至 Monorepo 上,這樣的考量點可能是前後端的開發成員是同一批人、使用同一種語言或便於 Ops 統一管理與版控,優點是能做到有效的資源共享、前後端整合是很便利的,但也意味著複雜度變高,一旦 repo 出現問題前後端都會受影響等,關於這個議題這篇文章提到不錯的觀點與說明。個人經驗來說,由於大多待在前後端分離的團隊,開發人員、語言和環境差異甚大 (React vs Go),並且專案規模龐大,因此都是在這樣的配置下選擇此架構來達成 Micro Frontends。
快速上手
最後,在管理 Monorepo 的工具方面,推薦可用 PNPM、Lerna、Bit、NX 等,有選擇困難症的這裡有重點整理和比較;而 NX 提供了很棒的 Webpack Module Federation 範例,加上 Vercel 對 NX 支援也很友善 (還提供樣板) (註 1),可以參考看看。
總結
比較 Monolith、Multi-Repo 與 Monorepo 如下表。
# | Monolith | Multi-Repo | Monorepo |
---|---|---|---|
特色 | 將所有的功能放在單一 repo | 將個別功能放在不同 repo | 將個別功能放在同一 repo 的不同 package |
優點 | 簡單方便 | 1. 專案體積小、高效開發;2. 技術線獨立、相依管理簡單、高彈性 | 共享與獨立兼顧 |
缺點 | 1. 專案過於龐大,開發低效;2. 相依管理困難、難以擴充、缺乏彈性 | 共享不易 | repo 龐大、開發需明確規範、檔案權限難以控管、Git 效能差 |
工具 | - | - | PNPM Lerna Bit NX 等 |
適用情境 | 產品開發初期或非大型規模專案 | 切分大型專案、相依和共享狀況少 | 切分大型專案、相依和共享狀況多 |
相似解法 | - | Git Submodules 或 Git Subtree | - |
這些架構都有各自適用的狀況,評估後選用適合的解法即可 (๑•̀ㅂ•́)و✧
備註
- 註 1:部署 Monorepo 到 Netlify (同理 Vercel) 的設定可參考這篇文章。