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

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

写在最前面

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

为了提升你们的阅读体验,对语句的结构和内容略有调整。若是发现本文中有存在瑕疵的地方,或者你有任何意见或者建议,能够在评论区留言,或者加个人微信:code_maomao,欢迎相互沟通交流学习。前端

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

第15章 Web身份验证和状态

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

客户端Web身份验证的工做方式几乎与这相同。浏览器

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

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

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

建立注册表格

开始使用咱们的应用程序的客户端身份验证,咱们能够建立一个用户注册React组件。在这样作以前,让咱们先肯定组件的工做方式。react-router

首先,用户将导航到咱们应用程序中的/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)(http://vipkshttp0.wiz.cn/ks/s...]

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的本地状态管理整个应用程序中的状态。

有了这些关键概念,你如今就能够构建强大的用户界面应用程序。

若是有理解不到位的地方,欢迎你们纠错。若是以为还能够,麻烦您点赞收藏或者分享一下,但愿能够帮到更多人。

相关文章
相关标签/搜索