阅读 318

翻译 | 《JavaScript Everywhere》第15章 Web身份验证和状态

翻译 | 《JavaScript Everywhere》第15章 Web身份验证和状态

写在最前面

大家好呀,我是毛小悠,是一位前端开发工程师。正在翻译一本英文技术书籍。

为了提高大家的阅读体验,对语句的结构和内容略有调整。如果发现本文中有存在瑕疵的地方,或者你有任何意见或者建议,可以在评论区留言,或者加我的微信:code_maomao,欢迎相互沟通交流学习。

(σ゚∀゚)σ..:*☆哎哟不错哦

第15章 Web身份验证和状态

最近我和我的家人搬家了。填写并签署了几种表格(我的手仍然很累)后,我们就用钥匙进了前门。每次我们回到家,我们都可以使用这些钥匙来解锁并进入门。我很高兴我每次回家都不需要填写表格,但也感谢拥有一把锁,这样我们就不会有任何不速之客到来了。

客户端Web身份验证的工作方式几乎与这相同。

我们的用户将填写表格,并以密码和存储在他们的浏览器中的令牌的形式交给网站。当他们返回站点时,他们将使用令牌自动进行身份验证,或者能够使用其密码重新登录。

在本章中,我们将使用GraphQL API构建一个Web身份验证系统。

为此,我们将构建表单,将JWT存储在浏览器中,随每个请求发送令牌,并跟踪应用程序的状态。

创建注册表格

开始使用我们的应用程序的客户端身份验证,我们可以创建一个用户注册React组件。在这样做之前,让我们先确定组件的工作方式。

首先,用户将导航到我们应用程序中的/signup路由。在此页面上,他们将看到一个表单,可以在其中输入电子邮件地址、用户名和密码。提交表单将执行我们APIsignUp请求 。如果请求成功,将创建一个新的用户帐户,API将返回一个JWT。如果有错误,我们可以通知用户。我们将显示一条通用错误消息,但我们可以更新API以返回特定的错误消息,例如预先存在的用户名或重复的电子邮件地址。

让我们开始创建新路由。首先,我们将在src/pages/signup.js创建一个新的React组件 。

import React, { useEffect } from 'react';

// include the props passed to the component for later use
const SignUp = props => {
  useEffect(() => {
    // update the document title
    document.title = 'Sign Up — Notedly';
  });

  return (
    <div> <p>Sign Up</p> </div>
  );
};

export default SignUp; 
复制代码

现在,我们将在src/pages/index.js中更新路由列表,包括注册路由:

// import the signup route
import SignUp from './signup';

// within the Pages component add the route
<Route path="/signup" component={SignUp} /> 
复制代码

通过添加路由,我们将能够导航到 http:// localhost:1234/signup 来查看(大部分为空)注册页面。现在,让我们为表单添加标记:

import React, { useEffect } from 'react';

