全栈开发

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 };
    }
}