React 部落格實作


Posted by chihyu on 2021-01-25

部落格

結構設置

  1. 安裝 React router,可以用來做不同頁面
    npm install react-router-dom
    
  2. 引入 HashRouter
    import {
    HashRouter as Router,
    Switch,
    Route
    } from "react-router-dom";
    
  3. 最外層用 Rooter 包住,定義 path
    function App() {
    return (
     <Root>
     <Router>
       <Header />
       <Switch>
         <Route exact path="/">
           <HomePage />
         </Route>
         <Route exact path="/login">
           <LoginPage />
         </Route>
       </Switch>
     </Router>
     </Root>
    )   
    }
    
  4. 在跟 components 同層的地方新增 pages 的資料夾
  5. 在 pages 的資料夾裡新增 HomePage、LoginPage 的資料夾
  6. 分別在各自的資料夾裡新增要 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";
    
  7. 在 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

  1. 在 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 = () => {
    ...
    }
    
  2. 引入 api
    import WebAPI from '../../WebAPI';
    
  3. 取得的文章使用 useState,初始值傳空陣列
    const [posts, setPosts] = useState([]);
    
  4. 使用 useEffect,後接空陣列,讓串接 api 只執行第一次
    useEffect(() => {
     getPosts().then(posts => setPosts(posts))
    }, [])
    
  5. 用 .map() 讓回傳的每一個 post 都套用相同格式,並傳入參數
    <Root>
    {posts.map(
     post => <Post post={post} />
    )}
    </Root>
    
  6. 寫 Post 的 function component
    function Post({ post }) {
    return (
     <PostContainer>
       <PostTitle>{post.title}</PostTitle>
       <PostDate>{new Date(post.createdAt).toLocalString()}</PostDate>
     </PostContainer>
    )
    }
    
  7. 把剛剛寫的 component 寫上 css

實作登入功能

JWT - JSON Web Token
身分驗證時 header 帶 JWT 給 Server

  1. 登入的 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))
    }
    
  2. 身分驗證的 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))
    }
    
  3. 切 login 的板
  4. 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>
    
  5. 使用 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
    })
    }
    
  6. 登入成功後,導回首頁,用 useHistory
    // 引入 useHistory
    import { useHistory } from "react-router-dom";
    const handleSubmit = (e) => {
     ...
       setAuthToken(data.token)
       history.push("/") // push 裡面放要到的頁面
     })
    }
    
  7. 在 App.js 使用 useState 綁定登入的 user,如果 user 有東西就代表有登入,沒東西代表沒登入
    const [user, setUser] = useState(null)
    
  8. 建立 context,讓 user 可以傳到其他檔案
    // 新建檔案 context.js
    import { createContext } from 'react';
    // 透過 AuthContext 來拿取值
    export const AuthContext = createContext(null);
    
  9. 在 App.js 提供 provider,把 value 的值往下傳
    // 引入 AuthContext
    import { AuthContext } from "../../contexts";
    // 把 user、setUser 帶入 value
    <AuthContext.Provider value={{user, setUser}}>
     <Root>
       ...
     </Root>
    </AuthContext.Provider>
    
  10. 登入後用 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("/")
    })
    
  11. 在 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>}
    
  12. 用 handleLogout 處理 logout 的 onClick 事件
    // 引入 setAuthToken
    import { setAuthToken } from "../../utils";
    // 登出時,讓 token 為空,user 也為空
    const handleLogout = () => {
    setAuthToken('');
    setUser(null);
    if (location.pathname !== "/") {
      history.push('/');
    }
    }
    
  13. 用 useEffect(初始化),讓頁面重新整理時還看是有 user 資料
    // 引入 useEffect
    import React, { useState, useEffect } from "react";
    // app 剛 mount 的時候就去 call getMe()
    useEffect(() => {
    getMe().then((response) => {
      if (response.ok) {
        setUser(response.data)
      }
    });
    }, []);
    

實作單一文章頁面

  1. 新增一個資料夾 PostPage,新增檔案 PostPage.js、index.js
  2. 在 index.js export PostPage.js
    export { default } from "./PostPage";
    
  3. 在 PostPage.js 切版
  4. 在 PostPage.js 用 useParams 把文章的 id 傳入
    // 引入 useParams
    import { useParams } from "react-router-dom";
    // 把 id 宣告為 useParams
    const { id } = useParams();
    // 在拿到文章的地方把 id 帶入
    useEffect(() => {
    getPost(id).then(post => setPost(post));
    }, [])
    
  5. 在 Route 的地方把要帶入 id
    to 跟 path 兩個格式要寫一樣,":" 後面的東西就是可以替換的東西
    <Route exact path="/posts/:id">
    
    <PostTitle to={`/posts/${post.id}`}>{post.title}</PostTitle>
    

實作發文功能

  1. 在 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)
    }
    
  2. 新增一個發文的檔案,切版
  3. 用 useState 綁定 title、body
    const [title, setTitle] = useState("");
    const [body, setBody] = useState("");
    
  4. 發文的 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,可以依功能細分出多個資料夾,資料夾的名稱是小寫,裡面的檔案用大寫開頭

#Web #React #Blog







Related Posts

簡介 Markov Decision Process 與其應用

簡介 Markov Decision Process 與其應用

[FE302] React 基礎 ( hooks 版 ):React 基礎

[FE302] React 基礎 ( hooks 版 ):React 基礎

C++ 教學(四) 條件敘述句

C++ 教學(四) 條件敘述句


Comments