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

day00 關於android七日計畫,關於我

day00 關於android七日計畫,關於我

Week 21 學習的第二週

Week 21 學習的第二週

部署 (1) —— 建立 AWS EC2 主機及 SSH 連線

部署 (1) —— 建立 AWS EC2 主機及 SSH 連線


Comments