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

Posted

技术标签:

【中文标题】useMutation 后 useQuery 未正确更新并设置 cookie 以进行授权【英文标题】:useQuery not updating properly after useMutation and set cookies for authorization 【发布时间】:2020-07-16 22:02:51 【问题描述】:

我一直在开发一个使用 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.s-s-r=true]
 */
export function withApollo(PageComponent,  s-s-r = 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 (s-s-r || 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 s-s-r is enabled
        if (s-s-r) 
          try 
            // Run all GraphQL queries
            const  getDataFromTree  = await import('@apollo/react-s-s-r')
            await getDataFromTree(
              <AppTree
                pageProps=
                  ...pageProps,
                  apolloClient,
                
              />,
            )
           catch (error) 
            // Prevent Apollo Client GraphQL errors from crashing s-s-r.
            // 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 s-s-rMode = typeof window === 'undefined'
  const cache = new InMemoryCache().restore(initialState)

  return new ApolloClient(
    s-s-rMode,
    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 > 博客我如何使用 useQuery(isAuthenticatedQuery) 检查用户是否已登录。

obs.:如果我刷新页面,则始终设置令牌并且查询按预期工作。

【问题讨论】:

token 未在链接中读取(来自document.cookie)...等待查询? ...只需将Router.push 移动到onCompleted 我将 Router.push('/') 移到 onCompleted 内部,但没有成功 "token 未在链接中读取(来自 document.cookie)" 在我签入开发工具时设置了 cookie,问题是我正在使用 s-s-r(nextjs),并且只有在刷新页面时才会读取 cookie。在将cookie设置为读取之后,我需要找到一种方法,然后在client.js(我的s-s-r配置文件)中 cookie 设置好了,好吧……后面的链接里不用了 【参考方案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 s-s-r
  const token = localForage.getItem('token');
  return 
    headers: 
      ...headers,
      authorization: token ? `Bearer $token` : "",
    
  
);

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

  return new ApolloClient(
    s-s-rMode,
    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 并在后端启用 cors并且您的本地主机(如果正在开发中)在 cors 选项中被列入白名单。

【讨论】:

感谢您的回答,但我在尝试注销时遇到了同样的问题,刷新页面后我只能看到更新的数据。注销的行为会向我发送一个查询并检查我是否得到响应。在此查询中,有我的身份验证标头。 IDK 为什么,但查询仍然像使用 cookie 一样保存本地存储值。

以上是关于useMutation 后 useQuery 未正确更新并设置 cookie 以进行授权的主要内容,如果未能解决你的问题,请参考以下文章

如何在 GraphQL 中正确链接 useQuery 和 useMutation?

以与 `useMutation` 失败相同的方式使用 `useQuery` 钩子

Apollo 客户端 useQuery 在 useMutation 后不更新数据

Apollo useQuery 钩子不会使用 writeQuery() 方法从缓存中获取更新。没有看到错误

如何在没有 useMutation 的情况下在 Next.js 中使用 GraphQL 突变

如何在 x 时间后重新渲染反应(突变阿波罗)