在 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)。可能此解决方案不适用于旧协议,这应该不是问题。