const SignUp = props => {
  useEffect(() => {
    // update the document title
    document.title = 'Sign Up — Notedly';
  });

  return (
    <div>
      <form>
        <label htmlFor="username">Username:</label>
        <input required type="text" id="username" name="username" placeholder="username" />
        <label htmlFor="email">Email:</label>
        <input required type="email" id="email" name="email" placeholder="Email" />
        <label htmlFor="password">Password:</label>
        <input required type="password" id="password" name="password" placeholder="Password" />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

export default SignUp; 
复制代码

htmlFor

如果你只是在学习React,那么常见的陷阱之一就是与HTML对应的JSX属性的不同。在这种情况下,我们使用JSX htmlFor 代替HTMLfor 属性来避免任何JavaScript冲突。你可以在以下页面中看到这些属性的完整列表(虽然简短)

React DOM Elements文档。

现在,我们可以通过导入Button 组件并将样式设置为样式化组件来添加某种样式 :

import React, { useEffect } from 'react';
import styled from 'styled-components';

import Button from '../components/Button';

const Wrapper = styled.div` border: 1px solid #f5f4f0;
  max-width: 500px;
  padding: 1em;
  margin: 0 auto; `;

const Form = styled.form` label,
  input { display: block;
    line-height: 2em;
  }

  input { width: 100%;
    margin-bottom: 1em;
  } `;

const SignUp = props => {
  useEffect(() => {
    // update the document title
    document.title = 'Sign Up — Notedly';
  });

  return (
    <Wrapper>
      <h2>Sign Up</h2>
      <Form>
        <label htmlFor="username">Username:</label>
        <input
          required
          type="text"
          id="username"
          name="username"
          placeholder="username"
        />
        <label htmlFor="email">Email:</label>
        <input
          required
          type="email"
          id="email"
          name="email"
          placeholder="Email"
        />
        <label htmlFor="password">Password:</label>
        <input
          required
          type="password"
          id="password"
          name="password"
          placeholder="Password"
        />
        <Button type="submit">Submit</Button>
      </Form>
    </Wrapper>
  );
};

export default SignUp; 
复制代码

React表单和状态

在应用程序中会有事情的改变。数据输入到表单中,用户将点击按钮,发送消息。在React中,我们可以通过分配state来在组件级别跟踪这些请求。在我们的表单中,我们需要跟踪每个表单元素的状态,以便在后面可以提交它。

React Hooks

在本书中,我们将使用功能组件和React的较新Hooks API。如果你使用了其他使用React的类组件的学习资源 ,则可能看起来有些不同。你可以在React文档中阅读有关钩子的更多信息。

要开始使用状态,我们首先将src/pages/signup.js文件顶部的React导入更新为useState

import React, { useEffect, useState } from 'react'; 
复制代码

接下来,在我们的 SignUp 组件中,我们将设置默认表单值状态:

const SignUp = props => {
  // set the default state of the form
  const [values, setValues] = useState();

// rest of component goes here
}; 
复制代码

现在,我们将更新组件在输入表单字段时更改状态,并在用户提交表单时执行操作。首先,我们将创建一个onChange 函数,该函数将在更新表单时更新组件的状态。

当用户做了改变后,通过调用这个函数的onChange属性来更新每个表单元素的标记。

然后,我们在onSubmit 处理程序更新表单元素。现在,我们仅将表单数据输出到控制台。

在/src/pages/sigunp.js

const SignUp = () => {
  // set the default state of the form
  const [values, setValues] = useState();

  // update the state when a user types in the form
  const onChange = event => {
    setValues({
      ...values,
      [event.target.name]: event.target.value
    });
  };

  useEffect(() => {
    // update the document title
    document.title = 'Sign Up — Notedly';
  });

  return (
    <Wrapper>
      <h2>Sign Up</h2>
      <Form onSubmit={event => {
          event.preventDefault();
          console.log(values);
        }}
        >
        <label htmlFor="username">Username:</label>
        <input required type="text" name="username" placeholder="username" onChange={onChange} />
        <label htmlFor="email">Email:</label>
        <input required type="email" name="email" placeholder="Email" onChange={onChange} />
        <label htmlFor="password">Password:</label>
        <input required type="password" name="password" placeholder="Password" onChange={onChange} />
        <Button type="submit">Submit</Button>
      </Form>
    </Wrapper>
  );
}; 
复制代码

使用此表单标记后,我们就可以请求具有GraphQL修改的数据了。

修改****注册

要注册用户,我们将使用APIsignUp 请求。如果注册成功,此请求将接受电子邮件、用户名和密码作为变量,并返回JWT。让我们写出我们的请求并将其集成到我们的注册表单中。

首先,我们需要导入我们的Apollo库。我们将利用useMutation useApolloClient 挂钩以及 Apollo Clientgql语法。

src/pages/signUp中,在其他库import语句旁边添加以下内容:

import { useMutation, useApolloClient, gql } from '@apollo/client';
复制代码

现在编写GraphQL修改,如下所示:

const SIGNUP_USER = gql`
  mutation signUp($email: String!, $username: String!, $password: String!) {
    signUp(email: $email, username: $username, password: $password)
  }
`; 
复制代码

编写了请求后,我们可以更新React组件标记以在用户提交表单时将表单元素作为变量传递来执行修改。现在,我们将响应(如果成功,应该是JWT)输出到控制台:

const SignUp = props => {
  // useState, onChange, and useEffect all remain the same here

  //add the mutation hook
  const [signUp, { loading, error }] = useMutation(SIGNUP_USER, {
   onCompleted: data => {
     // console.log the JSON Web Token when the mutation is complete
     console.log(data.signUp);
   }
 });

  // render our form
  return (
    <Wrapper> <h2>Sign Up</h2> {/* pass the form data to the mutation when a user submits the form */} <Form onSubmit={event => {
          event.preventDefault();
          signUp({
            variables: {
              ...values
            }
          });
        }}
      >
        {/* ... the rest of the form remains unchanged ... */} </Form> </Wrapper>
  );
}; 
复制代码

现在,如果你完成并提交表单,你应该会看到一个JWT输出到控制台(图15-1)。

另外,如果你在GraphQLPlaygroundhttp:// localhost4000/api)中执行用户查询,你将看到新帐户(图15-2)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6crer0sJ-1606432851137)(vipkshttp0.wiz.cn/ks/share/re…

15-1。如果成功,当我们提交表单时,JSON Web令牌将打印到我们的控制台

15-2。我们还可以通过在GraphQL Playground中执行用户查询来查看用户列表

设置好修改并返回期望的数据后,接下来我们要存储收到的响应。

JSON Web令牌和本地存储

成功完成我们的 signUp请求后,它会返回JSON Web令牌(JWT)。你可能会从本书的API部分回忆起JWT 允许我们在用户设备上安全存储用户ID。为了在用户的Web浏览器中实现此目的,我们将令牌存储在浏览器的 localStorage中。 localStorage 是一个简单的键值存储,可在浏览器会话之间保留,直到更新或清除该存储为止。让我们更新请求以将令牌存储在 localStorage中。

src/pages/signup.js ,更新 useMutation 钩子以将令牌存储在本地存储中 ( 见图15-3):

const [signUp, { loading, error }] = useMutation(SIGNUP_USER, {
  onCompleted: data => {
    // store the JWT in localStorage
    localStorage.setItem('token', data.signUp);
  }
}); 
复制代码

15-3。我们的Web令牌现在存储在浏览器的localStorage

JWT和安全性

当令牌存储在 localStorage中时,可以在页面上运行的任何JavaScript都可以访问该令牌,然后容易受到跨站点脚本(XSS)攻击。因此,在使用 localStorage 存储令牌凭证时,需要格外小心以限制(或避免)CDN托管脚本。如果第三方脚本被盗用,它将有权访问JWT

随着我们的JWT存储在本地,我们准备在GraphQL请求和查询中使用它。

重导向

当前,当用户完成注册表单时,该表单会重新呈现为空白表单。这不会给用户很多视觉提示,表明他们的帐户注册成功。相反,我们可以将用户重定向到应用程序的主页。另一种选择是创建一个“成功”页面,该页面感谢用户注册并将其注册到应用程序中。

你可能会在本章前面接触到,我们可以将属性传递到组件中。我们可以使用React Router的历史记录重定向路由,这将通过props.history.push 实现。为了实现这一点,我们将更新我们的修改的 onCompleted 事件,包括如下所示的重定向:

const [signUp, { loading, error }] = useMutation(SIGNUP_USER, {
    onCompleted: data => {
      // store the token
      localStorage.setItem('token', data.signUp);
      // redirect the user to the homepage
      props.history.push('/');
    }
  }); 
复制代码

进行此更改后,现在用户在注册帐户后将被重定向到我们应用程序的主页。

发送附加标头的请求

尽管我们将令牌存储在 localStorage中,但我们的API尚未访问它。这意味着即使用户创建了帐户,API也无法识别该用户。如果你回想我们的API开发,每个API调用都会在请求的标头中收到一个令牌。我们将修改客户端以将JWT作为每个请求的标头发送。

src/App.js中, 我们将更新依赖项,包括来自Apollo ClientcreateHttpLink以及来自ApolloLink Context包的setContext 。然后,我们将更新Apollo的配置,以在每个请求的标头中发送令牌:

// import the Apollo dependencies
import {
  ApolloClient,
  ApolloProvider,
  createHttpLink,
  InMemoryCache
} from '@apollo/client';
import { setContext } from 'apollo-link-context';

// configure our API URI & cache
const uri = process.env.API_URI;
const httpLink = createHttpLink({ uri });
const cache = new InMemoryCache();

// check for a token and return the headers to the context
const authLink = setContext((_, { headers }) => {
  return {
    headers: {
      ...headers,
      authorization: localStorage.getItem('token') || ''
    }
  };
});

// create the Apollo client
const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache,
  resolvers: {},
  connectToDevTools: true
}); 
复制代码

