跳到主要內容

層級架構:誰可以知道什麼

UI / domain / container 三層的責任與禁止事項、資料流方向、什麼時候該拆檔案,以及怎麼判斷一個檔案已經腐化。

約 5 分鐘 · architecture-layers.md

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/ 領域型別放這裡,不要在三個檔案各定義一次 User
  • utils/ 只放純函式(同樣輸入必得同樣輸出,沒有副作用)

5. 什麼時候拆檔案

不是看行數,是看責任數。問三個問題:

  1. 這個檔案能不能用一句話描述?需要「和」「還有」就是超過一個責任了
  2. 改動 A 功能會不會動到 B 功能的程式碼?會,就該拆
  3. 兩個人同時改這個檔案會不會衝突?經常會,就該拆

實務上的訊號:

  • 一個元件超過 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 的檔案裡
  • 領域型別只定義一次
  • 每個檔案能用一句話描述
  • 沒有循環依賴

顯示設定

這裡改的每一項,會即時套用到站上所有預覽。

風格

密度

圓角

動態

系統層級的「減少動態效果」永遠優先於這裡的設定。

語言