Next.js中构建带动态请求头的同构API客户端最佳实践
我有一个对接独立API的Next.js应用,所有API端点都要求携带特定的X-HOST请求头。我需要在服务端预取请求,有时在客户端使请求失效,当前使用axios和@tanstack/react-query。服务端需从getServerSideProps的context.req中获取host,客户端则从window.location.host获取。
问题在于,为避免用户间数据泄漏,我需要在getServerSideProps内创建新的axios实例,并通过请求拦截器设置X-HOST头;同时希望通过react-query在服务端和客户端复用如getUsers这类API函数。
我的简化实现如下:
const createFetchClient = () => axios.create(); const createQueryClient = (fetchClient: AxiosInstance) => new QueryClient({ queries: { meta: { fetchClient }, }, }) const getUsers = async (params) => { const { fetchClient } = params; const { data } = fetchClient.get('/users'); return data; } // queryOptions from react-query const usersQuery = queryOptions({ queryKey: ['users'], queryFn: ({ meta }) => getUsers({ fetchClient: meta!.fetchClient }), })
服务端代码:
export const getServerSideProps = async (context) => { const fetchClient = createFetchClient(); fetchClient.interceptors.request.use((config) => { config.headers['x-host'] = context.req.headers.host; return config; }); const queryClient = createQueryClient(fetchClient); await queryClient.prefetchQuery(usersQuery); // The meta is included to the dehydrated state. // We remove the fetchClient by using this workaround return { props: { dehydratedState: JSON.parse(JSON.stringify(dehydrate(queryClient))) } } }
客户端代码:
const Providers = ({ children }) => { const [queryClient] = useState(() => createQueryClient(createFetchClient())); useEffect(() => { const fetchClient = queryClient.getDefaultOptions().queries?.meta?.fetchClient; if (!fetchClient) return; const interceptor = fetchClient.interceptors.request.use((config) => { config.headers['x-host'] = window.location.host; return config; }); return () => { fetchClient.interceptors.request.eject(interceptor); } }, [queryClient]); return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> } const MyComponent = () => { const users = useQuery(usersQuery); ... }
我希望能不再通过meta传递fetchClient,同时也不想在每个API函数中传递host,请问这类应用的最佳实践是什么?
方案1:封装独立API层,显式注入Axios实例
核心是把API请求逻辑和Axios实例解耦,避免通过React Query的meta传递实例,同时保证服务端和客户端实例完全隔离。
1. 封装API请求函数
把每个API请求写成依赖Axios实例的函数,不用关心host的来源:
// api/users.ts export const getUsers = async (axiosInstance: AxiosInstance) => { const { data } = await axiosInstance.get('/users'); return data; }; // 其他API同理,比如 getPosts、getUserDetail 等
2. 服务端预取逻辑
在getServerSideProps里创建专属Axios实例,设置host头后直接传给API函数:
export const getServerSideProps = async (context) => { // 为当前用户创建独立Axios实例,避免跨用户数据泄漏 const axiosInstance = axios.create(); axiosInstance.interceptors.request.use((config) => { config.headers['X-HOST'] = context.req.headers.host; return config; }); const queryClient = new QueryClient(); // 预取时直接传入实例,不用碰QueryClient的meta await queryClient.prefetchQuery({ queryKey: ['users'], queryFn: () => getUsers(axiosInstance), }); // 无需再处理meta序列化问题,直接返回脱水状态 return { props: { dehydratedState: dehydrate(queryClient), }, }; };
3. 客户端请求逻辑
在客户端初始化全局Axios实例,自动设置host头,然后在Query配置中直接复用:
// 客户端全局Axios实例,所有请求自动带host头 const clientAxios = axios.create(); clientAxios.interceptors.request.use((config) => { if (typeof window !== 'undefined') { config.headers['X-HOST'] = window.location.host; } return config; }); // 复用的Query配置 const usersQuery = queryOptions({ queryKey: ['users'], queryFn: () => getUsers(clientAxios), }); // 简化Providers组件 const Providers = ({ children, dehydratedState }) => { const queryClient = useQueryClient(); useEffect(() => { hydrate(queryClient, dehydratedState); }, [queryClient, dehydratedState]); return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>; };
这个方案的优点:
- 完全避免meta传递和序列化问题
- API函数可测试性强(Mock Axios实例即可)
- 服务端每个请求对应独立Axios实例,彻底杜绝用户数据泄漏
方案2:利用React Query的queryFnContext统一注入实例
如果希望所有Query自动复用Axios实例,可以用QueryClient的默认配置注入实例,比meta更安全(context不会被序列化到脱水状态)。
1. 服务端配置
export const getServerSideProps = async (context) => { const axiosInstance = axios.create(); axiosInstance.interceptors.request.use((config) => { config.headers['X-HOST'] = context.req.headers.host; return config; }); // 把Axios实例放到queryFnContext而非meta const queryClient = new QueryClient({ defaultOptions: { queries: { queryFnContext: { axiosInstance }, }, }, }); await queryClient.prefetchQuery({ queryKey: ['users'], queryFn: ({ context }) => getUsers(context.axiosInstance), }); return { props: { dehydratedState: dehydrate(queryClient), }, }; };
2. 客户端配置
const Providers = ({ children }) => { const queryClient = useQueryClient(); useEffect(() => { const axiosInstance = axios.create(); axiosInstance.interceptors.request.use((config) => { config.headers['X-HOST'] = window.location.host; return config; }); // 设置客户端QueryClient的默认context queryClient.setQueryDefaults({ queryFnContext: { axiosInstance }, }); }, [queryClient]); return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>; };
3. API函数和Query配置
export const getUsers = async (axiosInstance: AxiosInstance) => { const { data } = await axiosInstance.get('/users'); return data; }; const usersQuery = queryOptions({ queryKey: ['users'], queryFn: ({ context }) => getUsers(context.axiosInstance), });
这个方案的优势:
- 所有Query自动继承Axios实例,无需重复传递
- 服务端和客户端实例完全隔离,无数据泄漏风险
- 避开meta序列化的坑,因为
queryFnContext不会被脱水到客户端
方案3:封装Axios工厂函数,自动适配环境
把host的获取逻辑封装到工厂函数里,减少重复代码,让实例创建更简洁:
// Axios实例工厂函数,自动根据环境设置host export const createAxiosInstance = (serverHost?: string) => { const instance = axios.create(); instance.interceptors.request.use((config) => { // 服务端用传入的host,客户端自动取window.location.host const targetHost = serverHost || (typeof window !== 'undefined' ? window.location.host : ''); if (targetHost) { config.headers['X-HOST'] = targetHost; } return config; }); return instance; }; // 服务端使用 export const getServerSideProps = async (context) => { const axiosInstance = createAxiosInstance(context.req.headers.host); const queryClient = new QueryClient(); await queryClient.prefetchQuery({ queryKey: ['users'], queryFn: () => getUsers(axiosInstance), }); return { props: { dehydratedState: dehydrate(queryClient) } }; }; // 客户端使用 const clientAxios = createAxiosInstance(); const usersQuery = queryOptions({ queryKey: ['users'], queryFn: () => getUsers(clientAxios), });
这个方案把环境判断逻辑收拢,代码更简洁,同时保持实例的独立性。
内容的提问来源于stack exchange,提问作者Vasyl Bielokopytov

