前後端整合
AppFuse Web 與 Server 透過 HTTP 契約整合。兩者沒有版本號互鎖,也不應各自維護另一套 認證、錯誤或檔案傳輸邏輯。
連線模式
| 模式 | Web baseURL | API 路由 |
|---|---|---|
| Vite 開發 | / | /api/* 經 Vite proxy 到 http://localhost:8080/app-server |
| 分離部署 | / | Web reverse proxy 將同源 /api/* 轉到 Server |
app-office-host | /app-server(由 /config 提供) | 瀏覽器直接呼叫部署服務 |
| Mock | / | MSW 攔截與真實 Server 相同的 /api/v1/* |
業務 service 一律使用相對 /api/v1/*。不要在每個 service 拼接 host、port 或 context
path。
Vite 開發代理
// app-office/vite.config.ts
export default defineConfig({
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080/app-server',
changeOrigin: true,
},
},
},
});
app-office 的 .env 預設 VITE_MSW=false,因此 request 會通過 proxy;需要 Mock 時以
VITE_MSW=true npm run dev 啟動。
單一 HTTP client
消費端只在 src/services/api-client.ts 建立一次 client,所有 service 共用該實例:
import { createHttpClient } from '@appfuse/appfuse-web/utils';
export const apiClient = createHttpClient({
baseURL: '/',
withCredentials: true,
systemEvents,
fileUpload: {
strategy: 'binary',
endpoint: '/api/v1/staging/files/prepare',
},
auth: {
getAccessToken: () => selectAuthorization(store.getState())?.access_token ?? null,
refresh: refreshAccessToken,
},
});
應用啟動取得 /config 後,再把部署感知的 environ.baseURL 套到同一個 client:
apiClient.defaults.baseURL = environ.baseURL;
不要在其他檔案再次呼叫 createHttpClient,否則會漏掉部署 base URL、認證攔截器、
single-flight refresh 與檔案傳輸設定。
認證契約
目前參考實作使用:
| 動作 | HTTP 契約 |
|---|---|
| 登入 | POST /api/v1/auth/login;Server 以 Set-Cookie 建立應用專屬 HttpOnly Refresh Cookie |
| 目前使用者 | GET /api/v1/auth/me |
| 更新 access token | POST /api/v1/auth/refresh;Browser 自動帶 Cookie,成功後 Server 嚴格輪替 Cookie credential |
| API 授權 | Authorization: Bearer {accessToken} |
access token 只留在 Redux memory;refresh credential 不暴露給 JavaScript,也不得寫入 Web Storage。
token 的附加、主動刷新、跨分頁輪替協調與 401 retry 應由共用 apiClient 的 auth callbacks 接線;
需要重新認證時由 systemEvents 的 auth/reauthentication-required 事件通知 App Shell。業務 service
不應直接寫 localStorage,也不應各自註冊 response interceptor。
列表與分頁
AppFuse 不強制所有 endpoint 使用同一種 JSON envelope。參考實作目前同時存在:
- Spring Data
Page<T>JSON。 - JSON array 搭配
X-Total-Count等 response headers。
消費端必須依該 endpoint 的 API Specification 實作,不要只因為其他 endpoint 使用
content 就假設所有列表都有相同 shape。若使用 header 分頁:
const response = await apiClient.get<Product[]>('/api/v1/products', { params });
return {
content: response.data,
totalElements: Number(response.headers['x-total-count'] ?? '0'),
};
CORS source 已暴露 X-Total-Count 等必要 headers;反向代理也不得移除它們。
錯誤契約
Server 的 StandardRestExceptionHandler 與 exception mappers 產生標準化 problem detail;
Web createHttpClient 會將失敗轉為公開的 ErrorResponse。UI 使用
getApiErrorMessage() 或表單 validator 的 violation mapper,不要再解析 Axios 私有 shape:
import { getApiErrorMessage } from '@appfuse/appfuse-web/utils';
import { prompt } from '@appfuse/appfuse-web/messaging';
try {
await apiClient.post('/api/v1/products', request);
} catch (error) {
prompt.error(getApiErrorMessage(error));
}
需要把 constraint violations 放回欄位時,使用
@appfuse/appfuse-web/form/validator 的 createServerErrorHandler。
檔案傳輸
參考實作採 binary-separate 流程:
POST /api/v1/staging/files/prepare
→ 取得暫存 upload URL
→ PUT binary
→ 原 request 的 File 替換為 FileDescriptor
→ 送出業務 JSON
這由共用 apiClient 的 fileUpload 設定處理。業務表單可傳入 File,不要自行把所有請求
改成 multipart;Server endpoint 若明確定義 multipart 才例外。
CORS
一般開發與部署都應優先採同源 proxy。只有瀏覽器直接跨 origin 呼叫 Server 時才配置:
app:
cors:
allowed-origins: "https://app.example.com,https://admin.example.com"
allowed-origins 是逗號分隔字串,支援精確 origin、*.example.com 與
http://localhost:*。框架會統一設定 methods、headers、credentials 與 exposed headers;
不要另寫一套 spring.web.cors 假設。
整合驗證清單
- Web 實際只建立一個
apiClient。 - Vite/reverse proxy 保留
/api/v1/*path 與 Server/app-servercontext。 - 登入、refresh、session expired 行為通過。
- 列表資料 shape 與分頁 headers 符合 API Specification。
- validation、not-found、conflict 等錯誤可被
ErrorResponse正確呈現。 - binary upload、永久檔案 URL 與下載 headers 通過。
- Mock 與真實 Server 對相同 request 回傳相同契約。
WebSocket/STOMP 不是 AppFuse Server 目前預設提供的整合能力;若應用自行加入,須另外定義 依賴、認證、proxy 與 reconnect 契約,不應把通用範例視為框架內建功能。