跳至主要内容

前後端整合

AppFuse Web 與 Server 透過 HTTP 契約整合。兩者沒有版本號互鎖,也不應各自維護另一套 認證、錯誤或檔案傳輸邏輯。

連線模式

模式Web baseURLAPI 路由
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 tokenPOST /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 接線; 需要重新認證時由 systemEventsauth/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/validatorcreateServerErrorHandler

檔案傳輸

參考實作採 binary-separate 流程:

POST /api/v1/staging/files/prepare
→ 取得暫存 upload URL
→ PUT binary
→ 原 request 的 File 替換為 FileDescriptor
→ 送出業務 JSON

這由共用 apiClientfileUpload 設定處理。業務表單可傳入 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.comhttp://localhost:*。框架會統一設定 methods、headers、credentials 與 exposed headers; 不要另寫一套 spring.web.cors 假設。

整合驗證清單

  • Web 實際只建立一個 apiClient
  • Vite/reverse proxy 保留 /api/v1/* path 與 Server /app-server context。
  • 登入、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 契約,不應把通用範例視為框架內建功能。

下一步