进行此更改后,我们现在可以将已登录用户的信息传递给我们的API

本地状态管理

我们已经研究了如何在组件中管理状态,但是整个应用程序呢?有时在许多组件之间共享一些信息很有用。我们可以在整个应用程序中从基本组件传递组件,但是一旦我们经过几个子组件级别,就会变得混乱。一些库如Redux MobX 试图解决状态管理的挑战,并已证明对许多开发人员和团队非常有用。

在我们的案例中,我们已经在使用Apollo客户端库,该库包括使用GraphQL查询进行本地状态管理的功能。让我们实现一个本地状态属性,该属性将存储用户是否已登录,而不是引入另一个依赖关系。

Apollo React库将ApolloClient 实例放入 React的上下文中,但有时我们可能需要直接访问它。我们可以通过useApolloClient挂钩,这将使我们能够执行诸如直接更新或重置缓存存储区或写入本地数据之类的操作。

当前,我们有两种方法来确定用户是否登录到我们的应用程序。首先,如果他们成功提交了注册表单,我们知道他们是当前用户。其次,我们知道,如果访问者使用存储在localStorage中的令牌访问该站点 ,那么他们已经登录。让我们从用户填写注册表单时添加到我们的状态开始。

为此,我们将使用client.writeDatauseApolloClient挂钩直接将其写入Apollo客户的本地仓库。

