做部落格之前,先來認識 react router
前端處理換頁路徑,換頁不是發 requset 到 server 去。前端透過 js history API 改變網址列上的內容,react router 背後就是使用這個原理在做。
用 component 方式表現 router 要怎麼設計
- npm install react-router-dom
Router 有兩個方式
- BrowserRouter:路徑是後面直接帶。缺點:github page 上面當我打這個路徑,他會去找這個資料夾底下的 index.html,試著把他載入。因為現在是靠前端換頁,所以我是用 js api 去更改網址。但如果今天是將網址複製貼上按 enter,就不是透過 JS,瀏覽器會直接發 request 到這個地方去。如果是這種方式用到 github page 上面去,就會沒有東西他會以為你要找這個資料夾底下的 index.html.
- 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
- 登入打 API 到 server,server 確定帳密 ok 就回傳一個 set-cookie 的 http response header
- 驗證身分時打 API get /me 瀏覽器幫我們將 cookie 帶上去,若 session id 正確,server 回傳使用者資料,若錯誤就回傳錯誤信息
SPA 比較少用 cookie 做驗證!
也可以透session id 的方式完成身分驗證,不透過 cookie 帶而是透過瀏覽器存在 local storage 裡面,每次發一個 request 就自己帶上去
- 登入完後拿到 json web token (JWT),在 server 端產生 JWT,client 端就將這個 token 存在 local storage 裡面
- 打 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>
);
}
- 登入成功後,登入按鈕應該變成登出
- 需要一個地方存登入狀態
- 拿到 token 不算成功,要 getMe 才算成功,要將使用者資料存到所有 component 都能存取的地方,要存到 App.js 這層。hint:context
登入功能講解加實作(下)
因為 contexts 別的 component 也會用到,src 底下建立 contexts.js
部落格實戰總結
- 學習資料夾結構設計,資料夾名稱小寫、component 名稱大寫
- component 裡面 call api
- react 中做身分驗證、登入登出相關功能
- deploy