useMutation之后useQuery无法正确更新并设置cookie以进行授权

时间:2020-04-04 19:44:36

标签: javascript reactjs typescript graphql apollo

我一直在使用Cookie和apollo-client开发具有简单授权的项目。面临的挑战是,有时当我尝试useQUery(isAuthenticatedQuery)时,他们会检索正确的数据,而有时却无法。此查询用于检查我的用户是否已登录,我在请求标头中发送了LoginMutation之后返回的令牌。我已经在“网络”标签中检查了我的请求,当出现错误时,标题发送的是“ bearer undefined”,而不是“ bearer $ {token}”。

这是我的第一个使用apollo的应用程序,所以这可能是一个虚拟问题,我以为异步请求存在一些问题,但useQuery中的所有请求都已经异步了,对

login.tsx

import React, { useState } from 'react'
import Layout from '../components/Layout'
import Router from 'next/router'
import { withApollo } from '../apollo/client'
import gql from 'graphql-tag'
import { useMutation, useQuery, useApolloClient } from '@apollo/react-hooks'


const LoginMutation = gql`
  mutation LoginMutation($email: String!, $password: String!) {
    login(email: $email, password: $password) {
      token
    }
  }
`


function Login(props) {
  const client = useApolloClient()
  const [password, setPassword] = useState('')
  const [email, setEmail] = useState('')

  const [login] = useMutation(LoginMutation, {
    onCompleted(data) {
      document.cookie = `token=${data.login.token}; path=/`
    }
  })

  return (
    <Layout>
      <div>
        <form
          onSubmit={async e => {
            e.preventDefault();

            await login({
              variables: {
                email: email,
                password: password,
              }
            })

            Router.push('/')
          }}>
          <h1>Login user</h1>
          <input
            autoFocus
            onChange={e => setEmail(e.target.value)}
            placeholder="Email"
            type="text"
            value={email}
          />
          <input
            onChange={e => setPassword(e.target.value)}
            placeholder="Password"
            type="password"
            value={password}
          />
          <input disabled={!password || !email} type="submit" value="Login" />
          <a className="back" href="#" onClick={() => Router.push('/')}>
            or Cancel
          </a>
        </form>
      </div>
    </Layout>
  )
}

export default withApollo(Login)

index.tsx

import { useEffect } from 'react'
import Layout from '../components/Layout'
import Link from 'next/link'
import { withApollo } from '../apollo/client'
import { useQuery } from '@apollo/react-hooks'
import { FeedQuery, isAuthenticatedQuery } from '../queries';


export interface Item {
  content: string
  author: string
  title: string
  name: string
}

export interface Post {
  post: {
    [key: string]: Item
  }
}
const Post = ({ post }: Post) => (
  <Link href="/p/[id]" as={`/p/${post.id}`}>
    <a>
      <h2>{post.title}</h2>
      <small>By {post.author.name}</small>
      <p>{post.content}</p>
      <style jsx>{`
        a {
          text-decoration: none;
          color: inherit;
          padding: 2rem;
          display: block;
        }
      `}</style>
    </a>
  </Link>
)

const Blog = () => {
  const { loading, error, data } = useQuery(FeedQuery)

  const { loading: loadingAuth, data: dataAuth, error: errorAuth } = useQuery(isAuthenticatedQuery)

  console.log("data auth", dataAuth, loadingAuth, errorAuth);


  if (loading) {
    return <div>Loading ...</div>
  }
  if (error) {
    return <div>Error: {error.message}</div>
  }

  return (
    <Layout>
      <div className="page">
        {!!dataAuth && !loadingAuth ? (
          <h1> Welcome back {dataAuth.me.name} </h1>
        ) : (
            <h1>My Blog</h1>
          )}
        <main>
          {data.feed.map(post => (
            <div className="post">
              <Post key={post.id} post={post} />
            </div>
          ))}
        </main>
      </div>
      <style jsx>{`

        h1 {
          text-transform: capitalize;
        }
        .post {
          background: white;
          transition: box-shadow 0.1s ease-in;
        }

        .post:hover {
          box-shadow: 1px 1px 3px #aaa;
        }

        .post + .post {
          margin-top: 2rem;
        }
      `}</style>
    </Layout>
  )
}

export default withApollo(Blog)

client.js(我的配置apollo hoc文件)

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'
import fetch from 'isomorphic-unfetch'
import cookies from 'next-cookies'

let apolloClient = null
let token = undefined
/**
 * 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
      token = cookies(ctx).token || ''
      // 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)

  return new ApolloClient({
    ssrMode,
    link: createIsomorphLink(),
    cache,
  })
}

function createIsomorphLink() {
  const { HttpLink } = require('apollo-link-http')
  return new HttpLink({
    headers: { Authorization: `Bearer ${token}` },
    uri: 'http://localhost:4000',
    credentials: 'same-origin',
  })
}

TLDR; 检查HttpLink内的client.js文件如何定义标头,以及index.tsx> Blog如何使用useQuery(isAuthenticatedQuery)检查用户是否已签名内。

obs .:如果刷新页面,则会始终设置令牌,并且查询将按预期进行。

1 个答案:

答案 0 :(得分:1)

首先,您没有在此处将令牌传递给apollo HTTP客户端。您可以看到令牌已解析为未定义。

function createIsomorphLink() {
  const { HttpLink } = require('apollo-link-http')
  return new HttpLink({
    uri: 'http://localhost:4000',
    credentials: 'same-origin',
  })
}

这是您应该做的

import { setContext } from 'apollo-link-context';
import localForage from 'localforage';

function createIsomorphLink() {
  const { HttpLink } = require('apollo-link-http')
  return new HttpLink({
    uri: 'http://localhost:4000',
    credentials: 'same-origin',
  })
}

const authLink = setContext((_, { headers }) => {
  // I recommend using localforage since it's ssr
  const token = localForage.getItem('token');
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    }
  }
});

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

  return new ApolloClient({
    ssrMode,
    link: authLink.concat(createIsomorphLink()),
    cache,
  })
}

现在在您的登录组件中

import localForage from 'localforage';

const LoginMutation = gql`
  mutation LoginMutation($email: String!, $password: String!) {
    login(email: $email, password: $password) {
      token
    }
  }
`


function Login(props) {
  const client = useApolloClient()
  const [password, setPassword] = useState('')
  const [email, setEmail] = useState('')

  const [login] = useMutation(LoginMutation, {
    onCompleted(data) {
      // document.cookie = `token=${data.login.token}; path=/`
      localForage. setItem('token', data.login.token)
    }
  })

  return (
    <Layout>
      <div>
        <form
          onSubmit={async e => {
            e.preventDefault();

            await login({
              variables: {
                email: email,
                password: password,
              }
            })

            Router.push('/')
          }}>
          <h1>Login user</h1>
          <input
            autoFocus
            onChange={e => setEmail(e.target.value)}
            placeholder="Email"
            type="text"
            value={email}
          />
          <input
            onChange={e => setPassword(e.target.value)}
            placeholder="Password"
            type="password"
            value={password}
          />
          <input disabled={!password || !email} type="submit" value="Login" />
          <a className="back" href="#" onClick={() => Router.push('/')}>
            or Cancel
          </a>
        </form>
      </div>
    </Layout>
  )
}

export default withApollo(Login)

只要您的身份验证策略是Bearer令牌,它就应该起作用。如果您使用的是Cookie或会话Cookie,并且您的前端和后端具有不同的域名,则只需传递带有凭证include的自定义抓取操作,否则将其保留为same-site并设为{{1} }在后端中启用,并且在cors选项中将开发中的本地主机列入白名单。