文章
nextjs网络请求封装
1、server 端
import { BACKEND_BASE_URL } from "@/config/default";
import { getAuthTokens, getEmailFromToken } from "@/lib/token";
export async function fetchWithAuth(
path:string,
init?: RequestInit
) {
// 获取 token
const {accessToken} = await getAuthTokens();
const userId = await getEmailFromToken(accessToken);
// 2. 构建完整 API URL
const url = `${BACKEND_BASE_URL}${path}`;
console.log("[认证代理]: " + path);
// 3. 发起请求
const response = await fetch(url, {
...init,
headers: {
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
...(userId && { 'X-USER-ID': userId.toString() }),
'Content-Type': 'application/json',
...init?.headers
}
});
return response;
}2、client 端
/**
* API 服务配置
* 核心思想:默认单服务,预留扩展接口,空配置自动回退到主服务
*/
import { BACKEND_BASE_URL } from "@/config/default";
// 服务配置类型定义
interface ServiceConfig {
url: string; // 服务地址,空字符串表示回退到主服务
prefix: string; // API 路径前缀
}
// 服务配置定义
export const API_SERVICES: Record<string, ServiceConfig> = {
// 主服务 - 当前正在使用的核心服务
main: {
url: BACKEND_BASE_URL || 'http://localhost:5000', // 主服务地址
prefix: '/api' // API 路径前缀
},
// AI 服务 - 预留扩展,用于未来接入 AI 功能
// 如果 FASTAPI_AI_URL 未设置,空字符串会自动回退到主服务
ai: {
url: process.env.API_AI_URL || '', // 空 = 回退到主服务
prefix: '/api/ai'
},
// 统计分析服务 - 预留扩展,用于未来接入数据分析
analytics: {
url: process.env.API_ANALYTICS_URL || '', // 空 = 回退到主服务
prefix: '/api/analytics'
}
};
/**
* 根据请求路径识别目标服务
* 实现智能路由:特定前缀走特定服务,其他走主服务
*
* @param path 请求路径,如 '/users/profile' 或 '/ai/chat'
* @returns 目标服务名称和清理后的路径
*
* @example
* getTargetService('/users/profile') // { service: 'main', cleanPath: '/users/profile' }
* getTargetService('/ai/recommend') // { service: 'ai', cleanPath: '/recommend' }
*/
export function getTargetService(path: string): { service: string; cleanPath: string } {
// AI 相关请求路由到 AI 服务
if (path.startsWith('/ai/')) {
return {
service: 'ai',
cleanPath: path.replace('/ai/', '') // 移除 'ai/' 前缀
};
}
// 统计分析请求路由到分析服务
if (path.startsWith('/analytics/')) {
return {
service: 'analytics',
cleanPath: path.replace('/analytics/', '') // 移除 'analytics/' 前缀
};
}
// 默认路由到主服务(包括未识别的路径)
return {
service: 'main',
cleanPath: path // 保持原路径
};
}
/**
* 获取服务的实际 URL
* 核心特性:如果服务 URL 为空,自动回退到主服务
*
* @param service 服务名称
* @returns 服务的基础 URL
*
* @example
* getServiceUrl('ai') // 如果 API_AI_URL 未设置,返回主服务地址
*/
export function getServiceUrl(service: string): string {
const config = API_SERVICES[service];
if (!config) {
// 服务不存在,回退到主服务
return API_SERVICES.main.url;
}
// 如果服务 URL 为空,回退到主服务
return config.url || API_SERVICES.main.url;
}src/app/[...proxy]/route.ts
import { NextRequest } from 'next/server';
import { ACCESS_TOKEN_NAME } from '@/config/default';
import { API_SERVICES, getTargetService } from '@/lib/ api-config';
import { getEmailFromToken } from '@/lib/token';
/**
* 统一代理路由
* 功能:接收所有 /api/proxy/* 请求,转发到对应的 FastAPI 服务
* 优势:隐藏后端地址,统一处理认证,支持多服务扩展
*/
// 支持的 HTTP 方法 - 根据需要添加
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ proxy: string[] }> }
) {
const resolvedParams = await params;
return handleProxyRequest(request, resolvedParams);
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ proxy: string[] }> }
) {
const resolvedParams = await params;
return handleProxyRequest(request, resolvedParams);
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ proxy: string[] }> }
) {
const resolvedParams = await params;
return handleProxyRequest(request, resolvedParams);
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ proxy: string[] }> }
) {
const resolvedParams = await params;
return handleProxyRequest(request, resolvedParams);
}
/**
* 代理请求处理核心函数
* 处理流程:
* 1. 解析请求路径 → 2. 识别目标服务 → 3. 构建目标URL → 4. 转发请求 → 5. 返回响应
*/
async function handleProxyRequest(
request: NextRequest,
params: { proxy: string[] }
) {
// 1. 解析请求路径和查询参数
const fullPath = '/' + params.proxy.join("/");
const searchParams = request.nextUrl.searchParams.toString();
console.log(`[API代理] ${request.method} ${fullPath}`);
// 2. 智能路由:识别目标服务和清理路径
const { service, cleanPath } = getTargetService(fullPath);
// 3. 获取目标服务地址(支持自动回退)
const rawConfig = API_SERVICES[service];
const fallbackToMain = !rawConfig || !rawConfig.url;
const effectiveConfig = fallbackToMain ? API_SERVICES.main : rawConfig;
const baseUrl = effectiveConfig.url;
const prefix = effectiveConfig.prefix;
// 4. 构建目标 URL
const url = `${baseUrl}${prefix}${cleanPath}${searchParams ? `?${searchParams}` : ''}`;
// 5. 从 cookie 获取认证 token
const token = request.cookies.get(ACCESS_TOKEN_NAME)?.value;
const userId = await getEmailFromToken(token);
// 6. 转发请求到目标服务
const response = await fetch(url, {
method: request.method,
headers: {
// 自动添加认证头(如果有 token)
...(token && { Authorization: `Bearer ${token}` }),
...(userId && { 'X-USER-ID': userId.toString() }),
'Content-Type': 'application/json',
},
// GET 和 DELETE 请求没有 body
body: request.method === 'GET' || request.method === 'DELETE'
? undefined
: await request.text(), // 直接传递请求体
});
return response;
}3、统一管理
import { fetchWithAuth } from '@/lib/server-fetch';
import { ApiResponseType } from '@/types/req-types';
// 明确上下文类型
type RequestContext = 'server' | 'client';
// 假设你的后端统一响应格式为:
// { code: number; message?: string; data?: T }
interface ApiResponse<T = unknown> {
code: number;
message?: string;
data?: T;
}
// 显式定义 HTTP 协议层“成功”状态码(可按需调整)
const HTTP_SUCCESS_STATUSES = [200, 201, 204];
// 业务成功 code(根据后端约定调整,常见有 200、0、2000 等)
const BUSINESS_SUCCESS_CODES = [200];
/**
* 统一请求函数,支持 server/client 上下文 + 业务级 code 错误处理
*/
export async function requestAction(
context: RequestContext,
path: string,
init?: RequestInit
): Promise<ApiResponseType> {
let response: Response;
try {
if (context === 'server') {
response = await fetchWithAuth(path, init);
} else {
// client: 调用 /api/proxy/... 路由(由调用方确保 path 正确)
response = await fetch(path, {
...init,
headers: {
'Content-Type': 'application/json',
...init?.headers,
},
});
}
// 1. 先检查 HTTP 状态码(网络/代理层错误)
if (!HTTP_SUCCESS_STATUSES.includes(response.status)) {
// 尝试读取错误信息
let message = `请求失败(HTTP ${response.status})`;
try {
const errorData = await response.json();
message = errorData.message || errorData.error || message;
} catch (e) {
// 忽略 JSON 解析失败
}
return { success: false, message: message };
}
// 2. HTTP 2xx,但需检查业务 code(假设返回 JSON)
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
// 克隆 response 以便后续再次 .json()
const clone = response.clone();
const body: ApiResponse = await clone.json();
if (!BUSINESS_SUCCESS_CODES.includes(body.code)) {
const errorMessage = body.message || `业务错误(code: ${body.code})`;
return { success: false, message: errorMessage || '', data: body.data, code: body.code };
}
}
// 3. 一切正常,返回原始 Response(调用方可 .json())
const res: ApiResponse = await response.json()
return { success: true, message: res.message || '', data: res.data, code: res.code };
} catch (error) {
const message = error instanceof Error
? error.message
: '未知请求错误';
return { success: false, message };
}
}