React 實戰篇 - 部落格


做部落格之前,先來認識 react router

前端處理換頁路徑,換頁不是發 requset 到 server 去。前端透過 js history API 改變網址列上的內容,react router 背後就是使用這個原理在做。

用 component 方式表現 router 要怎麼設計

  1. npm install react-router-dom

Router 有兩個方式

  1. BrowserRouter:路徑是後面直接帶。缺點:github page 上面當我打這個路徑,他會去找這個資料夾底下的 index.html,試著把他載入。因為現在是靠前端換頁,所以我是用 js api 去更改網址。但如果今天是將網址複製貼上按 enter,就不是透過 JS,瀏覽器會直接發 request 到這個地方去。如果是這種方式用到 github page 上面去,就會沒有東西他會以為你要找這個資料夾底下的 index.html.
  2. HashRouter:A 頁面/#/換頁的東東,不管怎麼換頁對瀏覽器而言都是載入 A 頁面,# 表示換頁的東東是這個頁面底下的某個部分。參考閱讀:淺談新手在學習 SPA 時的常見問題:以 Router 為例

因為可能同時匹配到兩個不同的 link,用 switch 可以保證選第一個判斷的可以被匹配到。

加上 exact 就會變成完整匹配

建議新增一個資料夾 pages 不要在 components 裡面

import React, {useEffect, useState} from 'react'
import styled from 'styled-components'
import {
  HashRouter as Router,
  Switch,
  Route
} from 'react-router-dom'
import LoginPage from '../../pages/LoginPage'
import HomePage from '../../pages/HomePage'
import Header from '../Header'
const Root = styled.div``
function App() {
  return (
    <Root>
      <Router>
        <Header/>
        <Switch>
          <Route exact path="/">
            <HomePage/>
          </Route>
          <Route exact path="/login">
            <LoginPage/>
          </Route>
        </Switch>
      </Router>
    </Root>
  );
}
export default App;

先來切板外加整合 react router

點了之後到別的頁面去 => link

改變 active 就需要拿到現在的路徑 => 應該有內建的方式,目前拿 react router 進行判斷

import React, {useEffect, useState} from 'react'
import {Link, useLocation} from 'react-router-dom'
import styled from 'styled-components'
const HeaderContainer = styled.div`
  height: 64px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  border-bottom: 1px solid rgba(0, 0, 0, 0.2);
  padding: 0 32px;
  box-sizing: border-box;
`
const Brand = styled.div`
  font-size: 32px;
  font-weight: bold;
`
const NavbarList = styled.div`
  display: flex;
  align-items: center;
  height: 64px;
`
const Nav = styled(Link)`
  color: black;
  text-decoration: none;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  box-sizing: border-box;
  width: 100px;
  cursor: pointer;
  ${props => props.$active && `
    background: rgba(0, 0, 0, 0.1);
  `}
`
const LeftContainer = styled.div`
  display: flex;
  align-items: center;
  ${NavbarList} {
    margin-left: 64px;
  }
`
export default function Header() {
  const location = useLocation()
  return (
    <HeaderContainer>
        <LeftContainer>
        <Brand>我的第一個部落格</Brand>
        <NavbarList>
            <Nav to="/" $active={location.pathname === '/'}>首頁</Nav>
            <Nav to="/new-post" $active={location.pathname === '/new-post'}>發布文章</Nav>
        </NavbarList>
        </LeftContainer>
        <NavbarList>
            <Nav to="/login" $active={location.pathname === '/login'}>登入</Nav>
        </NavbarList>
    </HeaderContainer>
  );
}

實作文章列表頁面

src 裡面新增 WebAPI.js 的檔案

const BASE_URL = 'https://student-json-api.lidemy.me'
export const getPosts = () => {
    return fetch(`${BASE_URL}/posts?_sort=createdAt&_order=desc`)
        .then((res) => res.json())
}

處理 HomePage.js

import React, {useEffect, useState} from 'react'
import styled from 'styled-components'
import {getPosts} from '../../WebAPI'
import PropTypes from 'prop-types'
import {Link} from 'react-router-dom'
import {
  HashRouter as Router,
  Switch,
  Route
} from 'react-router-dom'
const Root = styled.div`
  width: 80%;
  margin: 0 auto;
`
const PostContainer = styled.div`
  border-bottom: 1px solid rgba(0, 12, 34, 0.2);
  padding: 16px;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
`
const PostTitle = styled(Link)`
  font-size: 24px;
  color: #333;
  text-decoration: none;
`
const PostDate = styled.div`
  color: rgba(0, 0, 0, 0.8);
`
function Post({post}) {
  return (
    <PostContainer>
      <PostTitle to={`/posts/${post.id}`}>{post.title}</PostTitle>
      <PostDate>{new Date(post.createdAt).toLocaleString()}</PostDate>
    </PostContainer>
  )
}
Post.propTypes = {
  post: PropTypes.object
}
export default function HomePage() {
  const [posts, setPosts] = useState([])
  useEffect(() => {
    getPosts().then(posts => setPosts(posts))
  }, [])
  return (
    <Root>
      {posts.map((post) => (<Post post={post}/>))}

    </Root>
  );
}

