首页 > 解决方案 > 无法确定 Next.js 的正确配置,使用 Apollo/GraphQL-Yoga 进行上传和订阅

问题描述

请原谅我的新手问题,但我很难让它正常工作。

我很难让 Next.js、Apollo 和 GraphQL-Yoga 的订阅行为正常。我已经尝试到处寻找答案,并且已经为此工作了大约 2 周,但没有运气。

我的代码从 Wes Bos 的 Advanced GraphQL 课程代码开始,它使用 Next.js、Apollo、Prisma 和 GraphQL-Yoga。它还使用 cookie 来存储 JWT 令牌,我认为这可能会导致 WebSocketLink 身份验证出现问题,但我可能是错的。我已成功将上传添加到 Amazon S3,但我一直在努力让订阅正常工作。我不断收到超时,与 websocket 断开连接,有时我在控制台中看到大量的连接/断开连接消息,这些消息似乎不正确,但我不能 100% 确定会发生什么。我想在我的网站上拥有实时聊天功能以及通知。我以为我在某一时刻完成了所有工作,但它似乎是断断续续的。有时它可以工作并且聊天/通知会立即出现,有时它们不会

无论如何,这是我的代码:

客户pages/_app.js

import App, { Container } from "next/app";
import Root from "../components/Root";
import { ApolloProvider } from "@apollo/react-hooks";
import withData from "../lib/withData";

class MyApp extends App {
  // gets all page properties and queries before loading to pass along
  static async getInitialProps({ Component, ctx }) {
    let pageProps = {};
    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }
    // this exposes the query to the user
    pageProps.query = ctx.query;
    return { pageProps };
  }

  render() {
    const { Component, apollo, pageProps } = this.props;

    return (
      <Container>
        <ApolloProvider client={apollo}>
          <Root client={apollo}>
            <Component client={apollo} {...pageProps} />
          </Root>
        </ApolloProvider>
      </Container>
    );
  }
}

export default withData(MyApp);

