部落格
結構設置
- 安裝 React router,可以用來做不同頁面
npm install react-router-dom
- 引入 HashRouter
import { HashRouter as Router, Switch, Route } from "react-router-dom";
- 最外層用 Rooter 包住,定義 path
function App() { return ( <Root> <Router> <Header /> <Switch> <Route exact path="/"> <HomePage /> </Route> <Route exact path="/login"> <LoginPage /> </Route> </Switch> </Router> </Root> ) }
- 在跟 components 同層的地方新增 pages 的資料夾
- 在 pages 的資料夾裡新增 HomePage、LoginPage 的資料夾
- 分別在各自的資料夾裡新增要 export 的檔案
// LoginPage.js 內容的部分 import React, { useState, useEffect } from "react"; import styled from "styled-components"; import PropTypes from 'prop-types'; import { HashRouter as Router, Switch, Route } from "react-router-dom"; const Root = styled.div`` export default function LoginPage() { return <div>Login page</div>; }
// index.js export 的部分 export { default } from "./LoginPage";
- 在 App 引入 pages
import LoginPage from '../../pages/LoginPage'; import HomePage from '../../pages/HomePage'; import Header from '../Header';
切版
- 連到別的頁面
// 引入 Link import { Link } from "react-router-dom"; // 把 Nav 的 styled 改成 Link const Nav = styled(Link)` // 在 Nav 傳入路徑 <Nav to="/" $active>Home</Nav>
- 讓當前頁面是 active
// 引入 useLocation import { Link, useLocation } from "react-router-dom"; // 宣告 location const location = useLocation(); // 設定 location 是該路徑時 active <Nav to="/" $active={location.pathname === '/'}>Home</Nav>
串 API
- 在 src 新建實作 API 的檔案 WebAPI.js,在該檔案串 api
const BASE_URL = "https://student-json-api.lidemy.me"; export cont getPosts = () => { return fetch(`${BASE_URL}`/posts?_sort=createdAt&_order=desc).then((res) => res.json() ); }; // login 的 api,會回傳 token export const login = (username, password) => { ... } // user 的 api,會回傳 user data export const getMe = () => { ... }
- 引入 api
import WebAPI from '../../WebAPI';
- 取得的文章使用 useState,初始值傳空陣列
const [posts, setPosts] = useState([]);
- 使用 useEffect,後接空陣列,讓串接 api 只執行第一次
useEffect(() => { getPosts().then(posts => setPosts(posts)) }, [])
- 用 .map() 讓回傳的每一個 post 都套用相同格式,並傳入參數
<Root> {posts.map( post => <Post post={post} /> )} </Root>
- 寫 Post 的 function component
function Post({ post }) { return ( <PostContainer> <PostTitle>{post.title}</PostTitle> <PostDate>{new Date(post.createdAt).toLocalString()}</PostDate> </PostContainer> ) }
- 把剛剛寫的 component 寫上 css
實作登入功能
JWT - JSON Web Token
身分驗證時 header 帶 JWT 給 Server
- 登入的 API,response 會拿到跟註冊時一樣的 token
export const login = (username, password) => { return fetch(`${BASE_URL}/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ username, password }) }) .then(res => res.json()) .then(data => console.log(data)) }
- 身分驗證的 API,server 會比對 header 帶的 token
export const getMe = () => { const token = localStorage.getItem("token"); return fetch(`${BASE_URL}/me`, { headers: { 'authorization': `Bearer ${token}` } }) .then(res => res.json()) .then(data => console.log(data)) }
- 切 login 的板
- username、password 的值使用 useState 綁定值跟 setter
// 初始值要帶空字串,不然會認定一開始是 uncontrolled const [username, setUsername] = useState("") const [password, setPassword] = useState("") <Username> Username: <input value={username} onChange={e => setUsername(e.target.value)} /> </Username> <Password> Password: <input type="password" value={password} onChange={e => setPassword(e.target.value)} /> </Password>
- 使用 handleSubmit 執行 submit,要做錯誤處理
// 引入串 login 的 api import { login } from '../../WebAPI'; // 錯誤處理訊息用 useState 綁定 const [errorMessage, setErrorMessage] = useState(); // handleSubmit 執行 onSubmit 事件 const handleSubmit = (e) => { setErrorMessage(null); login(username, password).then((data) => { if (data.ok === 0) { return setErrorMessage(data.message) } setAuthToken(data.token) // 設置 token }) }
- 登入成功後,導回首頁,用 useHistory
// 引入 useHistory import { useHistory } from "react-router-dom"; const handleSubmit = (e) => { ... setAuthToken(data.token) history.push("/") // push 裡面放要到的頁面 }) }
- 在 App.js 使用 useState 綁定登入的 user,如果 user 有東西就代表有登入,沒東西代表沒登入
const [user, setUser] = useState(null)
- 建立 context,讓 user 可以傳到其他檔案
// 新建檔案 context.js import { createContext } from 'react'; // 透過 AuthContext 來拿取值 export const AuthContext = createContext(null);
- 在 App.js 提供 provider,把 value 的值往下傳
// 引入 AuthContext import { AuthContext } from "../../contexts"; // 把 user、setUser 帶入 value <AuthContext.Provider value={{user, setUser}}> <Root> ... </Root> </AuthContext.Provider>
- 登入後用 getMe() 取得 user 的資料
// 在 LoginPage 引入 context import React, { useContext } from "react"; import { AuthContext } from "../../contexts"; // 把 setUser 從 AuthContext 拿出來 const { setUser } = useContext(AuthContext); getMe().then((response) => { if (response.ok !== 1) { // 錯誤處理 setAuthToken(null); return setErrorMessage(response.toString()); } setUser(response.data) // 設 user 為回傳的 data history.push("/") })
- 在 Header 的檔案用 user 來決定顯示的 Nav
// 引入 context import React, { useContext } from "react"; import { setAuthToken } from "../../utils"; // 從 AuthContext 取出 user、setUser const { user, setUser } = useContext(AuthContext) {user && <Nav to="/new-post" $active={location.pathname === '/new-post'}>Post Story</Nav>} {!user && <Nav to="/login" $active={location.pathname === '/login'}>Login</Nav>} {user && <Nav onClick={handleLogout}>Logout</Nav>}
- 用 handleLogout 處理 logout 的 onClick 事件
// 引入 setAuthToken import { setAuthToken } from "../../utils"; // 登出時,讓 token 為空,user 也為空 const handleLogout = () => { setAuthToken(''); setUser(null); if (location.pathname !== "/") { history.push('/'); } }
- 用 useEffect(初始化),讓頁面重新整理時還看是有 user 資料
// 引入 useEffect import React, { useState, useEffect } from "react"; // app 剛 mount 的時候就去 call getMe() useEffect(() => { getMe().then((response) => { if (response.ok) { setUser(response.data) } }); }, []);
實作單一文章頁面
- 新增一個資料夾 PostPage,新增檔案 PostPage.js、index.js
- 在 index.js export PostPage.js
export { default } from "./PostPage";
- 在 PostPage.js 切版
- 在 PostPage.js 用 useParams 把文章的 id 傳入
// 引入 useParams import { useParams } from "react-router-dom"; // 把 id 宣告為 useParams const { id } = useParams(); // 在拿到文章的地方把 id 帶入 useEffect(() => { getPost(id).then(post => setPost(post)); }, [])
- 在 Route 的地方把要帶入 id
to 跟 path 兩個格式要寫一樣,":" 後面的東西就是可以替換的東西<Route exact path="/posts/:id">
<PostTitle to={`/posts/${post.id}`}>{post.title}</PostTitle>
實作發文功能
- 在 WebAPI 的檔案裡寫 post 文章的 API,把 title 跟 body 當參數帶入
export const postNew = (title, body) => { const token = getAuthToken(); return fetch(`${BASE_URL}/posts`, { method: 'POST', headers: { 'content-type': 'application/json', 'authorization': `Bearer ${token}` }, body: JSON.stringify({ title, body }) }) .then(res => res.json) }
- 新增一個發文的檔案,切版
- 用 useState 綁定 title、body
const [title, setTitle] = useState(""); const [body, setBody] = useState("");
- 發文的 submit 用 handleSubmit 執行發文 API
單一文章頁面const handleSubmit = () => { setErrorMessage(null); if (token) { if (!title || !body) { return setErrorMessage("Please fill the empty area.") } postNew(title, body).then((response) => { // 呼叫 api console.log(response) }) getPosts().then(posts => setPosts(posts)); //重新呼叫全部文章的 API history.push("/") // 回首頁 } }
- 新增路徑到 postpage
- 在 postpage call api
- useParams 傳參數,可以在 component 拿到參數
發文功能- 登入功能
- 登入才能新增文章
- 按完發文導回首頁
- 導回首頁會發一個新的 request 去拿 post 就可以看到剛剛拿的 POST
閃一下的問題
- 在確定登入登出之前不要顯示
- 剛進來時用一個 state isLoading,用一個 loading 畫面
- 可以檢查 localstorage 有沒有東西
檔案結構
- 用一個資料夾裡面放 component,可以依功能細分出多個資料夾,資料夾的名稱是小寫,裡面的檔案用大寫開頭