src/pages/signup.js中,我们首先需要更新 @apollo/client 库导入以包含 useApolloClient

import { useMutation, useApolloClient } from '@apollo/client'; 
复制代码

src/pages/signup.js中, 我们将调用 useApolloClient 函数,并在完成后使用writeData更新该修改以添加到本地存储中 :

// Apollo Client
const client = useApolloClient();
// Mutation Hook
const [signUp, { loading, error }] = useMutation(SIGNUP_USER, {
  onCompleted: data => {
    // store the token
    localStorage.setItem('token', data.signUp);
    // update the local cache
    client.writeData({ data: { isLoggedIn: true } });
    // redirect the user to the homepage
    props.history.push('/');
  }
}); 
复制代码

现在,让我们更新应用程序,以在页面加载时检查预先存在的令牌,并在找到令牌时更新状态。在 src/App.js ,首先将ApolloClient 配置更新为一个空的 resolvers 对象。这将使我们能够在本地缓存上执行GraphQL查询。

// create the Apollo client
const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache,
  resolvers: {},
  connectToDevTools: true
}); 
复制代码

接下来,我们可以对应用程序的初始页面加载执行检查:

// check for a local token
const data = {
  isLoggedIn: !!localStorage.getItem('token')
};

// write the cache data on initial load
cache.writeData({ data }); 
复制代码

