实现登录
登录页面属于静态内容和动态内容混合的页面,登录之后的用户状态需要 Redux 管理。
安装和配置 Axios
发送请求需要安装 Axios 模块:npm i axios
配置 Axios 基础地址:
// gatsby-browser.js
// 用到 JSX 语法,所以要引入 React
const React = require("react")
// 组件是 ESM 语法,所以用 default 引入默认导出
const Layout = require("./src/components/Layout").default
// Redux
const { Provider } = require("react-redux")
const createStore = require("./src/store/createStore").default
// Axios
const axios = require("axios")
axios.defaults.baseURL = "https://conduit.productionready.io/api"
// element 是每个页面组件的内容
exports.wrapPageElement = ({ element }) => {
return <Layout>{element}</Layout>
}
// element 是应用的最外层组件
exports.wrapRootElement = ({ element }) => {
return <Provider store={createStore()}>{element}</Provider>
}
定义一个 Input 的自定义钩子函数
// src\hooks\useInput.js
import { useState } from "react"
export default function useInput(initialValue) {
const [value, setValue] = useState(initialValue)
return {
input: {
value,
onChange(e) {
setValue(e.target.value)
},
},
setValue,
}
}
创建存储登录状态的 Reducer
// src\store\reducers\auth.reducer.js
const initialState = {}
export default function (state = initialState, action) {
switch (action.type) {
case "loginSuccess":
return {
success: true,
user: action.payload,
}
break
case "loginFailed":
return {
success: false,
errors: action.payload,
}
break
default:
return state
break
}
}
// src\store\reducers\root.reducer.js
import { combineReducers } from "redux"
import counterReducer from "./counter.reducer"
import authReducer from "./auth.reducer"
export default combineReducers({
counterReducer,
authReducer,
})
创建登录 Sage
// src\store\sagas\auth.saga.js
import { takeEvery, put } from "redux-saga/effects"
import axios from "axios"
// 执行登录
function* login({ payload }) {
try {
const { data } = yield axios.post("/users/login", payload)
localStorage.setItem("token", data.user.token)
yield put({ type: "loginSuccess", payload: data.user })
} catch (ex) {
const errors = ex.response.data.errors
const message = []
for (let attr in errors) {
errors[attr].forEach(error => {
message.push(`${attr} ${error}`)
})
}
yield put({ type: "loginFailed", payload: message })
}
}
export default function* authSaga() {
yield takeEvery("login", login)
}
// src\store\sagas\root.saga.js
import { all } from "redux-saga/effects"
import counterSaga from "./counter.saga"
import authSaga from "./auth.saga"
export default function* rootSaga() {
yield all([counterSaga(), authSaga()])
}
修改登录页
// src\pages\login.js
import React from "react"
import useInput from "../hooks/useInput"
import { useDispatch, useSelector } from "react-redux"
import { navigate } from "gatsby"
export default function Login() {
const email = useInput("")
const password = useInput("")
const dispatch = useDispatch()
const authReducer = useSelector(state => state.authReducer)
// 如果已登录 跳转首页
if (authReducer.success) {
// navigate 是 Gatsby 提供的用于在代码内部导航的辅助函数
navigate("/")
return null
}
// 显示登录失败信息
function displayErrors() {
if (authReducer.errors) {
return authReducer.errors.map((item, index) => (
<li key={index}>{item}</li>
))
}
return null
}
// 提交表单
const handleSubmit = e => {
e.preventDefault()
const passwordValue = password.input.value
const emailValue = email.input.value
dispatch({
type: "login",
payload: {
user: {
email: emailValue,
password: passwordValue,
},
},
})
}
return (
<div className="auth-page">
<div className="container page">
<div className="row">
<div className="col-md-6 offset-md-3 col-xs-12">
<h1 className="text-xs-center">Sign up</h1>
<ul className="error-messages">{displayErrors()}</ul>
<form onSubmit={handleSubmit}>
<fieldset className="form-group">
<input
className="form-control form-control-lg"
type="text"
placeholder="Email"
{...email.input}
/>
</fieldset>
<fieldset className="form-group">
<input
className="form-control form-control-lg"
type="password"
placeholder="Password"
{...password.input}
/>
</fieldset>
<button className="btn btn-lg btn-primary pull-xs-right">
Sign up
</button>
</form>
</div>
</div>
</div>
</div>
)
}
同步用户登陆状态
获取用户登录信息
// src\store\sagas\auth.saga.js
import { takeEvery, put } from "redux-saga/effects"
import axios from "axios"
// 执行登录
function* login({ payload }) {
try {
const { data } = yield axios.post("/users/login", payload)
localStorage.setItem("token", data.user.token)
yield put({ type: "loginSuccess", payload: data.user })
} catch (ex) {
const errors = ex.response.data.errors
const message = []
for (let attr in errors) {
errors[attr].forEach(error => {
message.push(`${attr} ${error}`)
})
}
yield put({ type: "loginFailed", payload: message })
}
}
// 获取登录信息
function* loadUser({ payload }) {
const { data } = yield axios.get("/user", {
headers: {
Authorization: `Token ${payload}`,
},
})
yield put({ type: "loadUserSuccess", payload: data.user })
}
export default function* authSaga() {
yield takeEvery("login", login)
yield takeEvery("loadUser", loadUser)
}
// src\store\reducers\auth.reducer.js
const initialState = {}
export default function (state = initialState, action) {
switch (action.type) {
case "loginSuccess":
case "loadUserSuccess":
return {
success: true,
user: action.payload,
}
break
case "loginFailed":
return {
success: false,
errors: action.payload,
}
break
default:
return state
break
}
}
修改 Header
// src\components\Header.js
import React, { useEffect } from "react"
import { useDispatch, useSelector } from "react-redux"
export default function Header() {
const dispatch = useDispatch()
const authReducer = useSelector(state => state.authReducer)
useEffect(() => {
const token = localStorage.getItem("token")
if (token) {
dispatch({
type: "loadUser",
payload: token,
})
}
}, [])
return (
<nav className="navbar navbar-light">
<div className="container">
<a className="navbar-brand" href="index.html">
conduit
</a>
<ul className="nav navbar-nav pull-xs-right">
<li className="nav-item">
{/* Add "active" class when you're on that page" */}
<a className="nav-link active">Home</a>
</li>
{authReducer.success ? (
<Login username={authReducer.user.username} />
) : (
<Logout />
)}
</ul>
</div>
</nav>
)
}
// 已登录显示的链接
function Login({ username }) {
return (
<>
<li className="nav-item">
<a className="nav-link">
<i className="ion-compose" />
New Post
</a>
</li>
<li className="nav-item">
<a className="nav-link">
<i className="ion-gear-a" />
Settings
</a>
</li>
<li className="nav-item">
<a className="nav-link">{username}</a>
</li>
</>
)
}
// 未登录显示的链接
function Logout() {
return (
<>
<li className="nav-item">
<a className="nav-link">Sign in</a>
</li>
<li className="nav-item">
<a className="nav-link">Sign up</a>
</li>
</>
)
}
页面访问权限
在 realworld 中有一部分页面只有在登陆后才能访问,为了将它们保护起来,添加权限控制,就需要用到客户端路由。
客户端路由有更多的操作权限,并且这些页面不用实现 SEO,所以可以做成动态的客户端路由。
这些路由是在用户登陆后,所有数据都将从 API 加载,不需要服务器渲染,所以它们应该是客户端专用路由(Client-only Routes)。
实现客户端专用路由
将受限页面组件移动到 pages 目录外
放在 src/pages
目录下的组件会在构建应用时创建不受限制的页面,所以要将受限路由对应的页面组件放到其它地方,如将 create.js
和 settings.js
移动到 src/components
目录下。
创建受限路由的页面
在根目录下创建 gatsby-node.js
文件,导出 onCreatePage
方法,该方法会在每次创建页面后被调用。
该方法内可以获取到页面信息,根据页面访问地址可以判断是否要调用 createPage
创建页面。
createPage
会创建并更新页面内容。
这一系列操作可以用插件 gatsby-plugin-create-client-paths
完成。
npm i gatsby-plugin-create-client-paths
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: "gatsby-plugin-create-client-paths",
options: {
prefixes: ["/app/*"], // 指定客户端专用路由匹配规则
},
},
],
}
创建生成受限制页面的通用页面
接着在 src/pages
目录下创建 app.js
,app
是上面定义的匹配路径。
这是个通用页面,用于生成或配置受限制的页面。
页面使用 Gatsby 内置模块 @reach/router
的 Router
组件配置路由,它会为配置的路由生成页面信息对象(包括路径、组件文件等),在 Gatsby 创建页面的时候传递给 onCreatePage
方法。
// src\pages\app.js
import React from "react"
import { Router } from "@reach/router"
import Settings from "../components/settings"
import Create from "../components/create"
export default function App() {
return (
<Router>
<Settings path="app/settings" />
<Create path="app/create" />
</Router>
)
}
配置权限
封装判断用户登录
将判断用户是否登录的操作封装到一个自定义钩子函数中
// src\hooks\useLogin.js
import { useState, useEffect } from "react"
import axios from "axios"
export default function useLogin() {
const initialState = [
false, // 是否已经登陆
true, // 是否正在发送请求
]
const [status, setStatus] = useState(initialState)
useEffect(() => {
const token = localStorage.getItem("token")
if (token) {
try {
;(async function () {
await axios.get("/user", {
headers: {
Authorization: `Token ${token}`,
},
})
})()
setStatus([true, false])
} catch (ex) {
setStatus([false, false])
}
} else {
setStatus([false, false])
}
}, [])
return state
}
创建受保护的路由组件
创建一个组件,用于包装受限制的路由组件,在里面编写权限判断代码。
该组件接收路由组件属性,和需要传递给路由组件的全部属性:
// src\components\PrivateRoute.js
import React from "react"
import { navigate } from "gatsby"
import useLogin from "../hooks/useLogin"
export default function PrivateRoute({ component: Component, ...rest }) {
const [isLogin, loading] = useLogin()
if (loading) return null
if (isLogin) return <Component {...rest} />
navigate("/login")
return null
}
修改通用页面
// src\pages\app.js
import React from "react"
import { Router } from "@reach/router"
import PrivateRoute from "../components/PrivateRoute"
import Settings from "../components/settings"
import Create from "../components/create"
export default function App() {
return (
<Router>
<PrivateRoute component={Settings} path="app/settings" />
<PrivateRoute component={Create} path="app/create" />
</Router>
)
}