在 next.js api 路由中使用订阅的 Apollo 服务器:websockets 问题
Apollo Server with subscriptions used inside next.js api routes: websockets trouble
我尝试在 next.js 9.x 应用程序中设置 GraphQL 订阅。该应用程序完全是假的,它只是为了尝试 Apollo Server 订阅。 "database" 只是一个数组,我将新用户推送给它。
这是我目前得到的代码。
import { ApolloServer, gql, makeExecutableSchema } from "apollo-server-micro"
import { PubSub } from "apollo-server"
const typeDefs = gql`
type User {
id: ID!
name: String
status: String
}
type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
addUser(id: String, name: String, status: String): User
}
type Subscription {
newUser: User!
}
`
const fakedb = [
{
id: "1",
name: "myname",
status: "active",
},
]
const NEW_USER = "NEW_USER"
const resolvers = {
Subscription: {
newUser: {
subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(NEW_USER),
},
},
Query: {
users: (parent, args, context) => {
console.log(context)
return fakedb
},
user: (_, { id }) => {
console.log(id)
console.log(fakedb)
return fakedb.find((user) => user.id == id)
},
},
Mutation: {
addUser(_, { id, name, status }, { pubsub }) {
console.log(pubsub)
const newUser = {
id,
name,
status,
}
pubsub.publish(NEW_USER, { newUser: newUser })
fakedb.push(newUser)
return newUser
},
},
}
export const schema = makeExecutableSchema({
typeDefs,
resolvers,
})
const pubsub = new PubSub()
const apolloServer = new ApolloServer({
// typeDefs,
// resolvers,
schema,
context: ({ req, res }) => {
return { req, res, pubsub }
},
introspection: true,
subscriptions: {
path: "/api/graphql",
// keepAlive: 15000,
onConnect: () => console.log("connected"),
onDisconnect: () => console.log("disconnected"),
},
})
export const config = {
api: {
bodyParser: false,
},
}
export default apolloServer.createHandler({ path: "/api/graphql" })
我运行这个订阅在localhost:3000/api/graphql:
订阅{
新用户 {
ID
姓名
}
}
我收到这个错误。我不确定在哪里以及如何解决这个问题,因为我找不到任何关于这个的文档。
{
"error": "Could not connect to websocket endpoint ws://localhost:3000/api/graphql. Please check if the endpoint url is correct."
}
我发现了如何添加订阅路径,因为它之前抱怨过(之前是/graphql)。但是还是不行。
我就是这样实现的。
import { ApolloServer } from 'apollo-server-micro';
import schema from './src/schema';
const apolloServer = new ApolloServer({
schema,
context: async ({ req, connection }) => {
if (connection) {
// check connection for metadata
return connection.context;
}
// get the user from the request
return {
user: req.user,
useragent: req.useragent,
};
},
subscriptions: {
path: '/api/graphqlSubscriptions',
keepAlive: 9000,
onConnect: console.log('connected'),
onDisconnect: () => console.log('disconnected'),
},
playground: {
subscriptionEndpoint: '/api/graphqlSubscriptions',
settings: {
'request.credentials': 'same-origin',
},
},
});
export const config = {
api: {
bodyParser: false,
},
};
const graphqlWithSubscriptionHandler = (req, res, next) => {
if (!res.socket.server.apolloServer) {
console.log(`* apolloServer first use *`);
apolloServer.installSubscriptionHandlers(res.socket.server);
const handler = apolloServer.createHandler({ path: '/api/graphql' });
res.socket.server.apolloServer = handler;
}
return res.socket.server.apolloServer(req, res, next);
};
export default graphqlWithSubscriptionHandler;
只需确保 websocket 路径有效。 https://www.websocket.org/echo.html
我受到@ordepim 的回答的启发,并以这种方式解决了热重载问题(我还添加了类型):
import { ApolloServer } from 'apollo-server-micro'
import { NextApiRequest, NextApiResponse } from 'next'
import { schema } from '../../lib/schema'
//note: this log occurs on every hot-reload
console.log('CREATING APOLLOSERVER ')
const apolloServer = new ApolloServer({
schema,
context: async ({ req, connection }) => {
if (connection) {
// check connection for metadata
return connection.context
}
// get the user from the request
return {
user: req.user,
useragent: req.useragent,
}
},
subscriptions: {
path: '/api/graphqlSubscriptions',
keepAlive: 9000,
onConnect: () => console.log('connected'),
onDisconnect: () => console.log('disconnected'),
},
playground: {
subscriptionEndpoint: '/api/graphqlSubscriptions',
settings: {
'request.credentials': 'same-origin',
},
},
})
export const config = {
api: {
bodyParser: false,
},
}
type CustomSocket = Exclude<NextApiResponse<any>['socket'], null> & {
server: Parameters<ApolloServer['installSubscriptionHandlers']>[0] & {
apolloServer?: ApolloServer
apolloServerHandler?: any
}
}
type CustomNextApiResponse<T = any> = NextApiResponse<T> & {
socket: CustomSocket
}
const graphqlWithSubscriptionHandler = (
req: NextApiRequest,
res: CustomNextApiResponse
) => {
const oldOne = res.socket.server.apolloServer
if (
//we need compare old apolloServer with newOne, becasue after hot-reload are not equals
oldOne &&
oldOne !== apolloServer
) {
console.warn('FIXING HOT RELOAD !!!!!!!!!!!!!!! ')
delete res.socket.server.apolloServer
}
if (!res.socket.server.apolloServer) {
console.log(`* apolloServer (re)initialization *`)
apolloServer.installSubscriptionHandlers(res.socket.server)
res.socket.server.apolloServer = apolloServer
const handler = apolloServer.createHandler({ path: '/api/graphql' })
res.socket.server.apolloServerHandler = handler
//clients losts old connections, but clients are able to reconnect
oldOne?.stop()
}
return res.socket.server.apolloServerHandler(req, res)
}
export default graphqlWithSubscriptionHandler
他们已从 appollo-server v3.
中删除订阅支持
v3 的更新解决方案如下所示。这是打字稿,但您可以将其改编为 JS 删除类型。
import { ApolloServer } from 'apollo-server-micro'
import { makeExecutableSchema } from '@graphql-tools/schema';
import { useServer } from 'graphql-ws/lib/use/ws';
import { Disposable } from 'graphql-ws';
import Cors from 'micro-cors'
import type { NextApiRequest } from 'next'
import { WebSocketServer } from 'ws';
import { typeDefs } from '../../graphql/schema'
import { resolvers } from '../../graphql/resolvers'
import { NextApiResponseServerIO } from '../../types/next';
const schema = makeExecutableSchema({ typeDefs, resolvers });
const cors = Cors()
let serverCleanup: Disposable | null = null;
const apolloServer = new ApolloServer({
schema,
plugins: [
// Proper shutdown for the WebSocket server.
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup?.dispose();
},
};
},
},
]
});
const startServer = apolloServer.start()
const getHandler = async () => {
await startServer;
return apolloServer.createHandler({
path: '/api/graphql',
});
}
const wsServer = new WebSocketServer({
noServer: true
});
export default cors(async function handler(req: any, res: any) {
if (req.method === 'OPTIONS') {
res.end()
return false
}
res.socket.server.ws ||= (() => {
res.socket.server.on('upgrade', function (request, socket, head) {
wsServer.handleUpgrade(request, socket, head, function (ws) {
wsServer.emit('connection', ws);
})
})
serverCleanup = useServer({ schema }, wsServer);
return wsServer;
})();
const h = await getHandler();
await h(req, res)
})
export const config = {
api: {
bodyParser: false,
},
}
该解决方案启动服务器一次,并与查询/变更一起使用。请注意,在 graphql 资源管理器中,应选择较新版本的协议 (graphql-ws
)。可能此解决方案不适用于旧协议,这应该不是问题。
我尝试在 next.js 9.x 应用程序中设置 GraphQL 订阅。该应用程序完全是假的,它只是为了尝试 Apollo Server 订阅。 "database" 只是一个数组,我将新用户推送给它。
这是我目前得到的代码。
import { ApolloServer, gql, makeExecutableSchema } from "apollo-server-micro"
import { PubSub } from "apollo-server"
const typeDefs = gql`
type User {
id: ID!
name: String
status: String
}
type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
addUser(id: String, name: String, status: String): User
}
type Subscription {
newUser: User!
}
`
const fakedb = [
{
id: "1",
name: "myname",
status: "active",
},
]
const NEW_USER = "NEW_USER"
const resolvers = {
Subscription: {
newUser: {
subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(NEW_USER),
},
},
Query: {
users: (parent, args, context) => {
console.log(context)
return fakedb
},
user: (_, { id }) => {
console.log(id)
console.log(fakedb)
return fakedb.find((user) => user.id == id)
},
},
Mutation: {
addUser(_, { id, name, status }, { pubsub }) {
console.log(pubsub)
const newUser = {
id,
name,
status,
}
pubsub.publish(NEW_USER, { newUser: newUser })
fakedb.push(newUser)
return newUser
},
},
}
export const schema = makeExecutableSchema({
typeDefs,
resolvers,
})
const pubsub = new PubSub()
const apolloServer = new ApolloServer({
// typeDefs,
// resolvers,
schema,
context: ({ req, res }) => {
return { req, res, pubsub }
},
introspection: true,
subscriptions: {
path: "/api/graphql",
// keepAlive: 15000,
onConnect: () => console.log("connected"),
onDisconnect: () => console.log("disconnected"),
},
})
export const config = {
api: {
bodyParser: false,
},
}
export default apolloServer.createHandler({ path: "/api/graphql" })
我运行这个订阅在localhost:3000/api/graphql:
订阅{ 新用户 { ID 姓名 } }
我收到这个错误。我不确定在哪里以及如何解决这个问题,因为我找不到任何关于这个的文档。
{ "error": "Could not connect to websocket endpoint ws://localhost:3000/api/graphql. Please check if the endpoint url is correct." }
我发现了如何添加订阅路径,因为它之前抱怨过(之前是/graphql)。但是还是不行。
我就是这样实现的。
import { ApolloServer } from 'apollo-server-micro';
import schema from './src/schema';
const apolloServer = new ApolloServer({
schema,
context: async ({ req, connection }) => {
if (connection) {
// check connection for metadata
return connection.context;
}
// get the user from the request
return {
user: req.user,
useragent: req.useragent,
};
},
subscriptions: {
path: '/api/graphqlSubscriptions',
keepAlive: 9000,
onConnect: console.log('connected'),
onDisconnect: () => console.log('disconnected'),
},
playground: {
subscriptionEndpoint: '/api/graphqlSubscriptions',
settings: {
'request.credentials': 'same-origin',
},
},
});
export const config = {
api: {
bodyParser: false,
},
};
const graphqlWithSubscriptionHandler = (req, res, next) => {
if (!res.socket.server.apolloServer) {
console.log(`* apolloServer first use *`);
apolloServer.installSubscriptionHandlers(res.socket.server);
const handler = apolloServer.createHandler({ path: '/api/graphql' });
res.socket.server.apolloServer = handler;
}
return res.socket.server.apolloServer(req, res, next);
};
export default graphqlWithSubscriptionHandler;
只需确保 websocket 路径有效。 https://www.websocket.org/echo.html
我受到@ordepim 的回答的启发,并以这种方式解决了热重载问题(我还添加了类型):
import { ApolloServer } from 'apollo-server-micro'
import { NextApiRequest, NextApiResponse } from 'next'
import { schema } from '../../lib/schema'
//note: this log occurs on every hot-reload
console.log('CREATING APOLLOSERVER ')
const apolloServer = new ApolloServer({
schema,
context: async ({ req, connection }) => {
if (connection) {
// check connection for metadata
return connection.context
}
// get the user from the request
return {
user: req.user,
useragent: req.useragent,
}
},
subscriptions: {
path: '/api/graphqlSubscriptions',
keepAlive: 9000,
onConnect: () => console.log('connected'),
onDisconnect: () => console.log('disconnected'),
},
playground: {
subscriptionEndpoint: '/api/graphqlSubscriptions',
settings: {
'request.credentials': 'same-origin',
},
},
})
export const config = {
api: {
bodyParser: false,
},
}
type CustomSocket = Exclude<NextApiResponse<any>['socket'], null> & {
server: Parameters<ApolloServer['installSubscriptionHandlers']>[0] & {
apolloServer?: ApolloServer
apolloServerHandler?: any
}
}
type CustomNextApiResponse<T = any> = NextApiResponse<T> & {
socket: CustomSocket
}
const graphqlWithSubscriptionHandler = (
req: NextApiRequest,
res: CustomNextApiResponse
) => {
const oldOne = res.socket.server.apolloServer
if (
//we need compare old apolloServer with newOne, becasue after hot-reload are not equals
oldOne &&
oldOne !== apolloServer
) {
console.warn('FIXING HOT RELOAD !!!!!!!!!!!!!!! ')
delete res.socket.server.apolloServer
}
if (!res.socket.server.apolloServer) {
console.log(`* apolloServer (re)initialization *`)
apolloServer.installSubscriptionHandlers(res.socket.server)
res.socket.server.apolloServer = apolloServer
const handler = apolloServer.createHandler({ path: '/api/graphql' })
res.socket.server.apolloServerHandler = handler
//clients losts old connections, but clients are able to reconnect
oldOne?.stop()
}
return res.socket.server.apolloServerHandler(req, res)
}
export default graphqlWithSubscriptionHandler
他们已从 appollo-server v3.
中删除订阅支持v3 的更新解决方案如下所示。这是打字稿,但您可以将其改编为 JS 删除类型。
import { ApolloServer } from 'apollo-server-micro'
import { makeExecutableSchema } from '@graphql-tools/schema';
import { useServer } from 'graphql-ws/lib/use/ws';
import { Disposable } from 'graphql-ws';
import Cors from 'micro-cors'
import type { NextApiRequest } from 'next'
import { WebSocketServer } from 'ws';
import { typeDefs } from '../../graphql/schema'
import { resolvers } from '../../graphql/resolvers'
import { NextApiResponseServerIO } from '../../types/next';
const schema = makeExecutableSchema({ typeDefs, resolvers });
const cors = Cors()
let serverCleanup: Disposable | null = null;
const apolloServer = new ApolloServer({
schema,
plugins: [
// Proper shutdown for the WebSocket server.
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup?.dispose();
},
};
},
},
]
});
const startServer = apolloServer.start()
const getHandler = async () => {
await startServer;
return apolloServer.createHandler({
path: '/api/graphql',
});
}
const wsServer = new WebSocketServer({
noServer: true
});
export default cors(async function handler(req: any, res: any) {
if (req.method === 'OPTIONS') {
res.end()
return false
}
res.socket.server.ws ||= (() => {
res.socket.server.on('upgrade', function (request, socket, head) {
wsServer.handleUpgrade(request, socket, head, function (ws) {
wsServer.emit('connection', ws);
})
})
serverCleanup = useServer({ schema }, wsServer);
return wsServer;
})();
const h = await getHandler();
await h(req, res)
})
export const config = {
api: {
bodyParser: false,
},
}
该解决方案启动服务器一次,并与查询/变更一起使用。请注意,在 graphql 资源管理器中,应选择较新版本的协议 (graphql-ws
)。可能此解决方案不适用于旧协议,这应该不是问题。