翻译 | 《JavaScript Everywhere》第15章 Web身份验证和状态php
你们好呀,我是毛小悠,是一位前端开发工程师。正在翻译一本英文技术书籍。html
为了提升你们的阅读体验,对语句的结构和内容略有调整。若是发现本文中有存在瑕疵的地方,或者你有任何意见或者建议,能够在评论区留言,或者加个人微信:code_maomao,欢迎相互沟通交流学习。前端
(σ゚∀゚)σ..:*☆哎哟不错哦react
最近我和个人家人搬家了。填写并签署了几种表格(个人手仍然很累)后,咱们就用钥匙进了前门。每次咱们回到家,咱们均可以使用这些钥匙来解锁并进入门。我很高兴我每次回家都不须要填写表格,但也感谢拥有一把锁,这样咱们就不会有任何不速之客到来了。api
客户端Web
身份验证的工做方式几乎与这相同。浏览器
咱们的用户将填写表格,并以密码和存储在他们的浏览器中的令牌的形式交给网站。当他们返回站点时,他们将使用令牌自动进行身份验证,或者可以使用其密码从新登陆。缓存
在本章中,咱们将使用GraphQL API
构建一个Web
身份验证系统。安全
为此,咱们将构建表单,将JWT
存储在浏览器中,随每一个请求发送令牌,并跟踪应用程序的状态。微信
开始使用咱们的应用程序的客户端身份验证,咱们能够建立一个用户注册React
组件。在这样作以前,让咱们先肯定组件的工做方式。react-router
首先,用户将导航到咱们应用程序中的/signup
路由。在此页面上,他们将看到一个表单,能够在其中输入电子邮件地址、用户名和密码。提交表单将执行咱们API
的 signUp
请求 。若是请求成功,将建立一个新的用户账户,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;
若是你只是在学习React
,那么常见的陷阱之一就是与HTML
对应的JSX
属性的不一样。在这种状况下,咱们使用JSX htmlFor
代替HTML
的 for
属性来避免任何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 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
修改的数据了。
修改注册
要注册用户,咱们将使用API
的 signUp
请求。若是注册成功,此请求将接受电子邮件、用户名和密码做为变量,并返回JWT
。让咱们写出咱们的请求并将其集成到咱们的注册表单中。
首先,咱们须要导入咱们的Apollo
库。咱们将利用useMutation
和useApolloClient
挂钩以及 Apollo Client
的 gql
语法。
在 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
)。
另外,若是你在GraphQLPlayground
( http
:// localhost
:4000/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
中
当令牌存储在 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 Client
的createHttpLink
以及来自Apollo
的Link 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.writeData
和useApolloClient
挂钩直接将其写入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
。
为了证实这一点,让咱们更新咱们的应用程序,
若是isLoggedIn
是false
,显示“注册”和“登陆”连接。
若是 isLoggedIn
是 true
就显示“注销”连接。
在 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 Router
的withRouter
高阶组件来处理重定向,由于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 Router
的 withRouter
高阶组件。当用户注销咱们的应用程序时,咱们但愿重置缓存存储区,以防止任何不须要的数据出如今会话外部。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 Router
的 Redirect
库并编写一个 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
等替代选项以及Auth0
,Okta
和Firebase
等身份验证服务。此外,你已经学会了使用React Hooks API
在组件级别管理应用程序中的状态,以及使用Apollo
的本地状态管理整个应用程序中的状态。
有了这些关键概念,你如今就能够构建强大的用户界面应用程序。
若是有理解不到位的地方,欢迎你们纠错。若是以为还能够,麻烦您点赞收藏或者分享一下,但愿能够帮到更多人。