練習:實作單一文章頁面

新增一個 route 去接收這個路徑,根據這個路徑 render 一個 post page 的 component,裡面再 call api 把東西拿出來

hint:useParams
作業

練習:發文功能

hint:要帶正確的 header,server 才能辨識身分
確定發文成功,將使用者導回首頁,發一個新的 request 去拿 post,所以可以看到自己發的 post

登入功能講解加實作(上)

以往未寫 SPA 以前,身分驗證會用 COOKIE

  1. 登入打 API 到 server,server 確定帳密 ok 就回傳一個 set-cookie 的 http response header
  2. 驗證身分時打 API get /me 瀏覽器幫我們將 cookie 帶上去,若 session id 正確,server 回傳使用者資料,若錯誤就回傳錯誤信息

SPA 比較少用 cookie 做驗證!

也可以透session id 的方式完成身分驗證,不透過 cookie 帶而是透過瀏覽器存在 local storage 裡面,每次發一個 request 就自己帶上去

  1. 登入完後拿到 json web token (JWT),在 server 端產生 JWT,client 端就將這個 token 存在 local storage 裡面
  2. 打 API,header 帶 JWT 到 server,server 確認正確就將資料回傳給你

JWT 的 token 有經過 base64 編碼、數位簽章等等,所以雖然可以看到 token 內容但無法偽造 token。另外不要將敏感資訊存在裡面。這邊存個 username / userid 就好了

WebAPI 新增兩個 API

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())
}

export const getMe = () => {
    const token = localStorage.getItem('token')
    return fetch(`${BASE_URL}/me`, {
      headers: {
        authorization: `Bearer ${token}`,
      },
    }).then((res) => res.json())
}

react 裡面 value 是 undefined 就相當於是沒傳

建立 utils.js,避免打錯字造成的錯誤

const TOKEN_NAME = 'token'
export const setAuthToken = (token) => {
  localStorage.setItem(TOKEN_NAME, token)
}
export const getAuthToken = () => {
  return localStorage.getItem(TOKEN_NAME)
}

透過 react router 的 history 導回首頁

import React, {useEffect, useState} from 'react'
import styled from 'styled-components'
import {
  HashRouter as Router,
  Switch,
  Route
} from 'react-router-dom'
import {login} from '../../WebAPI'
import {setAuthToken} from '../../utils'
import {useHistory} from 'react-router-dom'
const ErrorMessage = styled.div`
  color: red;
`
export default function LoginPage() {
  const [username, setUsername] = useState("")
  const [password, setPassword] = useState("")
  const [errorMessage, setErrorMessage] = useState("")
  const history = useHistory()
  const handleSubmit = (e) => {
    setErrorMessage(null)
    login(username, password).then((data) => {
      if (data.ok === 0) {
        return setErrorMessage(data.message)
      }
      const token = data.token
      setAuthToken(token)
      history.push('/')
    })
  }
  return (
    <form onSubmit={handleSubmit}>
      <div>
        username: <input value={username} onChange={e => setUsername(e.target.value)}/>
      </div>
      <div>
        password: <input type="password" value={password} onChange={e => setPassword(e.target.value)}/>
      </div>
      <button>登入</button>
      {errorMessage && <ErrorMessage>{errorMessage}</ErrorMessage>}
    </form>
  );
}
  1. 登入成功後,登入按鈕應該變成登出
  2. 需要一個地方存登入狀態
  3. 拿到 token 不算成功,要 getMe 才算成功,要將使用者資料存到所有 component 都能存取的地方,要存到 App.js 這層。hint:context

登入功能講解加實作(下)

因為 contexts 別的 component 也會用到,src 底下建立 contexts.js

部落格實戰總結

  1. 學習資料夾結構設計,資料夾名稱小寫、component 名稱大寫
  2. component 裡面 call api
  3. react 中做身分驗證、登入登出相關功能
  4. deploy






Related Posts

為什麼你不應該自己組 query string

為什麼你不應該自己組 query string

做個載入更多按鈕

做個載入更多按鈕

W11_SQL Injection 相關

W11_SQL Injection 相關






Comments