跳到主要內容

前端落地計畫:架構、資料流、狀態模型

從路由結構、資料夾配置、client/server 邊界,到狀態機、型別策略與開發節奏的完整計畫。

約 6 分鐘 · frontend-plan.md

本文件為 frontend-plan.md 規格,以 SvelteKit 為主要示例,並標出 Next.js / Vue 的對應做法。

1. 架構摘要

伺服器負責取得與塑形資料,瀏覽器負責互動。 兩邊之間只傳可序列化的純資料。

在 SvelteKit 是 +page.server.ts(只在伺服器)與 +page.ts(兩邊都跑);在 Next.js 是 Server Component 與 "use client"。名字不同,界線一樣。

界線上只允許純資料通過。函式、class 實例、元件、Date 以外的物件都不行 —— 這條限制其實是好事,它逼你在伺服器就把資料整理好。

2. 路由結構

/                          landing
/styles                    風格庫
/styles/[slug]             單一風格的完整示範頁
/styles/[slug]/design      設計系統頁(強制存在)
/modules                   模組庫(篩選狀態在 URL)
/modules/[category]        分類
/modules/[category]/[id]   單一模組 + 交付
/embed/[category]/[id]     無外框的預覽,給 iframe 用
/guides                    規範索引
/guides/[slug]             單一規範
/learn /contact
/legal/terms /legal/privacy
/api/export                產生 zip
/themes.css                預先產生的所有風格

/embed/* 存在的理由:RWD 預覽只能用 iframe。 media query 看的是 viewport,把容器縮窄不會觸發它。用 transform: scale() 做的「手機預覽」是假的。

3. 資料夾結構

src/
  routes/            只做路由與組合,不放邏輯
  lib/
    styles/          contract.css / components.css / site.css
    registry/        themes.ts / modules.ts   ← 資料,不是程式
    modules/         可貼上的區塊,依分類分資料夾
    components/      本站自己的外框元件
    theme/           設定的 runtime
    i18n/            字典與 Intl 包裝
    markdown/        解析與淨化
    export/          prompt 產生器、原始碼處理
    log/             結構化日誌
    content/docs.ts  文件註冊表
  content/docs/      markdown 檔

判準:routes/ 裡出現超過 20 行的商業邏輯,就該搬到 lib/

4. Client / Server 邊界

放伺服器 放瀏覽器
資料抓取、驗證、授權 互動、動畫、拖拉
任何用到金鑰的事 表單即時驗證(伺服器仍要再驗一次)
markdown 解析(首次) 串流輸出的逐字渲染
產生 zip 剪貼簿

cookie 讀在伺服器,不是在 effect 裡。 主題、語言、登入狀態都要在伺服器就知道,否則第一幀一定是錯的,你會看到那個經典的白閃。

本站的做法:hooks.server.ts 解析 cookie → 寫進 <html data-style="..."> → 第一幀就是對的。

5. 資料層

元件 → hook / load → service → API
  • service 是唯一碰網路的地方,回傳已經打好型別的結果
  • load 負責組合 service,處理錯誤邊界
  • 元件只收整理好的資料

錯誤在邊界處理,不要讓 try/catch 散落在元件裡。

6. 狀態模型

用 union,不要用一堆布林。

type LoadState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; message: string; traceId?: string };

isLoading && isError 這種不該存在的組合,在 union 裡根本無法表達。

狀態該放哪裡

狀態 位置
篩選、分頁、排序、目前分頁籤 URL query
使用者偏好(主題、語言、密度) cookie(伺服器要讀)
伺服器資料 load / query 快取
純 UI 開關(選單開了沒) 元件 local state
跨頁共用的 domain 狀態 context / store,per-request 建立

伺服器上絕對不能有 module-level 可變狀態

// 危險:整個 worker 共用,A 使用者的設定會出現在 B 使用者身上
export const settings = new Settings();

正確做法是每次 render 建立、用 context 往下傳。這在 SvelteKit、Next.js App Router、Nuxt 都一樣。

7. 渲染模型

  • render 必須純:不 fetch、不改 DOM、不寫 localStorage
  • 副作用只放在 effect、event handler、server action
  • 先不要 memo。等你測到慢再說;提前最佳化只會讓依賴陣列變成 bug 溫床

8. TypeScript 策略

  • strict: true,沒得商量
  • 禁止 any;不得已用 unknown 再收窄
  • 禁止用 as 消滅錯誤(as const 例外)
  • API 回應必須有型別,而且要在邊界驗證,不是只標注 —— 標注只是騙自己
  • domain type 放 types/,不要在三個檔案各定義一次同一個 User

catch (error)errorunknown,因為 JavaScript 什麼都能 throw。所以:

const message = error instanceof Error ? error.message : String(error);

9. 分層責任

可以 不可以
UI 元件 收 props、顯示 fetch、知道 domain、管複雜狀態
Domain 元件 用 hook、組合 UI 元件、轉換資料 直接操作路由
頁面 / 容器 取資料、往下傳 寫樣式細節、放商業規則

詳見 architecture-layers

10. 開發節奏

  1. 先做設計頁。 沒有設計頁不准做功能。
  2. 一次一個區塊。 做完看得到、驗收得了,再做下一個。
  3. 每個功能的完成條件包含「設計頁已更新」。
  4. 每次改動跑 npm run check 型別錯誤不進 main。

11. 效能預算

項目 上限
首屏 JS(gzip) 120KB
首屏圖片 250KB
LCP(4G) 2.5s
CLS 0.1
字體家族數 2

超過就要有人簽字,不能默默過去。

12. 部署

Cloudflare Workers + @sveltejs/adapter-cloudflare。靜態資產走 assets binding,動態路由走 worker,/themes.css 這種純算出來的東西預先產生成靜態檔。詳見 deployment-cloudflare

顯示設定

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

風格

密度

圓角

動態

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

語言