You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 18:45:11