【问题标题】:Apollo server set cookie to be accessed in next js appApollo 服务器设置 cookie 以在下一个 js 应用程序中访问
【发布时间】:2020-02-19 10:45:05
【问题描述】:

我有一个 Next.js 应用程序,我正在使用 apollo-server-micro 和 apollo-client。当用户点击登录突变时,我无法设置 cookie 会话。这是我的 server.js 设置(npm start)

const express = require('express');
const next = require('next');
const { apolloServer } = require('pages/api/graphql');
const cookieSession = require('cookie-session');

const dev = process.env.NODE_ENV !== 'production';
const app = nextApp({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const server = express();

  server.use(express.static('./public'));

  server.use(
        '/api',
        cookieSession({
            httpOnly: false,
            keys: ['super-secret-key'],
            maxAge: 24 * 60 * 60 * 1000,
            name: 'xyz',
        })
    );

  server.get('*', handle);

  server.listen(8000, () => {
    console.log('Server running on port 8000');
  })
})

pages/api/graphql.js 中,我有以下代码(直接来自 Next.js 文档)

import { ApolloServer } from 'apollo-server-micro'
import { schema } from '../../apollo/schema'

export const apolloServer = new ApolloServer({
  schema,
  context: (ctx) => {
    return ctx.req;
  }
})

export const config = {
  api: {
    bodyParser: false
  }
}

export default apolloServer.createHandler({ path: '/api/graphql' })

这是登录的突变

const jobs = require('../data/jobs');
const users = require('../data/users');

const getUserByEmail = email => users.find(user => user.email === email);

export const resolvers = {
  Query: {
    viewer (_parent, _args, _context, _info) {
      return { id: 1, name: 'John Smith', status: 'cached' }
    },
    jobs: (parent, args, context, info) => {
      return jobs;
    }
  },

  Mutation: {
    login: (parent, args, context, info) => {
      const { email } = args;
      const user = getUserByEmail(email);
      console.log(context.headers);
      if (user) {
        context.req = {
          ...context.req,
          session: user
        }
        return user;
      }
    }
  }
}

目前还没有加密,我只想暂时启动并运行它。我将如何设置会话(以便 cookie 出现在 Chrome 中并可用于将来的传入请求)

更新:Apollo 客户端配置

import React from 'react'
import Head from 'next/head'
import { ApolloProvider } from '@apollo/react-hooks'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'

let apolloClient = null

/**
 * Creates and provides the apolloContext
 * to a next.js PageTree. Use it by wrapping
 * your PageComponent via HOC pattern.
 * @param {Function|Class} PageComponent
 * @param {Object} [config]
 * @param {Boolean} [config.ssr=true]
 */
export function withApollo (PageComponent, { ssr = true } = {}) {
  const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
    const client = apolloClient || initApolloClient(apolloState)
    return (
      <ApolloProvider client={client}>
        <PageComponent {...pageProps} />
      </ApolloProvider>
    )
  }

  // Set the correct displayName in development
  if (process.env.NODE_ENV !== 'production') {
    const displayName =
      PageComponent.displayName || PageComponent.name || 'Component'

    if (displayName === 'App') {
      console.warn('This withApollo HOC only works with PageComponents.')
    }

    WithApollo.displayName = `withApollo(${displayName})`
  }

  if (ssr || PageComponent.getInitialProps) {
    WithApollo.getInitialProps = async ctx => {
      const { AppTree } = ctx

      // Initialize ApolloClient, add it to the ctx object so
      // we can use it in `PageComponent.getInitialProp`.
      const apolloClient = (ctx.apolloClient = initApolloClient())

      // Run wrapped getInitialProps methods
      let pageProps = {}
      if (PageComponent.getInitialProps) {
        pageProps = await PageComponent.getInitialProps(ctx)
      }

      // Only on the server:
      if (typeof window === 'undefined') {
        // When redirecting, the response is finished.
        // No point in continuing to render
        if (ctx.res && ctx.res.finished) {
          return pageProps
        }

        // Only if ssr is enabled
        if (ssr) {
          try {
            // Run all GraphQL queries
            const { getDataFromTree } = await import('@apollo/react-ssr')
            await getDataFromTree(
              <AppTree
                pageProps={{
                  ...pageProps,
                  apolloClient
                }}
              />
            )
          } catch (error) {
            // Prevent Apollo Client GraphQL errors from crashing SSR.
            // Handle them in components via the data.error prop:
            // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
            console.error('Error while running `getDataFromTree`', error)
          }

          // getDataFromTree does not call componentWillUnmount
          // head side effect therefore need to be cleared manually
          Head.rewind()
        }
      }

      // Extract query data from the Apollo store
      const apolloState = apolloClient.cache.extract()

      return {
        ...pageProps,
        apolloState
      }
    }
  }

  return WithApollo
}

