Next.js App Router服务端/客户端统一请求最佳实践(含Cookie、CSRF、Header)
问题背景
在Next.js 15 App Router项目中,需要构建支持服务端(RSC/预取)和客户端的统一请求工具,正确处理Cookie、CSRF令牌及请求头。现有实现存在以下问题:
- 服务端请求函数每次调用
cookies()和headers(),导致串行阻塞,无法并行发起请求 - 服务端请求与请求状态紧耦合,序列化困难
- 代理方案需手动转发Cookie,代码冗余且破坏抽象性
核心问题解答
1. 优雅实现服务端/客户端统一请求的方案
通过环境自动识别+服务端上下文传递的方式,避免重复获取请求上下文,同时自动处理Cookie和CSRF:
步骤1:定义类型与统一接口
// types.ts import { cookies, headers } from 'next/headers'; export type ServerContext = { cookies: ReturnType<typeof cookies>; headers: ReturnType<typeof headers>; }; export interface FetcherOptions { url: string; method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; body?: Record<string, any>; serverContext?: ServerContext; // 仅服务端调用时传入 }
步骤2:实现统一Fetcher
自动判断运行环境,服务端使用传入的上下文,客户端直接调用浏览器Fetch:
// fetcher.ts import { clientFetch } from "./clientFetch"; import { serverFetch } from "./serverFetch"; import type { FetcherOptions } from "./types"; export const fetcher = async ({ url, method, body, serverContext }: FetcherOptions) => { if (typeof window === 'undefined') { if (!serverContext) { throw new Error('Server context is required for server-side requests'); } return serverFetch(url, method, body, serverContext); } else { return clientFetch(url, method, body); } };
步骤3:优化服务端请求函数
接收传入的上下文,避免重复调用cookies()和headers():
// serverFetch.ts 'use server'; import type { ServerContext } from "./types"; export const serverFetch = async ( endpoint: string, method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE', body?: Record<string, any>, { cookies: nextCookies, headers: nextHeaders }: ServerContext ) => { const csrfToken = nextCookies.get('XSRF-TOKEN')?.value; if (!csrfToken && method !== 'GET') { throw new Error('CSRF token not found in server cookies'); } const referer = nextHeaders.get('referer') ?? process.env.NEXT_PUBLIC_BASE_URL_FE; const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}${endpoint}`, { method, headers: { 'Content-Type': 'application/json', Accept: 'application/json', Cookie: nextCookies.toString(), Referer: referer, ...(method !== 'GET' && { 'X-XSRF-TOKEN': csrfToken }), }, credentials: 'include', body: body ? JSON.stringify(body) : undefined, }); const data = await res.json(); if (!res.ok) { console.error('Server request failed:', data); throw new Error(data.message || 'Server request failed'); } return data; };
步骤4:实现客户端请求函数
利用浏览器自动携带Cookie,从Cookie中提取CSRF令牌:
// clientFetch.ts export const clientFetch = async ( endpoint: string, method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE', body?: Record<string, any> ) => { let csrfToken: string | undefined; if (method !== 'GET') { csrfToken = document.cookie.split('; ') .find(row => row.startsWith('XSRF-TOKEN=')) ?.split('=')[1]; if (!csrfToken) { throw new Error('CSRF token not found in client cookies'); } } const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}${endpoint}`, { method, headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(csrfToken && { 'X-XSRF-TOKEN': csrfToken }), }, credentials: 'include', body: body ? JSON.stringify(body) : undefined, }); const data = await res.json(); if (!res.ok) { console.error('Client request failed:', data); throw new Error(data.message || 'Client request failed'); } return data; };
服务端组件使用示例
一次获取上下文,并行发起多个请求,避免串行阻塞:
// app/page.tsx (服务端组件) import { cookies, headers } from 'next/headers'; import { fetcher } from './fetcher'; export default async function Home() { const serverContext = { cookies: await cookies(), headers: await headers() }; // 并行请求,无串行阻塞 const [userData, postsData] = await Promise.all([ fetcher({ url: '/api/user', method: 'GET', serverContext }), fetcher({ url: '/api/posts', method: 'GET', serverContext }), ]); return ( <div> <h1>Welcome, {userData.name}</h1> <ul> {postsData.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); }
2. 服务端组件是否应始终通过/api/proxy路由请求?
不需要。直接调用后端API更高效,避免额外的网络跳转。代理方案仅适合需要统一处理请求(如全局日志、权限拦截)的场景,但会增加延迟。
若必须使用代理,服务端组件调用时需手动传递Cookie(服务端Fetch不会自动携带浏览器Cookie),示例:
// 服务端组件中调用代理 const nextCookies = await cookies(); const res = await fetch('/api/proxy', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Cookie': nextCookies.toString(), }, body: JSON.stringify({ endpoint: '/api/user', method: 'GET' }), });
但这种方式冗余且破坏抽象,不推荐服务端组件使用代理。
3. App Router架构下更优的混合请求方案
- 基于环境自动切换的统一Fetcher:如上述方案,服务端传递上下文,客户端自动处理Cookie和CSRF,兼顾性能与抽象性。
- 结合TanStack Query:在服务端组件中预取数据时传入上下文,客户端组件直接使用统一Fetcher,无需额外配置:
// app/providers.tsx (客户端组件) 'use client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { fetcher } from './fetcher'; const queryClient = new QueryClient({ defaultOptions: { queries: { fetcher: ({ queryKey }) => { const [url, method] = queryKey as [string, string]; return fetcher({ url, method: method as any }); }, }, }, }); export function Providers({ children }: { children: React.ReactNode }) { return ( <QueryClientProvider client={queryClient}> {children} </QueryClientProvider> ); } - Laravel Sanctum适配:确保服务端请求的
Referer头指向Next.js域名,客户端首次请求后端时获取XSRF-TOKENCookie(Laravel Sanctum会自动设置)。
内容的提问来源于stack exchange,提问作者Marko Ticinovic
相关产品推荐
相关产品推荐

