HTTP 同 API 層
HTTP 棧拆分為三個套件,每個套件職責單一:
| 套件 | 職責 |
|---|---|
@vh5/request | 類型化 fetch 封裝、攔截器、Token 刷新、錯誤標準化 |
@vh5/api | 接口定義同請求/響應 DTO(無副作用) |
@vh5/services | 領域服務,消費 @vh5/api 並返回領域模型 |
應用同特性模組唔直接調用 fetch。
1. 請求客戶端(@vh5/request)
ts
export const request = createRequest({
baseURL: import.meta.env.VITE_API_BASE_URL ?? "/api",
timeout: 15_000,
});
// 注入 Access Token
request.interceptors.request.use((config) => {
const token = useAuthStore().accessToken;
if (token) config.headers.set("Authorization", `Bearer ${token}`);
return config;
});
// 401 時單請求刷新
request.interceptors.response.use(undefined, async (error) => {
if (error.httpStatus === 401 && !error.config._retried) {
await useAuthStore().refresh();
error.config._retried = true;
return request(error.config);
}
throw error;
});RequestError 數據結構:
ts
class RequestError extends Error {
code: number; // 後端業務碼
httpStatus: number; // HTTP 狀態碼
payload?: unknown; // 原始響應體
config: RequestConfig; // 原始請求配置
}2. API SDK(@vh5/api)
接口定義為純聲明——唔引入 Vue、Pinia 或 Toast。
ts
// packages/api/src/product.ts
export interface ProductDTO {
id: number;
title: string;
price: string;
imgUrl: string;
description?: string;
}
export const productApi = {
list: (params: { page: number; size: number }) =>
request.get<{ items: ProductDTO[]; total: number }>("/product/list", { params }),
detail: (id: number) => request.get<ProductDTO>("/product/detail", { params: { id } }),
};3. 領域服務(@vh5/services)
服務層將 DTO 轉換為領域模型,集中業務規則。
ts
// packages/services/src/product.service.ts
export interface Product {
id: number;
title: string;
price: number; // 領域模型用 number 而非 string
imgUrl: string;
description: string;
}
export const ProductService = {
async getList(page = 1, size = 20) {
const { items, total } = await productApi.list({ page, size });
return { items: items.map(toProduct), total };
},
getDetail: (id: number) => productApi.detail(id).then(toProduct),
};4. 喺視圖中消費服務
通過特性 Composable 而非直接喺模板中調用服務。Composable 負責管理 loading、error、取消同刷新。
ts
// packages/features/product/composables/use-product-detail.ts
import { ProductService } from "@vh5/services";
import { tryOnScopeDispose } from "@vueuse/core";
export function useProductDetail(id: MaybeRef<number>) {
const data = ref<Product | null>(null);
const error = ref<Error | null>(null);
const loading = ref(false);
const ac = new AbortController();
tryOnScopeDispose(() => ac.abort());
watch(
() => unref(id),
async (value) => {
if (!value) return;
loading.value = true;
try {
data.value = await ProductService.getDetail(value);
error.value = null;
} catch (err) {
error.value = err as Error;
} finally {
loading.value = false;
}
},
{ immediate: true },
);
return { data, error, loading };
}5. 新增接口
- 喺
packages/api/src/<domain>.ts中添加 DTO 同接口定義。 - 如需領域轉換,喺
packages/services/src/<domain>.service.ts中添加。 - 喺特性套件嘅 Composable 中封裝(
packages/features/<domain>/composables/)。 - 喺視圖中使用該 Composable。