前一期我們提到了後端進行Monorepo的架構設計(現代軟體架構的集權藝術: 將多個 Python 後端整併成 monorepo 架構的實戰心法),而Monorepo不僅僅可以用在後端,也可以用在前端設計,本期就透過前端的角度來看看可以怎麼應用,並且因為設計Monorepo要顧慮到很多問題,我們還可交由Agent幫我們分擔.
在前端,常常會有專案之間需要共用組件或是需要同時管理多個專案的需求,比如一個專案可能會有一個對外的前端網頁跟一個對內的,他們之間可能就會有共用組件、邏輯的需求,如果分成各自的repo各自維護的話,可能就有會有對外的組件更新但對內忘記更新的情況,這種時候就可以採用monorepo的架構。
1.什麼是Monorepo
monorepo由mono跟repo組成,mono為單一的意思,repo就是repository就是儲存庫,單一儲存庫意思就是由單一一個儲存庫去管理多個專案的架構,比如前面所述對外跟對內的例子,用monorepo的架構管理的話結構就會類似:
apps
|- public-frontend
|- src
|- internal-frontend
|- src
node_modules
packages
|- components
|- utils
兩個專案分開管理但是引用相同的node_modules、packages,讓你可以一次更新共用的部分,當然也可以分開部署,這就是使用monorepo的好處。
2.什麼是Turborepo
Turborepo 是由 Vercel 開發的一款高效能 Monorepo 建置系統(Build System),專為 JavaScript / TypeScript 專案設計。他有幾個核心特性:
- 智慧快取(Caching)
記住每次任務的輸出,若輸入未變動則直接使用快取,跳過重複建置。 - 並行執行(Parallelism)
同時執行多個互不依賴的任務,充分利用 CPU 多核心。 - 任務管線(Pipeline)
透過turbo.json定義任務之間的依賴順序(如先 build 再 test) - 遠端快取(Remote Caching)
快取可上傳至雲端,讓團隊成員或 CI/CD 共享,避免重複建置。 - 增量建置
只重新建置有變動的套件,而非整個 Monorepo。
3.遷移紀錄
剛好T編目前專案需要調整架構成monorepo所以趁著這個機會研究並練習。T編目前的專案是Next.js搭配react admin套件的一份後台管理系統,因為之後要將後台管理的部分複用並掛上不同的功能模組變成獨立專案,因此需要調整現有架構。
先以上面的資料結構為例,monorepo最重要的兩個資料夾就是apps 及packages ,apps負責存放個別的專案比如public-frontend ,packages則負責存放共用的部分比如components ,packages的概念就是把你們會共用到的組件、邏輯方法等等封裝成一個套件,並讓他們在各自專案內使用,所以記得當在新增/修改 package.json 的依賴時需要重新 npm install 。
基本上採用漸進式遷移的方式,在平常的開發流程中一步步地將架構遷移比起一下全部遷移完成比較不會佔到原專案功能進度。遷移中的一個大重點就是不要讓package去引用app內的東西,都是讓app去引用package的內容,比如:
在public-frontend裡
import {component} from "shared-packages"
但不要在shared-packages裡
import {AppComponent} from "public-frontend"
在 Monorepo 中,正確的依賴方向應該是單向的,如果 packages/ 反過來 import apps/,就會形成循環依賴(Circular Dependency),這麼做會導致幾個問題:
1. 循環依賴導致建置失敗
packages/utils import from apps/web
apps/web import from packages/utils
建置工具(webpack、tsc、turbo)無法決定誰先建置,直接報錯或進入無限迴圈。
2. 破壞 packages 的可重用性
packages/ 存在的意義就是被多個 app 共用:
apps/web ──┐
apps/mobile ──┼──→ packages/ui ✅ 正確
apps/admin ──┘
如果 packages/ui 依賴 apps/web,那 apps/mobile 用這個 package 時就會被迫拉進 apps/web 的程式碼,完全失去共用的意義。
3. 違反關注點分離(Separation of Concerns)
| 層級 | 職責 |
| apps/ | 特定產品的業務邏輯、路由、頁面 |
| packages/ | 通用、可重用的邏輯(UI、工具、型別) |
packages/ 應該對 apps/ 一無所知,否則它就不再是「通用」的了。
4. Turborepo 的 Pipeline 會出錯
Turborepo 根據依賴關係決定建置順序:
// turbo.json
"build": {
"dependsOn": ["^build"] // 先建置所有 dependencies
}
若 packages/ui 依賴 apps/web,Turbo 的 DAG(有向無環圖)就會出現環(cycle),直接報錯。
5. 測試與 CI 變得不可預測
- 改動
apps/web的一個小功能,卻意外觸發所有依賴packages/utils的測試 - 無法單獨測試 package,因為它需要整個 app 的環境才能運行
正確的架構設計原則
┌─────────────────────────────────┐
│ apps/ │ ← 只能往下依賴
│ web / mobile / admin │
└────────────┬────────────────────┘
│ import
┌────────────▼────────────────────┐
│ packages/ │ ← 不知道 apps 的存在
│ ui / utils / config / types │
└────────────┬────────────────────┘
│ import
┌────────────▼────────────────────┐
│ 外部 npm 套件 │
│ react / lodash / zod ... │
└─────────────────────────────────┘
黃金法則:依賴箭頭永遠朝下,絕不往上。
如果真的有共用需求及客製化的業務邏輯該怎麼辦?這個就需要自己去思考你的packages需要拆到多細,以T編的開發專案為例子,需要各個專案針對新增使用者的表單有不同欄位的需求,比如:
專案A:username, age, email
專案B:firstname, lastname, age
如果兩個專案需要的欄位都一樣,可以直接把整個新增使用者的流程都抽成packages讓兩個專案共用,但這次有客製化的需求,因此T編的思路是把packages當成一個工廠的概念,透過各個專案傳入不同的欄位CONFIG來轉換成不同的表單UI,比如:
// 專案A
const USER_CONFIG = [
{
label: "使用者名稱",
source: "username",
},
{
label: "年齡",
source: "age",
},
{
label: "電子信箱",
source: "email",
},
]
export function UserFormA(){
// packages裡的轉換函式
return <ConfigToUI columns={USER_CONFIG}/>
}
// 專案B
const USER_CONFIG = [
{
label: "名字",
source: "firstname",
},
{
label: "姓氏",
source: "lastname",
},
{
label: "年齡",
source: "age",
},
]
export function UserFormB(){
// packages裡的轉換函式
return <ConfigToUI columns={USER_CONFIG}/>
}
4. AI協作(Agent Skills)
因為這種migration通常會是大工程,所以勢必是需要agent的協助,但agent容易產出不一樣的output,因此這時可以選擇寫在claude.md或是cursor.md這種專案文件裡,或是可以寫一個skills來幫助我們做monorepo的migration。
.md vs skills
| 維度 | .md 集中寫法 | Agent Skills 模組化 |
|---|---|---|
| 載入方式 | 每次對話全部載入 | 按需載入,只載入匹配的 Skill |
| Context 佔用 | 規則越多,佔用越多 Token | 不用的 Skill 不佔 Context |
| 維護方式 | 改一個規則要找到那一行 | 直接編輯對應 Skill,互不影響 |
| 版本控制 | 整份檔案一起改 | 每個 Skill 獨立版本 |
| 可重用性 | 綁定在單一專案/Agent | Skill 可跨 Agent 共用 |
| 協作 | 多人改同一檔案容易衝突 | 各自負責各自的 Skill |
| 啟用/停用 | 要手動刪除或註解 | is_active 一鍵切換 |
skills最關鍵的優勢就是Context Window的效率
claude.md 做法:
┌─────────────────────────────────────┐
│ System Prompt │
│ ████████████████████████████████ │ ← 所有規則全塞進來
│ pptx規則 + xlsx規則 + pdf規則 + │
│ 圖片規則 + 影片規則 + 搜尋規則... │
│ (使用者只是問天氣,卻載入了全部) │
└─────────────────────────────────────┘
Agent Skills 做法:
┌─────────────────────────────────────┐
│ System Prompt │
│ ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ ← 只有基本指令
│ │
│ 使用者要做簡報 → 動態載入 pptx skill│ ← 用到才載入
└─────────────────────────────────────┘
因此T編自己針對monorepo的migration寫一個skills,內容大致包含:
---
name: extract-package
description: Use when creating a new shared package in the monorepo, extracting reusable components or utilities into packages/, or when code needs to be shared across multiple apps in apps/
---
## 何時抽 package
## 何時**不**抽 package
## 絕對禁止(Package 邊界規範)
### 1. 禁止在 package 內自我 import(循環 import)
...
### 4. 禁止在 `index.ts` 使用 glob 路徑匯出
## 命名規則
## 完整步驟
### 步驟 1:建立 package 目錄
...
### 步驟 9:在 app 中使用
## 常見錯誤
## 本 Monorepo 現有 Packages 參考
大家也可以根據自身專案需求來客製化自己的skills,當agent開發時就會根據你的skills來進行packages的抽取。
5. 結語
這次遷移讓T編最有感的不是 Turborepo 有多快,而是「強迫你思考邊界」這件事。當你開始問「這段邏輯該放在 app 還是 package?」的時候,其實就是在問「這個功能的本質是業務邏輯還是通用能力?」這個問題在單一 repo 時很容易被忽略,但在 Monorepo 的架構下你必須給出答案。某種程度上,Monorepo 不只是一種技術選擇,更是一種強制你釐清系統邊界的工程紀律。
備註:圖片來源
![]()