我一直在使用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 .:如果刷新页面,则会始终设置令牌,并且查询将按预期进行。
答案 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选项中将开发中的本地主机列入白名单。