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() 方法从缓存中获取更新。没有看到错误