这里很酷:我们现在可以使用@client 指令在应用程序中的任何位置以GraphQL查询形式访问 isLoggedIn

为了证明这一点,让我们更新我们的应用程序,

如果isLoggedInfalse,显示“注册”和“登录”链接。

如果 isLoggedIntrue 就显示“注销”链接。

src/components/Header.js ,导入必要的依赖项并像下面这样编写查询:

// new dependencies
import { useQuery, gql } from '@apollo/client';
import { Link } from 'react-router-dom';

// local query
const IS_LOGGED_IN = gql`
  {
    isLoggedIn @client
  }
`; 
复制代码

现在,在我们的React组件中,我们可以包括一个简单的查询来检索状态,以及一个三级运算符,该运算符显示注销或登录的选项:

const UserState = styled.div`
  margin-left: auto;
`;

const Header = props => {
  // query hook for user logged in state
  const { data } = useQuery(IS_LOGGED_IN);

  return (
    <HeaderBar> <img src={logo} alt="Notedly Logo" height="40" /> <LogoText>Notedly</LogoText> {/* If logged in display a logout link, else display sign-in options */} <UserState> {data.isLoggedIn ? ( <p>Log Out</p> ) : ( <p> <Link to={'/signin'}>Sign In</Link> or{' '} <Link to={'/signup'}>Sign Up</Link> </p> )} </UserState> </HeaderBar> );
}; 
复制代码

这样,当用户登录时,他们将看到“注销”选项。否则,将由于本地状态来为他们提供用于登录或注册的选项。我们也不限于简单的布尔逻辑。Apollo使我们能够编写本地解析器和类型定义,从而使我们能够利用GraphQL在本地状态下必须提供的一切。

注销

目前,一旦用户登录,他们将无法退出我们的应用程序。让我们将标题中的“注销”变成一个按钮,单击该按钮将注销用户。为此,当单击按钮时,我们将删除存储在localStorage中的令牌 。我们将使用一个元素来实现其内置的可访问性,因为当用户使用键盘导航应用程序时,它既充当用户动作的语义表示,又可以获得焦点(如链接)。

在编写代码之前,让我们编写一个样式化的组件,该组件将呈现一个类似于链接的按钮。在src/Components/ButtonAsLink.js中创建一个新文件,并添加以下内容:

import styled from 'styled-components';

const ButtonAsLink = styled.button`
  background: none;
  color: #0077cc;
  border: none;
  padding: 0;
  font: inherit;
  text-decoration: underline;
  cursor: pointer;

  :hover,
  :active {
    color: #004499;
  }
`;

export default ButtonAsLink; 
复制代码

现在在 src/components/Header.js, 我们可以实现我们的注销功能。我们需要使用React RouterwithRouter高阶组件来处理重定向,因为Header.js文件是UI组件,而不是已定义的路由。首先导入 ButtonAsLink 组件以及 withRouter

// import both Link and withRouter from React Router
import { Link, withRouter } from 'react-router-dom';
// import the ButtonAsLink component
import ButtonAsLink from './ButtonAsLink'; 
复制代码

现在,在我们的JSX中,我们将更新组件以包括 props 参数,并将注销标记更新为一个按钮:

const Header = props => {
  // query hook for user logged-in state,
  // including the client for referencing the Apollo store
  const { data, client } = useQuery(IS_LOGGED_IN);

  return (
    <HeaderBar> <img src={logo} alt="Notedly Logo" height="40" /> <LogoText>Notedly</LogoText> {/* If logged in display a logout link, else display sign-in options */} <UserState> {data.isLoggedIn ? ( <ButtonAsLink> Logout </ButtonAsLink> ) : ( <p> <Link to={'/signin'}>Sign In</Link> or{' '} <Link to={'/signup'}>Sign Up</Link> </p> )} </UserState> </HeaderBar> );
};

