有時候,我們在做專案時,總會有會員的需求,大概上就是指有些內容只有登入過的使用者能夠瀏覽,今天我們就要來實作這樣的流程。
在 Gatsby 中,我們透過 API 的方式去與後端交換資訊,例如說註冊、登入、忘記密碼等等,今天的文章主要是實作一個流程,針對 API 部分不會有太多著墨,不過有蠻多相關的資源可以協助我們做身份驗證,例如 Auth0、Firebase、Passport.js 等等,筆者會在參考資料中附上這些服務的相關資訊,有興趣的讀者可以在自行閱讀。
本篇文章預設的模板是 gatsby-starter-hello-world
我們就用以下指令快速起一個專案吧!
gatsby new ithelp-auth-tutorial gatsbyjs/gatsby-starter-hello-world
建立完專案後,就讓我們開始今天的章節吧!
因為目前的模板只有提供我們簡易的 Hello World 首頁,我們必須先快速的建立幾個頁面來應付之後的需求。
首先,我們就先建立 NavBar 元件吧!
我們在 src 目錄下建立一個 components 資料夾,並在裡面新增一支 NavBar.js,我們會再檔案中放入一下程式碼,程式碼中我們快速了建立幾個連到首頁的 NavItem,之後我們會再來微調導轉連結。
import React from "react"
import { Link } from "gatsby"
export default () => (
<div
style={{
display: "flex",
flex: "1",
justifyContent: "space-between",
borderBottom: "1px solid #d1c1e0",
}}
>
<span>You are not logged in</span>
<nav>
<Link to="/">Home</Link>
{` `}
<Link to="/">Profile</Link>
{` `}
<Link to="/">Logout</Link>
</nav>
</div>
)
完成 NavBar 後,我們來做一個通用的 Layout,所以我們繼續在 Components 目錄下新增一支 Layout.js,裡面的程式碼會放入以下程式碼,非常單純的結構,只是將 NavBar 引入後,放到頁面中,並與 Props.children 一起顯示在畫面上。
import React from "react"
import NavBar from "./nav-bar"
const Layout = ({ children }) => (
<>
<NavBar />
{children}
</>
)
export default Layout
完成 Layout 元件後,我們改寫一下 pages 目錄下的 index.js,調整的內容只是將我們剛剛完成 Layout 元件做引入,並放置 JSX 中即可
import React from "react"
import Layout from "../components/layout"
export default () => (
<Layout>
<h1>Hello world!</h1>
</Layout>
)
完成上述步驟後,我們開啟開發伺服器 ( gatsby develop ) 來看看吧!
應該會有一個抖擻的 Hello World 與顯示目前狀態與三個連結的 NavBar。
再完成畫面後,我們要來做一些跟會員相關的基礎功能,例如判斷是否已經有會員資訊,或者用我們 hard code 寫入的帳號密碼來判斷是否成功登入等等,所以我們先在 src 下創建一個 services 目錄,裡面新增一支 auth.js 。
我們在這支檔案中會做幾個功能
詳細程式碼與註解如下,讀者可以直接將以下程式碼貼入到 Auth.js 當中。
// 判斷是否藉由瀏覽器瀏覽
export const isBrowser = () => typeof window !== "undefined"
// 從 LocalStorage 中取得使用者資訊,有的話就 Parse 這筆資訊。
export const getUser = () =>
isBrowser() && window.localStorage.getItem("gatsbyUser")
? JSON.parse(window.localStorage.getItem("gatsbyUser"))
: {}
// 用來登入成功後,設定資訊在 LocalStorage 中。
const setUser = user =>
window.localStorage.setItem("gatsbyUser", JSON.stringify(user))
// 處理登入,這邊使用 Hard Code 的方式驗證,此方式極度不安全,只是為了示範登入流程才用此方式,正式環境上會打 API 去跟後端確認資料是否正確,並等回傳結果後才會進行下一步
export const handleLogin = ({ username, password }) => {
if (username === `ithelp` && password === `123456`) {
return setUser({
username: `reynold`,
name: `Reynold`,
email: `reynold@example.org`,
})
}
return false
}
// 判斷使用者是否已經登入,如以登入就直接從 LocalStorage 中撈取使用者資料
export const isLoggedIn = () => {
const user = getUser()
return !!user.username
}
// 登出,直接將使用者資料清空
export const logout = callback => {
setUser({})
callback()
}
功能完成後,我們要來建立私人路由,也就是指登入過後的使用者才能瀏覽的頁面,我們同樣會用 createPage API 來動態創建頁面,我們會在 Gatsby-node.js 設定所有 APP 開頭的頁面都需要權限才可瀏覽。
所以我們會在 Gatsby-node.js 放置以下程式碼
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
if (page.path.match(/^\/app/)) {
page.matchPath = "/app/*"
createPage(page)
}
}
這邊的邏輯,Gatsby 也有相關套件已經幫我們處理,詳細請參考 gatsby-plugin-create-client-paths
現在我們要來創建一個通用的頁面,裡面會用來放需要權限的頁面,
我們在 src 的 pages 目錄中建立一支 app.js,而裡面的內容是
import React from "react"
import { Router } from "@reach/router"
import Layout from "../components/layout"
import Profile from "../components/Profile"
import Login from "../components/Login"
const App = () => (
<Layout>
<Router>
<Profile path="/app/profile" />
<Login path="/app/login" />
</Router>
</Layout>
)
export default App
我們引入待會會製作的登入與個人資料頁,而聰明的讀者們應該都猜得到,個人資料頁就是需要權限的頁面,而登入頁就會拿來用於取得權限,接著我們來製作個人資料頁,個人資料頁的程式碼會如下方一般,非常單純,只是做資料的呈現。
import React from "react"
const Profile = () => (
<>
<h1>我的個人簡介</h1>
<ul>
<li>名稱: Your name will appear here</li>
<li>信箱: And here goes the mail</li>
</ul>
</>
)
export default Profile
再來,我們要製作登入頁,裡面邏輯會稍微複雜,會做一些表單的處理,並引入我們先前做好的函式來使用,最後使用我們引入的 navigate 來協助使用者導轉到正確的頁面。
import React, { useState } from "react"
import { navigate } from "gatsby"
import { handleLogin, isLoggedIn } from "../services/auth"
const Login = () => {
const [userAccount, setUserAccount] = useState({
username: '',
password: ''
});
const handleUpdate = event => {
setUserAccount({...userAccount, [event.target.name]: event.target.value})
}
const handleSubmit = event => {
event.preventDefault()
handleLogin(userAccount)
}
if (isLoggedIn()) {
navigate(`/app/profile`)
}
return (
<>
<h1>登入</h1>
<form
method="post"
onSubmit={event => {
handleSubmit(event)
navigate(`/app/profile`)
}}
>
<label>
Username
<input type="text" name="username" onChange={handleUpdate} />
</label>
<label>
Password
<input
type="password"
name="password"
onChange={handleUpdate}
/>
</label>
<input type="submit" value="登入" />
</form>
</>
)
}
export default Login
完成登入頁面後,我們可以重啟開發伺服器看看,此時應該 /app/login 與 /app/profile 都可以暢行無阻的瀏覽,所以接下來我們要製作一個 HOC ( High Order Component ) ,HOC 簡單來說是一個函數,我們可以將元件作為參數代入,經由處理後,會回傳一個新的元件,而使用 HOC 的目的在於我們將通用的邏輯都放在其中,使程式碼更容易維護且更簡潔 。
所以我們在 src/components 目錄下新增一支 privateRoute.js ,並在裡面放入以下程式碼,我們在程式碼中做了是否登入的檢查,如果沒登入就導轉回 Login 頁面
import React, { Component } from "react"
import { navigate } from "gatsby"
import { isLoggedIn } from "../services/auth"
const PrivateRoute = ({ component: Component, location, ...rest }) => {
if (!isLoggedIn() && location.pathname !== `/app/login`) {
navigate("/app/login")
return null
}
return <Component {...rest} />
}
export default PrivateRoute
若對 HOC 有興趣的讀者,可以閱讀此篇文章 React.js: Higher-Order Components
完成 PrivateRoute 後,我們回到 app.js 去將原本的 Profile 元件更改為我們剛剛製作好的 PrivateRoute,並把連結跟顯示的元件用 Props 的方式傳給 HOC 作處理,所以程式碼會調整成以下
import React from "react"
import { Router } from "@reach/router"
import Layout from "../components/layout"
import PrivateRoute from "../components/privateRoute"
import Profile from "../components/Profile"
import Login from "../components/Login"
const App = () => (
<Layout>
<Router>
<PrivateRoute path="/app/profile" component={Profile} />
<Login path="/app/login" />
</Router>
</Layout>
)
export default App
還記得我們之前先寫好的 NavBar 嗎?
在相關頁面與邏輯都處理的差不多了,我們現在要回頭去稍作調整,
首先我們打開 components 目錄下的 NavBar.js,
並把我們做好的登出函示放到對應的位置上,還有判斷如果會員已經登入,就顯示對應的資料,並把登出按鈕做隱藏。
import React from 'react'
import { Link, navigate } from "gatsby"
import { getUser, isLoggedIn, logout } from "../services/auth"
const NavBar = () => {
const content = { message: "", login: true }
if (isLoggedIn()) {
content.message = `歡迎回來, ${getUser().name}`
} else {
content.message = "還沒登入唷。"
}
return (
<div
style={{
display: "flex",
flex: "1",
justifyContent: "space-between",
borderBottom: "1px solid #d1c1e0",
}}
>
<span>{content.message}</span>
<nav>
<Link to="/">Home</Link>
{` `}
<Link to="/app/profile">Profile</Link>
{` `}
{isLoggedIn() ? (
<a
href="/"
onClick={event => {
event.preventDefault()
logout(() => navigate(`/app/login`))
}}
>
Logout
</a>
) : null}
</nav>
</div>
)
}
export default NavBar
再來,我們更改首頁,同樣的判斷使用者是否已經登入,並且顯示對應的資訊
import React from "react"
import { Link } from "gatsby"
import { getUser, isLoggedIn } from "../services/auth"
import Layout from "../components/Layout"
export default () => (
<Layout>
<h1>嗨, {isLoggedIn() ? getUser().name : "你還沒註冊唷"}!</h1>
<p>
{isLoggedIn() ? (
<>
你已經是會員,請確認你的{" "}
<Link to="/app/profile">個人資訊</Link>
</>
) : (
<>
你應該先進行<Link to="/app/login">登入</Link>來閱讀更多的內容。
</>
)}
</p>
</Layout>
)
再堅持一下,最後一步,我們修改 Profile 頁面,裡面會顯示會員的資訊
import React from "react"
import { getUser } from "../services/auth"
const Profile = () => (
<>
<h1>我的個人簡介</h1>
<ul>
<li>名稱: {getUser().name}</li>
<li>信箱: {getUser().email}</li>
</ul>
</>
)
export default Profile
完成後,各位讀者可以試著玩玩看,輸入帳號密碼後,是否能正確登入,
對應的頁面有沒有顯示正確的資料,如果都正常的話,
恭喜!各位讀者已經擁有一個可以登入並且限制權限頁面的網站囉!
Gatsby - Making a Site with User Authentication