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

GraphQL Apollo Server分两次返回数据:先返初始结果再返异步请求结果

解决方案:用GraphQL订阅实现异步数据推送

这是个很典型的异步后续数据推送场景——你希望先给客户端返回主请求结果,等服务端的异步任务完成后再把后续数据发过去。在Apollo生态里,最优雅的实现方式是利用GraphQL的Subscription(订阅)能力,它能让服务端主动推送数据给客户端,不用客户端反复轮询。下面是具体的实现步骤:

一、服务端改造

1. 先配置Apollo Server支持订阅

首先要确保你的Apollo Server启用了WebSocket订阅支持,这里以apollo-server-express配合graphql-ws为例:

import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import express from 'express';
import { makeExecutableSchema } from '@graphql-tools/schema';

// 定义基础Schema
const typeDefs = `#graphql
  type Query {
    getPlaylistItems(playlistId: String!): [String!]!
  }
  type Subscription {
    playlistExtraData(playlistId: String!): [String!]!
  }
`;

// 后续会补充resolvers,先创建可执行Schema
const schema = makeExecutableSchema({ typeDefs, resolvers });

const app = express();
const httpServer = createServer(app);

// 配置WebSocket服务器
const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql/subscriptions',
});

// 绑定GraphQL Schema到WebSocket
useServer({ schema }, wsServer);

// 初始化并启动Apollo Server
const server = new ApolloServer({ schema });
await server.start();
app.use('/graphql', expressMiddleware(server));

// 启动服务
httpServer.listen(4000, () => {
  console.log('Server running at http://localhost:4000/graphql');
  console.log('Subscriptions ready at ws://localhost:4000/graphql/subscriptions');
});

2. 修改getPlaylistItems Resolver,添加订阅发布逻辑

我们需要一个Pub/Sub机制来管理订阅事件,Apollo官方推荐用graphql-subscriptions里的PubSub:

import { PubSub } from 'graphql-subscriptions';
import axios from 'axios';

const pubsub = new PubSub();
const PLAYLIST_EXTRA_DATA = 'PLAYLIST_EXTRA_DATA'; // 订阅事件标识

const resolvers = {
  Query: {
    async getPlaylistItems(_, { playlistId }) {
      // 1. 发起第一个请求,获取主数据
      const url = `... external api endpoint?playlistId=${playlistId}`;
      const response = await axios.get(url);
      const playlistData = response.data;

      // 2. 异步发起第二个请求,不阻塞主请求返回
      (async () => {
        try {
          const secondUrl = `...another external endpoint?data=${JSON.stringify(playlistData)}`;
          const secondResponse = await axios.get(secondUrl);
          // 3. 第二个请求完成后,发布订阅事件
          await pubsub.publish(PLAYLIST_EXTRA_DATA, {
            playlistExtraData: secondResponse.data,
            playlistId, // 用来过滤订阅,确保数据精准推送给对应客户端
          });
        } catch (err) {
          console.error('Second request failed:', err);
          // 可选:发布错误事件通知客户端
        }
      })();

      // 4. 先返回主数据给客户端
      return playlistData;
    },
  },
  Subscription: {
    playlistExtraData: {
      // 订阅时过滤事件,只接收当前playlistId对应的数据
      subscribe: (_, { playlistId }) => {
        return pubsub.asyncIterator([PLAYLIST_EXTRA_DATA]);
      },
      resolve: (payload, args) => {
        if (payload.playlistId === args.playlistId) {
          return payload.playlistExtraData;
        }
        return null;
      },
    },
  },
};

二、客户端改造

1. 配置Apollo Client支持订阅

如果是React项目,需要给Apollo Client添加WebSocket链接支持:

import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';

// HTTP链接:处理Query和Mutation
const httpLink = new HttpLink({
  uri: 'http://localhost:4000/graphql',
});

// WebSocket链接:处理Subscription
const wsLink = new GraphQLWsLink(createClient({
  url: 'ws://localhost:4000/graphql/subscriptions',
}));

// 自动拆分链接:根据操作类型选择HTTP或WebSocket
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink,
);

// 创建Apollo Client实例
const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
});

2. 发起查询+订阅后续数据

在React组件里,你可以同时处理初始查询和后续订阅:

import { useQuery, useSubscription, gql } from '@apollo/client';

// 定义GraphQL语句
const GET_PLAYLIST_ITEMS = gql`
  query GetPlaylistItems($playlistId: String!) {
    getPlaylistItems(playlistId: $playlistId)
  }
`;

const PLAYLIST_EXTRA_DATA_SUBSCRIPTION = gql`
  subscription PlaylistExtraData($playlistId: String!) {
    playlistExtraData(playlistId: $playlistId)
  }
`;

function PlaylistComponent({ playlistId }) {
  // 1. 发起主查询,获取初始播放列表数据
  const { loading, data, error } = useQuery(GET_PLAYLIST_ITEMS, {
    variables: { playlistId },
  });

  // 2. 订阅后续的额外数据
  const { data: extraData } = useSubscription(PLAYLIST_EXTRA_DATA_SUBSCRIPTION, {
    variables: { playlistId },
  });

  if (loading) return <p>加载播放列表中...</p>;
  if (error) return <p>出错了:{error.message}</p>;

  return (
    <div>
      <h3>播放列表:</h3>
      <ul>
        {data.getPlaylistItems.map(title => (
          <li key={title}>{title}</li>
        ))}
      </ul>
      {extraData && (
        <div>
          <h3>额外数据:</h3>
          <ul>
            {extraData.playlistExtraData.map(item => (
              <li key={item}>{item}</li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

备选方案:轮询(适合不支持WebSocket的场景)

如果你的环境无法使用WebSocket,也可以用轮询方案:

  • 服务端在第二个请求完成后,把数据存入缓存(比如Redis),并给客户端返回一个唯一的taskId
  • 客户端拿到初始数据和taskId后,定期调用一个新的Query(比如getExtraData(taskId: String!))检查数据是否就绪

不过这种方式效率较低,不如订阅优雅,优先推荐用Subscription方案。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 08:14:09