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
相关产品推荐
相关产品推荐