// we wrap our component in the withRouter higher-order component
export default withRouter(Header);
复制代码

路由器

当我们想在本身不能直接路由的组件中使用路由时,我们需要使用React RouterwithRouter 高阶组件。当用户注销我们的应用程序时,我们希望重置缓存存储区,以防止任何不需要的数据出现在会话外部。Apollo可以调用resetStore 函数,它将完全清除缓存。

让我们在组件的按钮上添加一个 onClick处理函数,以删除用户的令牌,重置Apollo 仓库,更新本地状态并将用户重定向到首页。为此,我们将更新 useQuery 挂钩,以包括对客户端的引用,并将组件包装 在export 语句的 withRouter高阶组件中 。

const Header = props => {
  // query hook for user logged in state
  const { data, client } = useQuery(IS_LOGGED_IN);

  return (
    <HeaderBar> <img src={logo} alt="Notedly Logo" height="40" /> <LogoText>Notedly</LogoText> {/* If logged in display a logout link, else display sign-in options */} <UserState> {data.isLoggedIn ? ( <ButtonAsLink onClick={() => {
              // remove the token
              localStorage.removeItem('token');
              // clear the application's cache
              client.resetStore();
              // update local state
              client.writeData({ data: { isLoggedIn: false } });
              // redirect the user to the home page
              props.history.push('/');
            }}
          >
            Logout </ButtonAsLink> ) : ( <p> <Link to={'/signin'}>Sign In</Link> or{' '} <Link to={'/signup'}>Sign Up</Link> </p> )} </UserState> </HeaderBar> );
};

export default withRouter(Header); 
复制代码

最后,重置存储后,我们需要Apollo将用户状态添加回我们的缓存状态。在 src/App.js 将缓存设置更新为包括 onResetStore

// check for a local token
const data = {
  isLoggedIn: !!localStorage.getItem('token')
};

// write the cache data on initial load
cache.writeData({ data });
// write the cache data after cache is reset
client.onResetStore(() => cache.writeData({ data })); 
复制代码

这样,登录用户可以轻松注销我们的应用程序。我们已经将此功能直接集成到了 Header 组件中,但是将来我们可以将其重构为一个独立的组件。

创建登录表单

当前,我们的用户可以注册并注销我们的应用程序,但是他们无法重新登录。让我们创建一个登录表单,并在此过程中进行一些重构,以便我们可以重用许多代码在我们的注册组件中找到。

我们的第一步将是创建一个新的页面组件,该组件将位于/signin 。在src/pages/signin.js的新文件中 ,添加以下内容:

import React, { useEffect } from 'react';

const SignIn = props => {
  useEffect(() => {
    // update the document title
    document.title = 'Sign In — Notedly';
  });

  return (
    <div> <p>Sign up page</p> </div>
  );
};

export default SignIn; 
复制代码

现在我们可以使页面可路由,以便用户可以导航到该页面。在 src/pages/index.js 导入路由页面并添加新的路由路径:

 // import the sign-in page component
import SignIn from './signin';

