Meteor集成Apollo Subscription遇WebSocket连接关闭问题求助
我看到你在Meteor 1.6.1.1里配置Apollo Subscription时遇到了WebSocket握手失败的问题,这其实是因为Meteor自带的SockJS服务和Apollo的SubscriptionServer路径冲突导致的。下面我来一步步帮你修复这个问题,让订阅功能正常工作。
问题根源
Meteor默认会在根路径下启动SockJS服务(也就是你错误里看到的/sockjs/...路径),而如果你的Apollo SubscriptionServer直接绑定到Meteor的WebApp.httpServer上,两者会在WebSocket升级请求上产生冲突,导致握手失败。虽然你尝试了noServer: true解决了错误,但没配置客户端的WebSocket链接,所以订阅还是无法工作。
解决方案步骤
1. 修复服务器端SubscriptionServer配置
我们需要让SubscriptionServer不直接绑定到Meteor的HTTP服务器,而是通过Meteor的connectHandlers来处理/subscriptions路径的WebSocket升级请求,这样就能避开和Meteor自带SockJS的冲突。修改你的服务器代码:
import { WebApp } from 'meteor/webapp'; import { execute, subscribe } from 'graphql'; import { SubscriptionServer } from 'subscriptions-transport-ws'; import { schema } from './your-schema-path'; // 替换成你的schema实际路径 // 先创建Apollo Server,添加上下文配置用于用户验证 createApolloServer({ schema, tracing: true, cacheControl: true, context: async ({ req, connection }) => { let loginToken; // HTTP请求从headers提取登录令牌 if (req) { loginToken = req.headers['meteor-login-token']; } // WS订阅从connectionParams提取登录令牌 else if (connection) { loginToken = connection.context['meteor-login-token']; } // 验证令牌并获取用户(使用Meteor原生Accounts方法) const user = loginToken ? await Meteor.users.findOne({ 'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(loginToken) }) : null; return { user }; } }); // 创建SubscriptionServer,设置noServer: true避免直接绑定服务器 const subscriptionServer = new SubscriptionServer({ schema, execute, subscribe, // 处理WS连接时的上下文,传递客户端发来的令牌 onConnect: (connectionParams) => { return { 'meteor-login-token': connectionParams['meteor-login-token'] }; } }, { noServer: true }); // 让Meteor的connectHandlers接管/subscriptions路径的WebSocket升级请求 WebApp.connectHandlers.use('/subscriptions', (req, res) => { // 只处理WebSocket升级请求 if (!req.headers.upgrade || req.headers.upgrade.toLowerCase() !== 'websocket') { res.writeHead(400); res.end('Expected WebSocket upgrade'); return; } // 将请求转发给subscriptionServer处理 subscriptionServer.handleUpgrade(req, req.socket, Buffer.alloc(0), (ws) => { subscriptionServer.onConnect(req, ws); }); });
2. 修复客户端ApolloClient配置
你的客户端目前只配置了HTTP链接,订阅需要使用WebSocket链接,所以要添加WebSocketLink,并用split来区分请求类型:查询/突变走HTTP,订阅走WS。同时要把Meteor的登录令牌通过connectionParams传给服务器:
import { ApolloClient } from 'apollo-client'; import { createHttpLink } from 'apollo-link-http'; import { InMemoryCache } from 'apollo-cache-inmemory'; import { ApolloLink, split } from 'apollo-link'; import { WebSocketLink } from 'apollo-link-ws'; import { getMainDefinition } from 'apollo-utilities'; import { Accounts } from 'meteor/accounts-base'; // HTTP链接,保留原有的auth配置 const httpLink = createHttpLink({ uri: '/graphql' // 确保和Apollo Server的默认路径一致 }); const authLink = new ApolloLink((operation, forward) => { operation.setContext(() => ({ headers: { 'meteor-login-token': Accounts._storedLoginToken(), }, })); return forward(operation); }); // WebSocket链接,用于订阅请求 const wsLink = new WebSocketLink({ uri: `ws://localhost:3000/subscriptions`, // 和服务器端配置的路径完全匹配 options: { reconnect: true, // 传递Meteor登录令牌到服务器 connectionParams: { 'meteor-login-token': Accounts._storedLoginToken() } } }); // 拆分链接:根据请求类型自动选择HTTP或WS const splitLink = split( ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ); }, wsLink, authLink.concat(httpLink) ); // 创建最终的ApolloClient实例 export default new ApolloClient({ link: splitLink, cache: new InMemoryCache(), });
3. 关键验证要点
- 确保服务器端的
/subscriptions路径和客户端WebSocket链接的路径完全一致 - 不管是HTTP请求还是WS订阅,都要正确传递
meteor-login-token,这样服务器上下文才能拿到用户信息 - 你的依赖包版本在Meteor 1.6.1.1上是兼容的,如果后续仍有问题,可以尝试同步升级
subscriptions-transport-ws和apollo-link-ws的版本
测试订阅
现在你可以尝试创建一个简单的订阅查询来验证,比如:
subscription { sampleSubscription { id content } }
在客户端用client.subscribe()或者对应的React Hooks(如useSubscription)发起订阅,应该就能正常接收服务器推送的更新了。
内容的提问来源于stack exchange,提问作者tedex

