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

RTK Query GraphQL如何实现后端要求的文件上传

RTK Query + graphql-request 实现GraphQL文件上传方案

你当前用的@rtk-query/graphql-request-base-query@2.1.0默认仅发送JSON格式请求,不支持GraphQL multipart上传规范,需要自定义baseQuery兼容文件上传逻辑,实现后可完全兼容你已有的普通query/mutation调用,无需修改已有接口代码。


前置依赖安装

先安装提取文件所需的工具包:

npm i extract-files

这个包的作用是递归遍历GraphQL变量,提取其中的File/Blob类型值,同时记录每个文件在变量结构中的路径,用于构造符合规范的multipart请求体。

第一步:编写支持文件上传的自定义baseQuery

替换默认的graphqlRequestBaseQuery,逻辑为:检测到变量中存在文件时自动转multipart格式请求,无文件时走原有JSON请求逻辑。

import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react';
import { ClientError, GraphQLClient } from 'graphql-request';
import { extractFiles } from 'extract-files';

// 初始化GraphQL客户端,保留你之前的所有配置(鉴权、headers、接口地址等)
const gqlClient = new GraphQLClient('/graphql', {
  credentials: 'include',
  headers: {
    // 例:Authorization: `Bearer ${localStorage.getItem('token')}`
  }
});

const customGqlBaseQuery =
  ({ client }: { client: GraphQLClient }): BaseQueryFn<
    { document: string; variables?: Record<string, unknown> },
    unknown,
    Pick<ClientError, 'message' | 'response' | 'request'>
  > =>
  async ({ document, variables }) => {
    try {
      // 提取变量中的所有文件类型值
      const { clone: processedVariables, files } = extractFiles(
        variables ?? {},
        (value) => value instanceof File || value instanceof Blob,
        'variables'
      );

      // 无文件时走原有普通请求逻辑,和之前的调用行为完全一致
      if (files.size === 0) {
        const data = await client.request(document, variables);
        return { data };
      }

      // 构造符合GraphQL multipart上传规范的FormData
      const formData = new FormData();
      // 写入operations字段,包含query语句和替换文件为null的变量
      formData.append(
        'operations',
        JSON.stringify({
          query: document,
          variables: processedVariables
        })
      );

      // 写入map字段,映射文件索引到变量中的对应路径
      const fileMap: Record<string, string[]> = {};
      let index = 0;
      files.forEach((paths) => {
        fileMap[index++] = paths;
      });
      formData.append('map', JSON.stringify(fileMap));

      // 写入所有文件字段,索引和map中的key一一对应
      index = 0;
      files.forEach((_, file) => {
        formData.append(`${index++}`, file as Blob, (file as File).name);
      });

      // 发送请求,复用client原有配置
      const res = await fetch(client.url, {
        method: 'POST',
        headers: client.headers as HeadersInit,
        credentials: 'include',
        body: formData
      });
      const result = await res.json();

      if (result.errors) {
        throw new ClientError(
          { ...result, status: res.status },
          { query: document, variables: processedVariables }
        );
      }

      return { data: result.data };
    } catch (err) {
      const error = err as ClientError;
      return {
        error: {
          message: error.message,
          response: error.response,
          request: error.request
        }
      };
    }
  };

第二步:配置RTK Query的API服务

在createApi中使用自定义的baseQuery,普通接口和上传接口写法完全一致,不需要特殊区分:

export const gqlApi = createApi({
  reducerPath: 'gqlApi',
  baseQuery: customGqlBaseQuery({ client: gqlClient }),
  endpoints: (builder) => ({
    // 原有普通query/mutation不需要任何修改,直接保留即可
    // getXXX: builder.query({...}),
    // updateXXX: builder.mutation({...}),

    // 文件上传mutation示例,入参直接传File对象即可
    uploadUserAvatar: builder.mutation<
      { uploadAvatar: { avatarUrl: string } },
      { userId: string; avatar: File }
    >({
      query: (params) => ({
        document: `
          mutation UploadUserAvatar($userId: String!, $avatar: Upload!) {
            uploadAvatar(userId: $userId, file: $avatar) {
              avatarUrl
            }
          }
        `,
        variables: params
      })
    })
  })
});

// 导出自动生成的hook
export const { useUploadUserAvatarMutation } = gqlApi;

第三步:组件中调用上传接口

和普通mutation的调用方式完全相同,直接将选中的File对象传入参数即可:

const AvatarUpload = () => {
  const [uploadAvatar] = useUploadUserAvatarMutation();

  const handleSelectFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const selectedFile = e.target.files?.[0];
    if (!selectedFile) return;

    const result = await uploadAvatar({
      userId: '当前登录用户ID',
      avatar: selectedFile
    });
    if ('data' in result) {
      console.log('上传成功,头像地址:', result.data.uploadAvatar.avatarUrl);
    }
  };

  return <input type="file" accept="image/*" onChange={handleSelectFile} />;
};

注意事项

  • 该实现完全兼容你已调试通过的所有普通query、mutation,无文件时不会触发multipart格式转换,不会影响原有接口的正常调用
  • 你当前使用的graphql-request@4.2.0版本可直接运行上述代码,若后续升级graphql-request到5.x以上大版本,需要对应调整GraphQLClient实例的属性读取逻辑
  • 后端无需做额外适配,只要支持标准GraphQL Upload类型(和你之前用Apollo Client对接时的后端配置完全一致)即可正常接收文件

内容的提问来源于stack exchange,提问作者user9013856

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 15:12:45