元件 (Component) ↓ 呼叫 Store (Pinia) ← 管理狀態、快取 ↓ 呼叫 API Service ← 封裝業務邏輯 ↓ 呼叫 HTTP Client ← Axios 實例、攔截器
目前的資料流(以登入為例)
views/Login.vue(Playground 頁面)只負責表單/驗證碼/導頁等 UI 行為stores/auth.ts統一負責登入狀態(user/token/loading/error)services/modules/user.ts封裝login/getProfile/...端點services/client.ts建立axiosinstanceservices/interceptors.ts統一注入 token 與處理 HTTP 錯誤
Menu API 與資料結構
選單系統採用 API 驅動設計:
API 端點
GET /service/api/menu:取得完整選單樹GET /service/api/menu/favorite:取得使用者收藏選單
資料結構
interface MenuNode {
mdl_id: string // 模組 ID
mdl_name: string // 模組名稱
unt_id?: string // 單位 ID
unt_name?: string // 單位名稱
fnc_id?: string // 功能 ID
fnc_name?: string // 功能名稱
children?: MenuNode[]
}
階層關係
- 第一層:模組(mdl)
- 第二層:單位(unt)
- 第三層:功能(fnc),作為葉節點使用
fnc_id作為路由路徑
Store 持久化
stores/menu.ts 提供:
- 自動 localStorage 持久化選單與收藏
- 初始化時自動還原資料
- 登出時清除快取
API 前綴:/api
目前 Playground 已將 api 資料夾更名為 services,避免與 API 前綴 /api 衝突。
在開發模式下:
- 前端呼叫一律使用
/service/api/* - Vite dev server 透過 proxy 將
/service/*轉送到後端(目前指向http://192.168.89.54:9002)
HTTP Client 設定
baseURL:優先使用VITE_API_BASE_URL,否則預設/service/api(搭配 Vite proxy)Content-Type:預設application/json
Token Service(單一來源)
為避免「Pinia token」與「localStorage token」不同步的問題,這裡採用單一來源:
services/token.ts使用ref保存 token,並同步 localStorage- Store 與 Interceptor 都只透過
tokenService讀寫 token - 401 時清除 token,可立即同步到 UI
Token 注入策略(Interceptor)
Interceptor 會從 tokenService 讀取 token 並注入 Authorization: Bearer <token>。
這樣做的原因是避免循環依賴:
store(auth) -> services(userApi) -> httpClient -> interceptors -> store(auth)
Store 仍然是「唯一負責更新 token 的地方」,Interceptor 只負責「讀取 token 並附加到 request」。
錯誤正規化(normalizeError)
為了讓 UI 不需要理解 AxiosError,這裡將錯誤統一成 ApiRequestError:
services/error.ts提供normalizeError()與ApiRequestError- Interceptor 在 response error 時呼叫
normalizeError() - Store 只需要處理
error.message / error.code / error.status
最低限度的映射規則:
- 有
response.data.message優先使用 - 其次使用
AxiosError.message - 都沒有則顯示
請求失敗
請求取消(AbortController)
取消策略採「同類型請求互斥」,目前示範在 login:
- Store 建立
AbortController,每次登入前先取消前一次 - Service 只接收
signal,不管理 controller 狀態 normalizeError會將取消行為轉為CanceledRequestError- UI 不顯示取消造成的錯誤訊息
| DECISION | WHY | WHY NOT |
|---|---|---|
| Store 呼叫 API,不是元件直接呼叫 | 狀態集中管理、可快取、易測試 | 元件直接呼叫會導致狀態散落 |
| API 模組化(userApi、orderApi) | 關注點分離、好維護 | 全塞一個檔案會變超大 |
| Interceptor 獨立檔案 | 單一職責、好測試 | 寫在 client.ts 會雜亂 |
| 泛型 ApiResponse | 型別安全、IDE 提示 | 用 any 會失去 TS 優勢 |
API 前綴使用 /api |
使用習慣一致、搭配 Vite proxy 容易理解 | 若前端資料夾也叫 api 容易造成路徑衝突,因此此專案改名為 services |