const Pages = () => {
  return (
    <Router> <Layout> // ... our other routes
        // add a signin route to our routes list <Route path="/signin" component={SignIn} /> </Layout> </Router> );
}; 
复制代码

在实施登录表单之前,让我们暂停一下,考虑我们的选项。我们可以重新实现一个表单,就像我们在“注册”页面上写的那样,但这听起来很乏味,并且需要我们维护两个相似的表单。当一个更改时,我们需要确保更新另一个。另一个选择是将表单隔离到其自己的组件中,这将使我们能够重复使用通用代码并在单个位置进行更新。让我们继续使用共享表单组件方法。

我们将首先在src/components/UserForm.js中创建一个新组件,介绍我们的标记和样式。

我们将对该表单进行一些小的但值得注意的更改,使用它从父组件接收的属性。首先,我们将onSubmit请求重命名为props.action,这将使我们能够通过组件的属性将修改传递给表单。其次,我们将添加一些条件语句,我们知道我们的两种形式将有所不同。我们将使用第二个名为formType的属性,该属性将传递一个字符串。我们可以根据字符串的值更改模板的渲染。

我们会通过逻辑运算符&&或三元运算符。

import React, { useState } from 'react';
import styled from 'styled-components';

import Button from './Button';

const Wrapper = styled.div`
  border: 1px solid #f5f4f0;
  max-width: 500px;
  padding: 1em;
  margin: 0 auto;
`;

const Form = styled.form`
  label,
  input {
    display: block;
    line-height: 2em;
  }

  input {
    width: 100%;
    margin-bottom: 1em;
  }
`;

const UserForm = props => {
  // set the default state of the form
  const [values, setValues] = useState();

  // update the state when a user types in the form
  const onChange = event => {
    setValues({
      ...values,
      [event.target.name]: event.target.value
    });
  };

  return (
    <Wrapper>
      {/* Display the appropriate form header */}
      {props.formType === 'signup' ? <h2>Sign Up</h2> : <h2>Sign In</h2>}
      {/* perform the mutation when a user submits the form */}
      <Form
        onSubmit={e => {
          e.preventDefault();
          props.action({
            variables: {
              ...values
            }
          });
        }}
      >
        {props.formType === 'signup' && (
          <React.Fragment>
            <label htmlFor="username">Username:</label>
            <input
              required
              type="text"
              id="username"
              name="username"
              placeholder="username"
              onChange={onChange}
            />
          </React.Fragment>
        )}
        <label htmlFor="email">Email:</label>
        <input
          required
          type="email"
          id="email"
          name="email"
          placeholder="Email"
          onChange={onChange}
        />
        <label htmlFor="password">Password:</label>
        <input
          required
          type="password"
          id="password"
          name="password"
          placeholder="Password"
          onChange={onChange}
        />
        <Button type="submit">Submit</Button>
      </Form>
    </Wrapper>
  );
};

export default UserForm; 
复制代码

现在,我们可以简化 src/pages/signup.js 组件以利用共享表单组件:

import React, { useEffect } from 'react';
import { useMutation, useApolloClient, gql } from '@apollo/client';

import UserForm from '../components/UserForm';

const SIGNUP_USER = gql`
  mutation signUp($email: String!, $username: String!, $password: String!) {
    signUp(email: $email, username: $username, password: $password)
  }
`;

const SignUp = props => {
  useEffect(() => {
    // update the document title
    document.title = 'Sign Up — Notedly';
  });

  const client = useApolloClient();
  const [signUp, { loading, error }] = useMutation(SIGNUP_USER, {
    onCompleted: data => {
      // store the token
      localStorage.setItem('token', data.signUp);
      // update the local cache
      client.writeData({ data: { isLoggedIn: true } });
      // redirect the user to the homepage
      props.history.push('/');
    }
  });

  return (
    <React.Fragment>
      <UserForm action={signUp} formType="signup" />
      {/* if the data is loading, display a loading message*/}
      {loading && <p>Loading...</p>}
      {/* if there is an error, display a error message*/}
      {error && <p>Error creating an account!</p>}
    </React.Fragment>
  );
};

export default SignUp; 
复制代码

最后,我们可以使用 signIn 请求和 UserForm 组件编写 SignIn组件。

src/pages/signin.js

import React, { useEffect } from 'react';
import { useMutation, useApolloClient, gql } from '@apollo/client';

import UserForm from '../components/UserForm';

const SIGNIN_USER = gql`
  mutation signIn($email: String, $password: String!) {
    signIn(email: $email, password: $password)
  }
`;

const SignIn = props => {
  useEffect(() => {
    // update the document title
    document.title = 'Sign In — Notedly';
  });

  const client = useApolloClient();
  const [signIn, { loading, error }] = useMutation(SIGNIN_USER, {
    onCompleted: data => {
      // store the token
      localStorage.setItem('token', data.signIn);
      // update the local cache
      client.writeData({ data: { isLoggedIn: true } });
      // redirect the user to the homepage
      props.history.push('/');
    }
  });

  return (
    <React.Fragment>
      <UserForm action={signIn} formType="signIn" />
      {/* if the data is loading, display a loading message*/}
      {loading && <p>Loading...</p>}
      {/* if there is an error, display a error message*/}
      {error && <p>Error signing in!</p>}
    </React.Fragment>
  );
};

export default SignIn; 
复制代码

这样,我们现在有了一个易于管理的表单组件,并使用户能够注册和登录我们的应用程序。

保护路由

常见的应用程序模式是将对特定页面或网站部分的访问权限限制为经过身份验证的用户。在我们的情况下,未经身份验证的用户将无法使用“我的笔记”或“收藏夹”页面。我们可以在路由器中实现此模式,当未经身份验证的用户尝试访问那些路由时,会将他们自动导航到应用程序的“登录”页面。

src/pages/index.js中, 我们将首先导入必要的依赖项并添加我们的 isLoggedIn 查询:

import { useQuery, gql } from '@apollo/client';

const IS_LOGGED_IN = gql`
  {
    isLoggedIn @client
  }
`; 
复制代码

现在,我们将导入React RouterRedirect 库并编写一个 PrivateRoute 组件,如果用户未登录,它将对用户进行重定向:

// update our react-router import to include Redirect
import { BrowserRouter as Router, Route, Redirect } from 'react-router-dom';

// add the PrivateRoute component below our `Pages` component
const PrivateRoute = ({ component: Component, ...rest }) => {
  const { loading, error, data } = useQuery(IS_LOGGED_IN);
  // if the data is loading, display a loading message
  if (loading) return <p>Loading...</p>;
  // if there is an error fetching the data, display an error message
  if (error) return <p>Error!</p>;
  // if the user is logged in, route them to the requested component
  // else redirect them to the sign-in page
  return (
    <Route
      {...rest}
      render={props =>
        data.isLoggedIn === true ? (
          <Component {...props} />
        ) : (
          <Redirect
            to={{
              pathname: '/signin',
              state: { from: props.location }
            }}
          />
        )
      }
    />
  );
};

export default Pages; 
复制代码

最后,我们可以更新用于登录用户的任何路由以使用 PrivateRoute 组件:

const Pages = () => {
  return (
    <Router>
      <Layout>
        <Route exact path="/" component={Home} />
        <PrivateRoute path="/mynotes" component={MyNotes} />
        <PrivateRoute path="/favorites" component={Favorites} />
        <Route path="/note/:id" component={Note} />
        <Route path="/signup" component={SignUp} />
        <Route path="/signin" component={SignIn} />
      </Layout>
    </Router>
  );
}; 
复制代码

重定向状态

当我们重定向私有路由时,我们也将存储URL作为状态。这使我们能够将用户重定向到他们最初试图导航到的页面。我们可以更新登录页面的重定向,可以选择使用props.state. ' ' location.from来启用这个功能。

现在,当用户试图导航到为已登录用户准备的页面时,他们将被重定向到我们的登录页面。

结论

在本章中,我们介绍了构建客户端JavaScript应用程序的两个关键概念:身份验证和状态。通过构建完整的身份验证流程,你已洞悉用户帐户如何与客户端应用程序一起使用。从这里开始,我希望你探索OAuth等替代选项以及Auth0OktaFirebase等身份验证服务。此外,你已经学会了使用React Hooks API在组件级别管理应用程序中的状态,以及使用Apollo的本地状态管理整个应用程序中的状态。

有了这些关键概念,你现在就可以构建强大的用户界面应用程序。

如果有理解不到位的地方,欢迎大家纠错。如果觉得还可以,麻烦您点赞收藏或者分享一下,希望可以帮到更多人。