/**
 * Always creates a new apollo client on the server
 * Creates or reuses apollo client in the browser.
 * @param  {Object} initialState
 */
function initApolloClient (initialState) {
  // Make sure to create a new client for every server-side request so that data
  // isn't shared between connections (which would be bad)
  if (typeof window === 'undefined') {
    return createApolloClient(initialState)
  }

  // Reuse client on the client-side
  if (!apolloClient) {
    apolloClient = createApolloClient(initialState)
  }

  return apolloClient
}

/**
 * Creates and configures the ApolloClient
 * @param  {Object} [initialState={}]
 */
function createApolloClient (initialState = {}) {
  const ssrMode = typeof window === 'undefined'
  const cache = new InMemoryCache().restore(initialState)

  // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
  return new ApolloClient({
    ssrMode,
    link: createIsomorphLink(),
    cache
  })
}

function createIsomorphLink () {
  if (typeof window === 'undefined') {
    const { SchemaLink } = require('apollo-link-schema')
    const { schema } = require('./schema')
    return new SchemaLink({ schema })
  } else {
    const { HttpLink } = require('apollo-link-http')
    return new HttpLink({
      uri: '/apii/graphql',
      credentials: 'same-origin'
    })
  }
}

更新:使用 apollo micro 的 Apollo-server 配置

import { ApolloServer } from 'apollo-server-micro'
import { schema } from '../../apollo/schema'

export const apolloServer = new ApolloServer({
  schema,
  context: (ctx) => {
    return ctx.req;
  }
})

export const config = {
  api: {
    bodyParser: false
  }
}

export default apolloServer.createHandler({ path: '/api/graphql' })

【问题讨论】:

  • 请编辑您的问题以显示您如何初始化和使用 apollo 客户端。在配置 HttpLink 时,您需要确保实际发送 cookie。
  • @DanielRearden 我已经更新了 apollo-client 的代码。如何在登录突变中设置 cookie?

标签: node.js graphql apollo-client apollo-server


【解决方案1】:

这是本文https://www.rockyourcode.com/nextjs-with-apollo-ssr-cookies-and-typescript/的解决方案

import {
  ApolloClient,
  InMemoryCache,
  NormalizedCacheObject,
} from "@apollo/client";
import { createUploadLink } from "apollo-upload-client";
import merge from "deepmerge";
import { IncomingHttpHeaders } from "http";
import fetch from "isomorphic-unfetch";
import isEqual from "lodash/isEqual";
import { AppProps } from "next/app";
import { useMemo } from "react";

const APOLLO_STATE_PROP_NAME = "__APOLLO_STATE__";

let apolloClient: ApolloClient<NormalizedCacheObject> | undefined;

const createApolloClient = (headers: IncomingHttpHeaders | null = null) => {
  const enhancedFetch = (url: RequestInfo, init: RequestInit) => {
    return fetch(url, {
      ...init,
      headers: {
        ...init.headers,
        "Access-Control-Allow-Origin": "*", // TODO: Check this out
        Cookie: headers?.cookie ?? "",
      },
    }).then((response) => response);
  };

  return new ApolloClient({
    ssrMode: typeof window === "undefined",
    link: createUploadLink({
      uri: "http://localhost:4000/graphql",
      // Make sure that CORS and cookies work
      fetchOptions: {
        mode: "cors",
      },
      credentials: "include",
      fetch: enhancedFetch,
    }),
    cache: new InMemoryCache(),
  });
};

type InitialState = NormalizedCacheObject | undefined;

interface IInitializeApollo {
  headers?: IncomingHttpHeaders | null;
  initialState?: InitialState | null;
}

export const initializeApollo = (
  { headers, initialState }: IInitializeApollo = {
    headers: null,
    initialState: null,
  },
) => {
  const _apolloClient = apolloClient ?? createApolloClient(headers);

  if (initialState) {
    const existingCache = _apolloClient.extract();

    const data = merge(initialState, existingCache, {
      arrayMerge: (destinationArray, sourceArray) => [
        ...sourceArray,
        ...destinationArray.filter((d) =>
          sourceArray.every((s) => !isEqual(d, s)),
        ),
      ],
    });

    _apolloClient.cache.restore(data);
  }

  if (typeof window === "undefined") return _apolloClient;

  if (!apolloClient) apolloClient = _apolloClient;
  return _apolloClient;
};

export const addApolloState = (
  client: ApolloClient<NormalizedCacheObject>,
  pageProps: AppProps["pageProps"],
) => {
  if (pageProps?.props) {
    pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract();
  }

  return pageProps;
};

