跳到内容

d

登录与更新缓存

我们的应用的前端在更新了服务器后显示电话目录很好。然而,如果我们想添加新的人员,我们必须在前端添加登录功能。

User login

让我们在应用的状态中加入变量token。当一个用户被登录时,它将包含一个用户令牌。如果token是未定义的,我们将渲染负责用户登录的LoginForm组件。该组件接收一个错误处理程序和setToken函数作为参数。

const App = () => {
  const [token, setToken] = useState(null)
  // ...

  if (!token) {
    return (
      <div>
        <Notify errorMessage={errorMessage} />
        <h2>Login</h2>
        <LoginForm
          setToken={setToken}
          setError={notify}
        />
      </div>
    )
  }

  return (
    // ...
  )
}

接下来,我们定义一个用于登录的变体。

export const LOGIN = gql`
  mutation login($username: String!, $password: String!) {
    login(username: $username, password: $password)  {
      value
    }
  }
`

LoginForm组件的工作原理与我们之前创建的所有其他做改变的组件差不多。

代码中有趣的几行已被突出显示。

import { useState, useEffect } from 'react'
import { useMutation } from '@apollo/client'
import { LOGIN } from '../queries'

const LoginForm = ({ setError, setToken }) => {
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')

  const [ login, result ] = useMutation(LOGIN, {    onError: (error) => {
      setError(error.graphQLErrors[0].message)
    }
  })

  useEffect(() => {    if ( result.data ) {      const token = result.data.login.value      setToken(token)      localStorage.setItem('phonenumbers-user-token', token)    }  }, [result.data]) // eslint-disable-line
  const submit = async (event) => {
    event.preventDefault()

    login({ variables: { username, password } })
  }

  return (
    <div>
      <form onSubmit={submit}>
        <div>
          username <input
            value={username}
            onChange={({ target }) => setUsername(target.value)}
          />
        </div>
        <div>
          password <input
            type='password'
            value={password}
            onChange={({ target }) => setPassword(target.value)}
          />
        </div>
        <button type='submit'>login</button>
      </form>
    </div>
  )
}

export default LoginForm

我们使用一个效果钩子来保存令牌的值到App组件的状态和服务器响应改变后的本地存储。

为了避免无休止的渲染循环,使用效果钩子是必要的。

让我们也添加一个按钮,使登录的用户可以注销。这个按钮的onClick处理程序将token状态设置为null,从本地存储中删除token并重置Apollo客户端的缓存。最后一步是重要的,因为有些查询可能已经获取了数据到缓存中,而这些数据只有登录的用户才有机会访问。

我们可以使用Apollo client对象的resetStore方法重置缓存。

客户端可以通过useApolloClient钩子访问。

const App = () => {
  const [token, setToken] = useState(null)
  const [errorMessage, setErrorMessage] = useState(null)
  const result = useQuery(ALL_PERSONS)
  const client = useApolloClient()
  if (result.loading)  {
    return <div>loading...</div>
  }

  const logout = () => {    setToken(null)    localStorage.clear()    client.resetStore()  }
  if (!token) {    return (      <>        <Notify errorMessage={errorMessage} />        <LoginForm setToken={setToken} setError={notify} />      </>    )  }
  return (
    <>
      <Notify errorMessage={errorMessage} />
      <button onClick={logout}>logout</button>      <Persons persons={result.data.allPersons} />
      <PersonForm setError={notify} />
      <PhoneForm setError={notify} />
    </>
  )
}

该应用的当前代码可以在Github上找到,分支part8-6

Adding a token to a header

后端修改后,创建新的人需要在请求中发送一个有效的用户令牌。为了发送令牌,我们必须稍微改变一下index.js中定义ApolloClient对象的方式。

import { setContext } from '@apollo/client/link/context'
const authLink = setContext((_, { headers }) => {  const token = localStorage.getItem('phonenumbers-user-token')  return {    headers: {      ...headers,      authorization: token ? `bearer ${token}` : null,    }  }})
const httpLink = new HttpLink({ uri: 'http://localhost:4000' })
const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: authLink.concat(httpLink)})

给予client对象的链接参数定义了apollo连接到服务器的方式。这里,正常的httpLink连接被修改,以便请求的授权包含令牌,如果有一个已经被保存到localStorage。

创建新的人和改变号码又可以了。然而,还有一个问题。如果我们试图添加一个没有电话号码的人,这是不可能的。

fullstack content

验证失败,因为前端发送了一个空字符串作为phone的值。

让我们改变创建新人的函数,如果用户没有给出一个值,它就把phone设置为undefined

const PersonForm = ({ setError }) => {
  // ...
  const submit = async (event) => {
    event.preventDefault()
    createPerson({
      variables: {
        name, street, city,        phone: phone.length > 0 ? phone : undefined      }
    })

  // ...
  }

  // ...
}

目前的应用代码可以在Github上找到,分支part8-7

Updating cache, revisited

我们必须在创建新人时更新Apollo客户端的缓存。我们可以使用改变的refetchQueries选项来更新它,以定义 ALL_PERSONS query is done again.

const PersonForm = ({ setError }) => {
  // ...

  const [ createPerson ] = useMutation(CREATE_PERSON, {
    refetchQueries: [  {query: ALL_PERSONS} ],    onError: (error) => {
      setError(error.graphQLErrors[0].message)
    }
  })

这种方法相当不错,缺点是任何更新都要重新运行查询。

我们有可能通过自己处理更新缓存来优化解决方案。这可以通过为改变定义一个合适的update回调来实现,Apollo会在改变后运行。

const PersonForm = ({ setError }) => {
  // ...

  const [ createPerson ] = useMutation(CREATE_PERSON, {
    onError: (error) => {
      setError(error.graphQLErrors[0].message)
    },
    update: (cache, response) => {      cache.updateQuery({ query: ALL_PERSONS }, ({ allPersons }) => {        return {          allPersons: allPersons.concat(response.data.addPerson),        }      })    },  })

  // ..
}

该回调函数被赋予一个对缓存的引用和改变返回的数据作为参数。例如,在我们的例子中,这将是创建的人。

使用函数updateQuery,代码更新了

查询ALL_PERSONS在缓存中,将新的人加入到缓存数据中。

在某些情况下,保持缓存更新的唯一合理方式是使用update回调。

必要时,可以通过将管理缓存使用的字段fetchPolicy设置为no-cache,来禁用整个应用或单个查询的缓存。

勤于使用缓存。缓存中的旧数据会导致难以发现的bug。正如我们所知,保持缓存的更新是非常具有挑战性的。根据一个编码员的谚语。

计算机科学中只有两件难事:缓存失效和命名事物。阅读更多内容这里

目前的应用代码可以在Github上找到,分支part8-8