客户lib/withData.js

  import withApollo from "next-with-apollo";
  import { ApolloClient } from "apollo-client";
  import { InMemoryCache } from "apollo-cache-inmemory";
  import { HttpLink } from "apollo-link-http";
  import * as ws from "ws";
  import { WebSocketLink } from "apollo-link-ws";
  import { getMainDefinition } from "apollo-utilities";
  import { onError } from "apollo-link-error";
  import { ApolloLink, Observable, split } from "apollo-link";
  import { RetryLink } from "apollo-link-retry";
  import { createUploadLink } from "apollo-upload-client";
  import { endpoint, wsEndpoint } from "../config";

  const request = (operation, headers) => {
    operation.setContext({
      fetchOptions: {
        credentials: "include"
      },
      headers
    });
  };

  function createClient({ ctx, headers, initialState }) {
    const httpLink = new HttpLink({
      uri: process.env.NODE_ENV === "development" ? endpoint : endpoint
    });
    const wsLink = process.browser
      ? new WebSocketLink({
          uri: process.env.NODE_ENV === "development" ? wsEndpoint : wsEndpoint,
          options: {
            reconnect: true
          }
        })
      : () => {
          console.log("SSR");
        };

    return new ApolloClient({
      link: ApolloLink.from([
        onError(({ graphQLErrors, networkError }) => {
          if (graphQLErrors)
            graphQLErrors.map(({ message, locations, path }) =>
              console.log(
                `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
              )
            );
          // if (networkError) console.log(`[Network error]: ${networkError}`);
          if (networkError) console.log("[Network error]: ", networkError);
        }),
        new ApolloLink(
          (operation, forward) =>
            new Observable(observer => {
              let handle;
              Promise.resolve(operation)
                .then(oper => request(oper, headers))
                .then(() => {
                  handle = forward(operation).subscribe({
                    next: observer.next.bind(observer),
                    error: observer.error.bind(observer),
                    complete: observer.complete.bind(observer)
                  });
                })
                .catch(observer.error.bind(observer));

              return () => {
                if (handle) handle.unsubscribe();
              };
            })
        ),
        new RetryLink().split(
          ({ query }) => {
            const definition = getMainDefinition(query);
            return (
              definition.kind === "OperationDefinition" &&
              definition.operation === "subscription"
            );
          },
          wsLink,
          // httpLink,
          // ERROR: httpLink breaks uploader
          createUploadLink({
            uri: process.env.NODE_ENV === "development" ? endpoint : endpoint
          })
        )
      ]),
      cache: new InMemoryCache().restore(initialState || {})
    });
  }

  export default withApollo(createClient);

服务器index.js

require("dotenv").config({ path: ".env" });
const cookie = require("cookie");
const cookieParser = require("cookie-parser");
const cors = require("cors");
const jwt = require("jsonwebtoken");
const expressip = require("express-ip");
const helmet = require("helmet");
const compression = require("compression");
const { S3 } = require("aws-sdk");

const logger = require("./utils/logger");

const createServer = require("./createServer");
const db = require("./db");

const server = createServer();

const s3client = new S3({
  accessKeyId: process.env.S3_KEY,
  secretAccessKey: process.env.S3_SECRET,
  params: {
    Bucket: process.env.S3_BUCKET
  }
});

server.express.use(helmet());
server.express.use(compression());
server.express.use(cookieParser());
server.express.use(expressip().getIpInfoMiddleware);

// get the users IP information
server.express.use((req, res, next) => {
  const userIp = req.ipInfo;
  if (userIp) {
    req.userIp = userIp;
  }
  next();
});

// decode the JWT so we can ge the user ID on each request
server.express.use((req, res, next) => {
  const { token } = req.cookies;
  if (token) {
    const { userId } = jwt.verify(token, process.env.APP_SECRET);
    // put the userId onto the request for future requests to access
    req.userId = userId;
  }
  next();
});

// create a middleware that populates the user on each request
server.express.use(async (req, res, next) => {
  // if they aren't logged in, skip this
  if (!req.userId) return next();
  const user = await db.query.user(
    { where: { id: req.userId } },
    "{ id, email, emailMask, emailVerified, role, permissions, account { accountType isBanned } }"
  );
  req.user = user;
  next();
});

const corsWhitelist = [
  process.env.FRONTEND_URL,
  process.env.ADMIN_URL
];

server.express.use(
  "/*",
  cors({
    credentials: true,
    origin: function(origin, callback) {
      // note: if same origin makes requests with origin header, it needs to be whitelisted

      if (corsWhitelist.indexOf(origin) !== -1) {
        // console.log("express origin on whitelist");
        // console.log("express origin", origin);
        callback(null, true);
      } else if (origin === undefined) {
        // console.log("express origin is undefined");
        callback(null, true);
      } else {
        // callback(null, true)
        callback(new Error(`${origin} not allowed by CORS`));
      }
    }
  })
); // allow cors

// if (process.env.NODE_ENV === 'development') server.express.use(logger);

server.start(
  {
    cors: {
      credentials: true,
      origin: function(origin, callback) {
        // note: if same origin makes requests with origin header, it needs to be whitelisted

        if (corsWhitelist.indexOf(origin) !== -1) {
          // console.log("gql origin on whitelist");
          // console.log("gql origin", origin);
          callback(null, true);
        } else if (origin === undefined) {
          // console.log("gql origin is undefined");
          callback(null, true);
        } else {
          // callback(null, true)
          callback(new Error(`${origin} not allowed by CORS`));
        }
      }
    },
    subscriptions: {
      keepAlive: true,
      onConnect: async (connectionParams, webSocket) => {
        console.log("Websocket CONNECTED");
        const header = webSocket.upgradeReq.headers.cookie;
        const { token } = cookie.parse(header);
        try {
          const promise = new Promise((resolve, reject) => {
            const { userId } = jwt.verify(token, process.env.APP_SECRET);
            resolve(userId);
          });
          const user = await promise;
          return user;
        } catch (err) {
          throw new Error(err);
        }
      },
      onDisconnect: () => {
        console.log("Websocket DISCONNECTED");
      }
    }
  },
  deets => {
    console.log(
      ` Backend is now running on port http:/localhost:${deets.port}`
    );
  }
);

服务器createServer.js

const { GraphQLServer, PubSub } = require("graphql-yoga");
const depthLimit = require("graphql-depth-limit");
const Mutation = require("./resolvers/Mutation");
const Query = require("./resolvers/Query");
const Subscription = require("./resolvers/Subscription");
const Conversation = require("./resolvers/Conversation");
const db = require("./db");

const pubsub = new PubSub();

// Create the GraphQL Yoga Server

function createServer() {
  return new GraphQLServer({
    typeDefs: "src/schema.graphql",
    resolvers: {
      Mutation,
      Query,
      Subscription,
      Conversation
    },
    resolverValidationOptions: {
      requireResolversForResolveType: false
    },
    // https://github.com/stems/graphql-depth-limit
    // https://blog.apollographql.com/securing-your-graphql-api-from-malicious-queries-16130a324a6b
    validationRules: [depthLimit(10)],
    uploads: {
      // Limits here should be stricter than config for surrounding
      // infrastructure such as Nginx so errors can be handled elegantly by
      // graphql-upload:
      // https://github.com/jaydenseric/graphql-upload#type-uploadoptions
      maxFileSize: 10000000, // 10 MB
      maxFiles: 20
    },
    context: (req, connection) => ({
      ...req,
      pubsub,
      db
    })
  });
}

module.exports = createServer;

我也不断收到WebSocket connection to 'ws://localhost:4444/' failed: WebSocket is closed before the connection is established.错误,这可能是我的主要问题,但我不知道如何解决或清理它。

任何帮助将不胜感激,因为这非常令人沮丧!

谢谢!

标签: next.jsreact-apolloexpress-graphqlgraphql-subscriptions

解决方案


推荐阅读