前端落地計畫:架構、資料流、狀態模型
從路由結構、資料夾配置、client/server 邊界,到狀態機、型別策略與開發節奏的完整計畫。
本文件為 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) 的 error 是 unknown,因為 JavaScript 什麼都能 throw。所以:
const message = error instanceof Error ? error.message : String(error);
9. 分層責任
| 層 | 可以 | 不可以 |
|---|---|---|
| UI 元件 | 收 props、顯示 | fetch、知道 domain、管複雜狀態 |
| Domain 元件 | 用 hook、組合 UI 元件、轉換資料 | 直接操作路由 |
| 頁面 / 容器 | 取資料、往下傳 | 寫樣式細節、放商業規則 |
10. 開發節奏
- 先做設計頁。 沒有設計頁不准做功能。
- 一次一個區塊。 做完看得到、驗收得了,再做下一個。
- 每個功能的完成條件包含「設計頁已更新」。
- 每次改動跑
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。