層級架構:誰可以知道什麼
UI / domain / container 三層的責任與禁止事項、資料流方向、什麼時候該拆檔案,以及怎麼判斷一個檔案已經腐化。
1. 三層
容器 / 頁面 取資料、組合。不寫樣式細節,不放商業規則。
↓ props
Domain 元件 把領域資料翻譯成 UI props。可以用 hook。
↓ props
UI 元件 純顯示。只收 props。
資料往下流,事件往上流。 沒有例外,沒有捷徑。
2. 每一層可以做什麼
UI 元件(components/ui、本專案的 lib/modules)
可以:接收 props、顯示、發出事件、內部的純視覺狀態(開合、hover)
不可以:fetch、讀 store、知道路由、知道領域概念
判斷法:這個元件能不能原封不動搬到另一個產品? 不能,它就不是 UI 元件。
✓ <PricingCard tier="pro" price={480} featured />
✗ <WhatSubProPlanCard /> ← 知道太多了
Domain 元件(components/domain)
可以:用 hook、組合 UI 元件、把 API 型別轉成 UI props、處理領域規則
不可以:直接操作路由、直接碰資料庫
Domain 元件是「翻譯層」。API 回傳 { plan_code: 'PRO', monthly_cents: 48000 },UI 元件要的是 tier="pro" price={480} —— 中間的轉換就發生在這裡。
容器 / 頁面(routes/)
可以:取資料、處理載入與錯誤、往下傳
不可以:寫樣式細節、放商業規則
判斷法:頁面檔案超過 20 行商業邏輯,就該搬走。
3. 為什麼要分
不分層的專案,三個月後會變成這樣:一個「顯示訂單」的元件裡面同時有 fetch、有折扣計算、有樣式、有路由跳轉。於是:
- 想在別的地方重用 → 不行,它會自己去 fetch
- 想改折扣規則 → 要在四個元件裡各改一次
- 想寫測試 → 要先起一個伺服器
- 想換 API → 要改遍全部元件
分層的成本是多寫幾個檔案,收益是上面四件事都變成小事。
4. 資料夾
src/
routes/ 路由與組合
lib/
modules/ 可貼上的完整區塊(純顯示)
components/ 站台自己的外框
hooks/ 可重用的邏輯
services/ 唯一碰網路的地方
types/ 共用型別
utils/ 純函式
styles/ 設計系統
規則:
services/是唯一 fetch 的地方,回傳打好型別的資料hooks/一個 hook 一個責任types/領域型別放這裡,不要在三個檔案各定義一次Userutils/只放純函式(同樣輸入必得同樣輸出,沒有副作用)
5. 什麼時候拆檔案
不是看行數,是看責任數。問三個問題:
- 這個檔案能不能用一句話描述?需要「和」「還有」就是超過一個責任了
- 改動 A 功能會不會動到 B 功能的程式碼?會,就該拆
- 兩個人同時改這個檔案會不會衝突?經常會,就該拆
實務上的訊號:
- 一個元件超過 250 行 → 通常裡面藏了第二個元件
- 一個檔案有超過 5 個 import 來自不同領域 → 它知道太多了
- 一個函式超過 50 行 → 裡面通常有可命名的步驟
6. 依賴方向
routes → modules / components → hooks → services → types
箭頭不可以反向。 services 不可以 import 元件,types 不可以 import 任何東西。
出現循環依賴時,通常代表有一個概念沒被抽出來。把共用的部分抽成第三個模組,而不是互相 import。
檢查工具:
npx madge --circular src/
7. 狀態該放哪裡
| 狀態 | 位置 |
|---|---|
| 篩選、分頁、排序 | URL |
| 使用者偏好 | cookie(伺服器要讀) |
| 伺服器資料 | load / query 快取 |
| 表單輸入中的值 | 最近的共同父層 |
| 選單開了沒 | 元件自己 |
| 跨頁的領域狀態 | context / store,per-request 建立 |
狀態要盡量往下放。 每往上提一層,重新渲染的範圍就大一圈。但如果兩個兄弟元件都需要,就提到它們的父層 —— 不要用兩份互相同步的複本。
8. 伺服器上不能有 module-level 可變狀態
// 危險:SSR 時整個 worker 共用,A 使用者的資料會出現在 B 使用者身上
export const currentUser = writable(null);
export const settings = new Settings();
這是 SSR 最惡劣的 bug 種類:本機開發(一次一個使用者)完全正常,上線後在流量高的時候偶發資料串接。
正確做法:每次 request 建立,用 context 往下傳。
// +layout.svelte
setSettingsContext(data.settings); // 每次 render 一份新的
9. 腐化的訊號
- 元件裡出現
fetch - 同一個 API 型別在三個檔案各定義一次
utils.ts裡有 API 呼叫- import 路徑出現
../../../ - 一個檔案 1000 行以上
- 改一個顏色要動五個檔案
- 沒有人敢刪任何東西
出現三個以上,就該安排一次重整,不要繼續往上疊。
10. 檢查清單
- UI 元件不 fetch、不知道領域
- 只有
services/碰網路 - 資料往下、事件往上,沒有循環
- 沒有 module-level 可變狀態出現在會 SSR 的檔案裡
- 領域型別只定義一次
- 每個檔案能用一句話描述
- 沒有循環依賴