翻譯 | 《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
的本地狀態管理整個應用程序中的狀態。
有了這些關鍵概念,你如今就能夠構建強大的用戶界面應用程序。
若是有理解不到位的地方,歡迎你們糾錯。若是以爲還能夠,麻煩您點贊收藏或者分享一下,但願能夠幫到更多人。