NextJS集成GraphQL Apollo Subscription失败求助及问题排查
问题分析与解决方案
问题概述
使用NextJS开发基于GraphQL Subscription的聊天应用,后端采用graphql-yoga(NextJS Serverless环境),前端使用Apollo Client。后端在GraphQL Playground中运行正常,但前端集成后出现两个问题:
- 终端抛出错误:
TypeError [ERR_INVALID_ARG_TYPE]: The "string" argument must be of type string or an instance of Buffer or ArrayBuffer. Received type function ([Function (anonymous)]) - 前端
useSubscription返回{Error: undefined}
终端错误信息
error - TypeError [ERR_INVALID_ARG_TYPE]: The "string" argument must be of type string or an instance of Buffer or ArrayBuffer. Received type function ([Function (anonymous)])
代码片段
后端代码
import { createServer, createPubSub } from "@graphql-yoga/node"; const pubsub = createPubSub(); const NEW_MESSAGE = "NEW_MESSAGE"; const NEW_MESSAGES = "NEW_MESSAGES"; const messages = []; const resolvers = { Query: { messages: (_, { name }, context) => messages, }, Mutation: { createMessage(parent, { user, body }, { pubsub }) { const id = Math.floor(Math.random() * 10000000); messages.push({ id, user, body }); pubsub.publish(NEW_MESSAGE, { newMessage: { id, user, body }, }); pubsub.publish(NEW_MESSAGES); return true; }, }, Subscription: { newMessage: { subscribe: (_, __, { pubsub }) => { return pubsub.subscribe(NEW_MESSAGE); }, resolve: (payload) => { const { message } = payload; const { id, user, body } = message; return { id, user, body }; }, }, newMessages: { subscribe: (_, __, { pubsub }) => { return pubsub.subscribe(NEW_MESSAGES); }, resolve: () => messages, }, }, }; const typeDefs = ` type Message { id: ID! user: String! body: String! } type MessageInput { user: String! body: String! } type SubscriptionPayload { id: ID user: String body: String } type Query { messages: [Message!] } type Mutation { createMessage(user: String!, body: String!): Boolean! } type Subscription { newMessage: Message newMessages: [Message] } `; const server = createServer({ context: async ({ req }) => { return { user: { name: "jogn Doe", id: 0 }, pubsub, }; }, schema: { typeDefs, resolvers, }, }); export default server;
前端代码
_app.js
import "../styles/globals.css"; import { ApolloProvider } from "@apollo/client"; import { client } from "./apollo"; function MyApp({ Component, pageProps }) { return ( <ApolloProvider client={client}> <Component {...pageProps} /> </ApolloProvider> ); } export default MyApp;
apollo.js
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; import { GraphQLWsLink } from "@apollo/client/link/subscriptions"; import { createClient } from "graphql-ws"; const uri = 'localhost:3000/api/graphql'; const httpLink = new HttpLink({ uri: `http://${uri}` }); const wsLink = () => { return new GraphQLWsLink(createClient({ url: `ws://${uri}` })); } export const client = new ApolloClient({ link: typeof window === 'undefined' ? httpLink : wsLink(), cache: new InMemoryCache(), });
index.js
import Head from "next/head"; import styles from "../styles/Home.module.css"; import { gql, useSubscription } from "@apollo/client"; export default function Home() { const SUBSCRIPTION = gql` subscription newMessages { newMessages { id user body } } `; const { loading, error, data } = useSubscription(SUBSCRIPTION); console.log({ data, loading, error }); console.log(error); // {Error: undefined} is printed return ( <div className={styles.container}> <Head> <title>Create Next App</title> <meta name="description" content="Generated by create next app" /> <link rel="icon" href="/favicon.ico" /> </Head> <main> <div>Hello Nextjs Graphql</div> </main> </div> ); }
问题定位与修复
1. 前端Apollo Client链接配置错误
apollo.js中wsLink定义为函数,但Apollo Client的link参数需要直接传入Link实例,而非函数。同时建议用split自动区分HTTP和WebSocket操作:
修改后的apollo.js:
import { ApolloClient, HttpLink, InMemoryCache, split } from "@apollo/client"; import { GraphQLWsLink } from "@apollo/client/link/subscriptions"; import { createClient } from "graphql-ws"; import { getMainDefinition } from '@apollo/client/utilities'; const uri = 'localhost:3000/api/graphql'; const httpLink = new HttpLink({ uri: `http://${uri}` }); const wsLink = new GraphQLWsLink(createClient({ url: `ws://${uri}` })); // 自动根据操作类型选择链接 const splitLink = split( ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ); }, wsLink, httpLink, ); export const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache(), });
2. 后端Subscription解析器与Publish逻辑错误
newMessage解析器中,payload的键是newMessage而非message,导致取值undefined:resolve: (payload) => { // 原代码错误:const { message } = payload; const { newMessage } = payload; const { id, user, body } = newMessage; return { id, user, body }; },newMessages发布时未传递payload,导致订阅无法获取数据:
对应的解析器可简化:// 原代码错误:pubsub.publish(NEW_MESSAGES); pubsub.publish(NEW_MESSAGES, { newMessages: messages });resolve: (payload) => payload.newMessages,
3. NextJS Serverless环境适配问题
NextJS Serverless函数是无状态的,每次请求会启动新实例,默认内存PubSub无法跨实例共享数据,导致消息无法广播到所有在线用户。解决方案:
- 使用Redis作为外部PubSub服务,替代内存PubSub
- 将消息数据存储到Redis/数据库,而非内存数组
后端改造示例(Redis PubSub)
- 安装依赖:
npm install @graphql-yoga/plugin-redis-pubsub ioredis
- 修改后端代码:
import { createServer } from "@graphql-yoga/node"; import { useRedisPubSub } from '@graphql-yoga/plugin-redis-pubsub'; import Redis from 'ioredis'; // 初始化Redis客户端 const redis = new Redis({ host: '你的Redis地址', port: 6379, // 若有密码则添加 password: '你的Redis密码' }); const NEW_MESSAGE = "NEW_MESSAGE"; const NEW_MESSAGES = "NEW_MESSAGES"; // 改用Redis存储消息示例(替代内存数组) const getMessages = async () => { const messageStrings = await redis.lrange('chat_messages', 0, -1); return messageStrings.map(JSON.parse); }; const addMessage = async (message) => { await redis.lpush('chat_messages', JSON.stringify(message)); // 限制消息数量,避免内存溢出 await redis.ltrim('chat_messages', 0, 99); }; const resolvers = { Query: { messages: async () => getMessages(), }, Mutation: { createMessage: async (parent, { user, body }, { pubsub }) => { const id = Math.floor(Math.random() * 10000000); const message = { id, user, body }; await addMessage(message); pubsub.publish(NEW_MESSAGE, { newMessage: message }); pubsub.publish(NEW_MESSAGES, { newMessages: await getMessages() }); return true; }, }, Subscription: { newMessage: { subscribe: (_, __, { pubsub }) => pubsub.subscribe(NEW_MESSAGE), resolve: (payload) => payload.newMessage, }, newMessages: { subscribe: (_, __, { pubsub }) => pubsub.subscribe(NEW_MESSAGES), resolve: (payload) => payload.newMessages, }, }, }; const server = createServer({ plugins: [ useRedisPubSub({ redis, publish: { channelPrefix: 'chat-app:' }, subscribe: { channelPrefix: 'chat-app:' } }) ], context: async ({ req }) => { return { user: { name: "jogn Doe", id: 0 }, // PubSub由插件自动注入,无需手动传入 }; }, schema: { typeDefs, resolvers, }, }); export default server;
总结
- 前端优先修复Apollo Client的链接配置,确保WebSocket链接实例正确传入
- 后端修正Subscription解析器的payload取值与Publish逻辑
- NextJS Serverless环境下必须使用外部PubSub服务(如Redis)实现跨实例消息广播,同时消息数据需存储到外部介质,不能依赖内存
内容的提问来源于stack exchange,提问作者Gwenda Thomas
相关产品推荐
相关产品推荐