export function useApollo(pageProps: AppProps["pageProps"]) {
  const state = pageProps[APOLLO_STATE_PROP_NAME];
  const store = useMemo(
    () => initializeApollo({ initialState: state }),
    [state],
  );
  return store;
}

【讨论】:

    【解决方案2】:

    如果您使用的是 SchemaLink,我认为您可以从 PageContext 获取 req 对象并使用它来重构您的 GraphQL 上下文对象。

    const { AppTree, req } = ctx
    // elsewhere
    new SchemaLink({ schema, context: { req } })
    

    这是一个不使用 SchemaLink 的示例,您可以将其应用于您的项目。关键是从PageContext 中提取cookie 值。然后,您可以使用它来创建填充 Cookie 标头的自定义 fetch 实现,然后将此 fetch 函数传递给您的 HttpLink 构造函数。

    let apolloClient: ApolloClient<any>
    
    export const getClient = (
      initialState: NormalizedCacheObject = {},
      cookie?: string,
    ): ApolloClient<any> => {
      const fetch: WindowOrWorkerGlobalScope['fetch'] = async (url, init = {}) => {
        const headers = { ...init.headers } as Record<string, string>
        if (cookie) {
          headers.Cookie = cookie
        }
    
        const response = await unfetch(url, {
          ...init,
          headers,
        })
        return response
      }
    
      return new ApolloClient<any>({
        ssrMode: typeof window === 'undefined',
        link: new HttpLink({
          uri: 'http://localhost:3000/graphql',
          credentials: 'same-origin',
          fetch,
        }),
        cache: new InMemoryCache().restore(initialState),
      })
    }
    
    export const initApollo = (
      initialState?: NormalizedCacheObject,
      cookie?: string,
    ): ApolloClient<any> => {
      if (typeof window === 'undefined') {
        return getClient(initialState, cookie)
      }
      if (!apolloClient) {
        apolloClient = getClient(initialState, cookie)
      }
    
      return apolloClient
    }
    
    export const withApollo = (PageComponent: NextPage, { ssr = true } = {}) => {
      const WithApollo = ({ apolloState, ...pageProps }: WithApolloProps) => {
        const client = apolloClient || initApollo(apolloState)
    
        return (
          <ApolloProvider client={client} >
              <PageComponent {...pageProps} />
          </ApolloProvider>
        )
      }
    
      if (ssr || PageComponent.getInitialProps) {
        WithApollo.getInitialProps = async (ctx: PageContext) => {
          const { AppTree } = ctx
          const cookie = (ctx.req && ctx.req.headers.cookie) || undefined
          const apolloClient = (ctx.apolloClient = initApollo({}, cookie))
    
          let pageProps = {}
          if (PageComponent.getInitialProps) {
            pageProps = await PageComponent.getInitialProps(ctx)
          }
    
          if (typeof window === 'undefined') {
            if (ctx.res && ctx.res.finished) {
              return pageProps
            }
    
            if (ssr) {
              try {
                const { getDataFromTree } = await import('@apollo/react-ssr')
                await getDataFromTree(
                    <AppTree
                        pageProps={{
                          ...pageProps,
                          apolloClient,
                        }}
                    />,
                )
              } catch (error) {
                console.error('Error while running `getDataFromTree`', error)
              }
    
              Head.rewind()
            }
          }
    
          const apolloState = apolloClient.cache.extract()
    
          return {
            ...pageProps,
            apolloState,
          }
        }
      }
    
      return WithApollo
    }
    

    顺便说一句,您要确保在初始化 Apollo Server 时正确设置了上下文。不要只返回 req 对象,因为这会将您的上下文设置为 req 对象。而是这样做:

    context: ({ req }) => {
      return { req };
    }
    

    然后确保您在解析器中正确地改变了会话:

    context.req.session.user = user
    

    按原样,您的代码正在使用用户覆盖会话对象,这不是您想要做的。在会话上设置用户属性意味着您可以在代码的其他位置使用context.req.session.user 访问它。

    【讨论】:

    • 好的,但是我将如何在会话中设置用户?在登录突变中,req.session 未定义。
    • req.session 应该总是被定义——这就是会话中间件的作用。如果您没有看到它,请检查中间件的顺序。应该在应用任何 Apollo Server 中间件之前应用它
    • 我已经用我的 apollo-server 配置更新了这个问题。看起来对吗?
    猜你喜欢
    • 1970-01-01
    • 2023-02-25
    • 1970-01-01
    • 2023-01-16
    • 2020-03-29
    • 1970-01-01
    • 2019-02-09
    • 2021-07-30
    • 2020-09-24
    相关资源
    最近更新 更多