可以看到由create-react-app創建的檔案結構大致會如下
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2022/7/24 下午 10:57 node_modules
d----- 2022/7/24 下午 10:57 public
d----- 2022/7/24 下午 10:57 src
-a---- 2022/7/24 下午 10:57 310 .gitignore
-a---- 2022/7/24 下午 10:57 1177487 package-lock.json
-a---- 2022/7/24 下午 10:57 811 package.json
-a---- 2022/7/24 下午 10:57 3359 README.md
React網頁的進入點會在public/index.html,他會去呼叫src/index.js
index.js又會去使用App.js當作最上層的component
底下是src資料夾下有哪些預設的檔案
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2022/7/24 下午 10:57 564 App.css
-a---- 2022/7/24 下午 10:57 528 App.js
-a---- 2022/7/24 下午 10:57 246 App.test.js
-a---- 2022/7/24 下午 10:57 366 index.css
-a---- 2022/7/24 下午 10:57 535 index.js
-a---- 2022/7/24 下午 10:57 2632 logo.svg
-a---- 2022/7/24 下午 10:57 362 reportWebVitals.js
-a---- 2022/7/24 下午 10:57 241 setupTests.js
三大前端框架的使用方式和純用JS或是Jquery撰寫網頁時有個明顯的差異
那就是他們都用上了component的概念,和過往每個頁面都需要獨立html不同
每當使用者和網頁互動,只要render畫面上有異動的部分

可以把用不到的像是logo等等給移除,react支援hot-reload所以改的同時就能看到效果
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<a className="App-link" href="https://reactjs.org"
target="_blank" rel="noopener noreferrer">
Hello World
</a>
</header>
</div>
);
}
export default App;
JSX就是前面看到的JS和html tag混用後的產物
因為JSX 語法是特殊的語法擴充,所以一般的瀏覽器是沒辦法辨識 JSX 語法的
需要像是 Babel 等類似工具來轉譯成瀏覽器可以理解的 JavaScript
在使用create-react-app創建專案時就已經把babel設定好了
function PersonOne() {
return (
<div>
<h1>Bill</h1>
<p>Your Age: 26</p>
</div>
);
}
function formatName(user) {
return user.firstName+ ' ' + user.lastName;
}
const user = {
firstName: 'Harper',
lastName: 'Perez'
};
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
const className = 'container'
const content = 'Cool! This is JSX syntax'
const element = <div className={className}>{content}</div>
Fragment是非常實用的小技巧,可以省去return時會需要的最外層<div>
import { Fragment } from "react";
function PersonOne() {
return (
<Fragment>
<h1>Bill</h1>
<p>Your Age: 26</p>
</Fragment>
);
}
Fragment也可以縮寫成<> </>
現在啟動專案時給的範例就是functional component
function App() {
return (
<div className="App">
<img src={logo} className="App-logo" alt="logo" />
<p>This is my first React App!</p>
</div>
);
}
要用時也非常方便
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
早期多會使用class componet來實作,不過像在通常會轉向更好學的React Hooks
// car.js
class Car extends React.Component {
constructor() {
super();
this.state = {color: "red"};
}
render() {
return <h2>I am a Car!</h2>;
}
}
// app.js
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Car />);
看完前面單一個component的運作方式後,接下來要講比較複雜的child component概念
現在主流的前端框架邏輯都是將畫面上各個元素拆分成不同component來去實作
有時候會遇到一個比較大的部分就會需要再去切分成多個component
像是如果你有一個比較複雜的Nav-Bar,就可以把其中的元素給拆分開
接下來我們會示範最簡單的用法,在現有的App component下建立一個component
這樣App component實際上就變成了parent
Component的檔案與function命名會有以下的限制
// App2.js
function App2() {
return (
<div>
<p>I am child component!</p>
</div>
);
}
export default App2;
只要在App.js裡面加上<App2 />就可以了
預設設定好的test framework也是jest,之後會教更好用的cypress
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getAllByText(/I am/i); // screen.getAllByText(/I am/i)[0]
expect(linkElement).toBeInTheDocument();
});
由於用的是jest,自然輸出的格式也會看起來差不多
PASS src/App.test.js
√ renders learn react link (7 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.463 s, estimated 1 s
Ran all test suites.
在不同component之間傳遞資料,主要是由父傳給子(接受到的props是read-only的)
// App2.js
function App2(props) {
return <h1>Hello, {props.name}</h1>;
}
在parent component中的呼叫方式
// App1.js
let props = {
name: 'Wade'
}
// inside return()
<App2 name='John' />
<App2 {...props}/>
處理的是component內部的狀態,當props或是state更新都能觸發元件re-render
// clock.js
import React from 'react'
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
export default Clock
早期講到class component + state就必須提到的react的lifecycle
不然就會像前一頁的範例一樣,時鐘不會自動更新時間
Class Component外另一種更方便管理state和lifecycle的寫法
useState是最常用到的hook,可以存跟改變state
import { useState } from "react";
function FavoriteColor() {
const [color, setColor] = useState("red");
return (
<>
<h1>My favorite color is {color}!</h1>
<button
type="button"
onClick={() => setColor("blue")}
>Blue</button>
</>
)
}
useEffect是設計用來處理side effect (在render後才做事),包含call API等
useEffect會吃兩個參數,第一個是函式,第二個則是dependency array
const Counter = () => {
const [count, setCount] = useState(0)
useEffect(()=>{
// 第一次 render 後會執行以外,每當 count 變數的值有變動時執行
console.log('No.3')
}, [count])
const increaseHandler = () => {
setCount(prev => prev + 1)
}
return (
<div>
<span>{count}</span>
<button onClick={increaseHandler}></button>
</div>
)};
useEffect根據帶入的參數不同,總共會產生四種不同的情境
import { useEffect } from "react";
useEffect(()=>{
// 每一次 render 後會執行
})
useEffect(()=>{
// 只有在第一次 render 後執行
}, [])
useEffect(()=>{
// 第一次 render 後會執行以外,每當 count 變數的值有變動時執行
}, [count])
useEffect(()=>{
// 第一次 render 後執行以外,每當 count 變數的值有變動時執行,但會先等 return 的函式執行完後才會執行這裡
return ()=>{
// 第一次 render 不執行,第二次重新 render 後先執行這個函式
}
}, [count])
axios是一個常見能夠發http request的套件,需要安裝npm i axios
import axios from 'axios'
const Home = props => {
useEffect(() => {
axios.get('http://localhost:8000/api/hello')
.then(res => setState(res.data))
}, [])
const [state, setState] = useState('')
return(
<div>
Home
<p>{state}</p>
</div>
)
};
透過useEffect在component剛load時去fetch,然後用setState存資料
const Home = props => {
const [state, setState] = useState('')
useEffect(() => {
fetch('http://localhost:8000/api/hello', {"method": "GET"})
.then((response) => response.json())
.then(res => setState(res.data))
}, [])
return(
<div>
Home
<p>{state}</p>
</div>
)
};
使用useState + useEffect去fetch API要小心無限迴圈
useEffect(()=>{
getPosts()
const interval=setInterval(()=>{
getPosts()
},10000)
return()=>clearInterval(interval)
},[])
讓同parent的child component可以用到同一個state
實際功用就是將其中一個child component的state變成parent component的props
// App.js
import React from "react";
export default function App() {
const [todos, setTodos] = React.useState(["item 1", "item 2", "item 3"]);
return (
<>
<TodoCount todos={todos} />
<TodoList todos={todos} />
<AddTodo setTodos={setTodos} />
</>
);
}
Child component會用到來在parent component的useState去更新其state
// AddTodo.js
function AddTodo({ setTodos }) {
function handleSubmit(event) {
event.preventDefault();
const todo = event.target.elements.todo.value;
setTodos(prevTodos => [...prevTodos, todo]);
}
return (
<form onSubmit={handleSubmit}>
<input type="text" id="todo" />
<button type="submit">Add Todo</button>
</form>
);
useRef讓我們可以透過hook去修改DOM node的值
// App.js
function App() {
return (
<div>
<button>To top</button>
<button>To bottom</button>
<p className='box'>
這裡要放超多文字讓滾輪出現
</p>
<button onClick={toTop}>To top</button>
<button onClick={toBottom}>To bottom</button>
</div>
)
}
透過設定css讓文字不用滿版就會有滾輪出現
/* App.css */
.box {
width: 300px;
height: 300px;
overflow: auto;
padding: 10px;
border: 1px solid black;
}
useRef會建立一個mutable的物件,之後要把mutable物件給放到要取的物件
const boxContainer = React.useRef(null)
const toTop = () => {
boxContainer.current.scrollTop = 0
}
const toBottom = () => {
// const { current: boxEl } = boxContainer
const boxEl = boxContainer.current
boxEl.scrollTop = boxEl.scrollHeight
}
要把return的element上加上ref的參數
<p className='box' ref={boxContainer}></p>
useReducer和之前介紹過的useState較為接近,也和未來要介紹的Redux比較有關
const [state, setState] = useReducer((state, newState)=>{
console.log(newState) // 印出 3
}, initialValue)
setState(3)
如果去改寫在useState寫的範例就會變成
function countReducer(state, newState){
return newState
}
const [count, setCount] = React.useReducer(countReducer, 0)
可以看到實際差異就是第一個參數要帶的是自訂的callback function
前面示範的改法雖然也會work,但通常會習慣使用dispatch的寫法
function countReducer(state, action){
switch (action.type) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
default:
throw new Error(`no ${action.type} type in counterReducer`)
}
}
const [count, dispatch] = React.useReducer(countReducer, 0)
const increaseHandler = () => {
dispatch({type: 'increment'})
}
修改完就可以去拿state.count
雖然useReducer的寫法相較於useState複雜許多,但在特定場合下卻會比較好用
而這個特定場合就是指當一個state會去影響到另外一個state(同component)
我們這裡舉的例子是有pagination + sorting需求時,先定義一下reducer
function reducer(state, action){
switch (action.type) {
case 'change sort':
const nextType = state.sortBy.type === 'desc' ? 'asc' : 'desc'
const nextSortBy = { ...action.sortBy, type: nextType }
return { ...state, page: 1, sortBy: nextSortBy }
case 'change page':
return { ...state, page: action.page }
default:
throw new Error(`no ${action.type} type in reducer`)
}
}
component中的其他部分
const App = () => {
const [posts, setPosts] = useState([])
const [payload, dispatch] = useReducer(reducer, {
page: 1,
sortBy: {
category: 'date',
type: 'desc'
}
})
function sortHandler(e){
const category = e.target.id
dispatch({type: 'change sort', sortBy: { category } })
}
function pageHandler(e){
dispatch({type: 'change page', page: e.target.id})
}
useEffect(() => {
// fetch api and setPost
}, [page, sortBy]);
// return UI .....
}
這種情況如果選用useState就會把handler搞得很複雜
const App = () => {
const [posts, setPosts] = useState([])
const [page, setPage] = useState(1);
const [sortBy, setSortBy] = useState({
category: 'date',
type: 'desc'
});
function sortHandler(e){
const sortCategory = e.target.id
setPage(1)
setSortBy(prev => {
const nextType = prev.type === 'desc' ? 'asc' : 'desc'
return {
...prev,
category: sortCategory,
type: nextType
}
})
}
function pageHandler(e){
setPage(e.target.id)
}
}
之前提Lift state的時候是假定只要將state給上層的component
假設這時候要給上上/上上上層就會變得很難寫,可以改用context來協助傳遞state
// 建立context object
const Context = createContext(defaultValue)
// Context長的樣子
<Context.Provider value={/* some value */}>
{children}
</Context.Provider>
// 取得context中的value
const value = useContext(Context)
由於這裡會牽涉的範例component過多,詳細可以參考下方的連結
這裡會先建context provider
// ThemeContext.js
const ThemeContext = createContext('light')
export function ThemeContextProvider({children}) {
const [darkTheme, setDarkTheme] = useState(false)
const theme = darkTheme ? 'dark' : 'light'
return (
<ThemeContext.Provider value={[theme, setDarkTheme]}>
<App2 />
{children}
</ThemeContext.Provider>
)
}
再來要去建useContext
// ThemeContext.js
export function useThemeContext() {
const contextValue = useContext(ThemeContext)
if (!contextValue) {
throw new Error('you should wrap the component in theme context provider')
}
return contextValue
}
會用到context的component都必須被context provider給包住
// App.js
import { Header } from './components/Header'
import { Body } from './components/Body'
import { Footer } from './components/Footer'
import { ThemeContextProvider } from './components/ThemeContext'
function App() {
return (
<ThemeContextProvider>
<Header />
<Body />
<Footer />
</ThemeContextProvider>
)
}
會取到theme value的body
// body.js
import { useThemeContext } from './components/ThemeContext'
function Body() {
const [theme] = useThemeContext()
return (
<div>
{theme}
</div>
)
}
要設定theme value的button
// button.js
import { useThemeContext } from './components/ThemeContext'
function Button() {
const [theme, setDarkTheme] = useThemeContext()
return (
<button onClick={() => { setDarkTheme(prev => !prev) }}>{theme}</button>
)
}
如果不是在最上層先call API來存資料,就可以用來把所有資料存到最上層
useCallback可以幫我們盡量的減少一些不必要的render,來增強系統的表現
useCallback和useEffect接收的參數是一樣的,就是callback function + dependency array
const memorizeCallback = useCallback(() => {
// do something
}, [])
useCallback在re-render時會根據dependency array決定要不要生成一個新的function
而useCallback回傳的memoized callback就是負責紀錄function的物件
在不使用useCallback時只有第一次render時會return True,之後就只會return False
function App() {
const [count, setCount] = React.useState(0)
const callbackRef = React.useRef()
const test = () => {
console.log('test callback')
}
React.useEffect(() => {
callbackRef.current = test
}, [])
React.useEffect(() => {
console.log(test === callbackRef.current)
}, [count])
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
)
}
如果把test的function改成用useCallback寫就會得到不一樣的效果
const test = React.useCallback(() => {
console.log('test callback')
}, [])
因為每次使用到的其實都是相同的function實體
useCallback最適合的使用時機是某個function會很常被useEffect等hook使用到時
以call API為例子,我們可能會把共用到的部分以function的形式放在hook外
function fetchData(query){
const baseUrl = `xxxxxxxx?${query}`
}
React.useEffect(()=> {
fetchData('JavaScript')
}, [fetchData])
React.useEffect(()=> {
fetchData('React')
}, [fetchData])
這時候只要每次re-render fetchData就都會是新的實體
而解決的辦法就是透過useCallback來儲存fetchData,可以避免每次re-render都產生新的實體
const fetchData = React.useCallback((query) => {
const baseUrl = `xxxxxxxx?${query}`
}, [])
React.useEffect(()=> {
fetchData('JavaScript')
}, [fetchData])
React.useEffect(()=> {
fetchData('React')
}, [fetchData])
如果再把前面的範例搭配上useState就會變成
const [query, setQuery] = React.useState('JavaScript')
const fetchData = React.useCallback((query) => {
const baseUrl = `xxxxxxxx?${query}`
}, [query])
React.useEffect(()=> {
fetchData()
}, [fetchData])
可以去模擬靜態網頁的路由關係,需要先安裝npm install react-router-dom@6
// App.jsx
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'
function App () {
return (
<>
<Routes>
<Route element={<Home />} path={'/'}></Route>
<Route element={<List />} path='/list'></Route>
<Route element={<Id />} path='/id/:id'></Route>
</Routes>
</>
)
}
routes一定要放在return內才會生效
除了安裝套件並寫routes外還要去index.js中設定啟用模擬的router
// src/index.js
import { BrowserRouter } from "react-router-dom";
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);
除了以Link的方式轉向外也能直接在網址列輸入
// App.jsx
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'
function App() {
return (
<div className="App">
<nav>
<Link to="/home">Home</Link>
</nav>
</div>
);
}
可以用來回到上一頁
import { useHistory } from "react-router-dom";
export const Profile = () => {
let history = useHistory();
console.log(history);
return (
<div>
<button onClick={() => history.goBack()}>Back</button>
<button onClick={() => history.push("/")}>Home</button>
<section>
<p>profile page</p>
</section>
</div>
);
};
和Link一樣主要用來實現路由的變換
import { useNavigate } from "react-router-dom";
const ABOUT = () => {
const navigate = useNavigate()
const onClick = () => {
navigate('/')
}
return (
<div>
<button onClick={onClick}>BACK</button>
</div>
)
}
React Router還有提供其他實用的hook或是function
import { useLocation, useParams, useSearchParams } from "react-router-dom";
const location = useLocation()
const params = useParams()
const [getParams, setParam] = useSearchParams()
const name = getParams.getAll('name')
可以幫助切分程式碼,在會被用到的時候才讀取,這樣能夠增加網頁的效能
首先需要安裝套件 npm install --save react-loadable
// App.js
import Loadable from 'react-loadable';
// 撰寫如果網頁載入較慢時要render的component
const Loading = () => {
return <div>Loading......</div>
};
// 用react-loader包裝的Home Component
const Home = Loadable({
// 要load的component,用import的方式插入componenr
loader() {
return import('./Home');
},
// 如果loading較慢時,要render的component
loading: Loading,
});
把前面的code再加上react router
function App () {
return(
<Router>
<div>
<Route path="/" exact component={Home} />
<Route path="/About" component={About} />
<Route path="/Contact" component={Contact} />
<Footer />
</div>
</Router>
);
}
確保當連線失敗或是發生錯誤時都能給予正確的提示
function Loading(props) {
if (props.error) {
return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
} else if (props.timedOut) {
return <div>Taking a long time... <button onClick={ props.retry }>Retry</button></div>;
} else if (props.pastDelay) {
return <div>Loading...</div>;
} else {
return null;
}
}
Loadable({
loader: () => import('./components/Bar'),
loading: Loading,
timeout: 10000, // 10 seconds
});
Loader.Map在loading時可以透過function做多件事
Loadable.Map({
loader: {
Bar: () => import('./Bar'),
i18n: () => fetch('./i18n/bar.json').then(res => res.json()),
},
render(loaded, props) {
let Bar = loaded.Bar.default;
let i18n = loaded.i18n;
return <Bar {...props} i18n={i18n}/>;
},
});
在背景先將component給加載好
const LoadableBar = Loadable({
loader: () => import('./Bar'),
loading: Loading,
});
function App () {
onMouseOver = () => {
LoadableBar.preload();
};
return (
<div>
<button
onMouseOver={this.onMouseOver}>
Show Bar
</button>
</div>
)
}
如果要以靜態網頁的形式去serve react專案的話,就得把原本的react-router給修正
因為網頁的內容實際上會是由backend server先render好
// App.js
import { StaticRouter } from "react-router-dom/server"
const location = {
pathname: '/about', // 路徑,這是唯一必要的
search: '?bar=1', // query 的部份
hash: '#baz', // hash 的部份
state: {}, // 物件裡可以傳任何東西
}
<StaticRouter location={location}>
<Routes>
<Route path="/about" component={About} />
</Routes>
</StaticRouter>
BrowserRouter是由前端瀏覽器渲染的,而StaticRouter則是由後端處理
改用StaticRouter的話,記得要把index.js中的BrowserRouter給移除
// <BrowserRouter>
<App />
// </BrowserRouter>
可以去存跨component的state,也需要先安裝npm install @reduxjs/toolkit
由於redux的整體非常複雜,所以官方會建議從使用包好的toolkit來下手
如果在使用create-react-app時就下定決心要用到redux的話可以下
# Redux + Plain JS template
$ npx create-react-app my-app --template redux
# Redux + TypeScript template
$ npx create-react-app my-app --template redux-typescript
核心概念是當有action在UI上被發出後得靠dispatch才能去更動store內的state

常見的會有以下:
為了要在react專案中用redux-toolkit,這裡還會需要再安裝一個叫做react-redux的套件
首先要建立一個檔案來創建並管理store
// store.js
import { configureStore } from "@reduxjs/toolkit"
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
}
})
再來還得到App.js去做設定,這樣component才可以使用到store/reducer
// App.js
import { Provider } from 'react-redux';
import { store } from './store';
import Container from './Container';
// 在component內的最外層加上
<Provider store={store}>
<Container />
</Provider>
這樣設定完才能讓所有的component都可以用上來自redux的global state
透過 slice 可以產生 reducer 和 action creators
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: { value:0 },
reducers: {
increment: (state) => ({
value: state.value + 1,
}),
decrement: (state) => ({
value: state.value - 1,
}),
},
})
// action creators
export const { increment, decrement } = counterSlice.actions
// reducer
export default counterSlice.reducer
最後用個container component當作範例來使用store中的state
// container.js
import { useDispatch, useSelector } from 'react-redux'
import { increment, decrement } from './counterSlice';
const Container = () => {
const dispatch = useDispatch();
const count = useSelector((state) => state.counter.value)
const handleIncrement = () => {
dispatch(increment())
}
const handleDecrement = () => {
dispatch(decrement())
}
return (
<div>
<h1>Count: {count}</h1>
<button onClick={handleDecrement}>Decrement</button>
<button onClick={handleIncrement}>Increment</button>
</div>
)
}
export default Container
useSelector 會將前一個結果與當前的結果進行比較
如果不同就會取得新值並強制重新render component
const counter = useSelector(state => state.counter)
AsyncThunk是redux提供專門用來處理非同步事件的,尤其像是fetch API這類很花時間的
import { createAsyncThunk } from '@reduxjs/toolkit'
export const fetchPosts = createAsyncThunk('posts/fetchByIdStatus', async (Uid, thunkAPI) => {
const body = {"Uid": Uid}
let res = await fetch('http://localhost:8000/post/view', {
method: "POST",
body: JSON.stringify(body)
})
res = await res.json()
if (res !== null)
return res.posts
else
return []
})
由於預設的reducer無法直接處理異步事件,這裡要用extraReducer來處理不同狀態
export const pageSlice = createSlice({
name: 'page',
initialState: {
posts: [],
},
reducers: {
// ...
},
extraReducers: (builder) => {
builder.addCase(fetchPosts.fulfilled, (state, action) => {
state.posts = action.payload // return { posts: action.payload }
});
},
})
通常檔案名稱取的和component一致,像是app.js搭配app.css
/* App.css */
.App {
text-align: center;
}
// App.js
import './App.css';
function App() {
return (
<div className="App">
React 101
</div>
);
}
export default App;
React-Bootstrap就是把boostrap包裝成方便React使用的版本,但是版本號碼會落後些
要使用的話需要安裝兩個套件 npm install react-bootstrap bootstrap
import Button from 'react-bootstrap/Button';
function Demo () {
return (
<Stack direction="horizontal" gap={2}>
<Button as="a" variant="primary">
Button as link
</Button>
<Button as="a" variant="success">
Button as link
</Button>
</Stack>
)
}
CSS要在index.js中設定 import 'bootstrap/dist/css/bootstrap.min.css';
PrimeReact是專為React而生的UI套件,可以透過npm i primereact primeicons安裝
import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';
function Demo () {
return (
<Dialog visible={state} onHide={() => setState(false)}>
// content
</Dialog>
<Button label="Show" onClick={() => setState(true)} />
)
}
CSS的部分要額外到index.js去設定
import "primereact/resources/themes/lara-light-indigo/theme.css"; //theme
import "primereact/resources/primereact.min.css"; //core css
import "primeicons/primeicons.css"; //icons
PrimeBlock是PrimeReact的付費版,他提供了一系列已經設計好的版型
其中有80種是可以免費體驗使用的,要在額外安裝套件npm install primeflex --save
使用createElement來建立React element
const rootEl = document.createElement('div')
document.body.append(rootEl)
// 寫法一
const element = React.createElement('div', {
children: 'Hello React!',
className: 'container'
})
// 寫法二
const element = React.createElement(
'div',
{ className: 'container' },
'Hello world'
)
ReactDOM.render(element, rootEl)
這部分會比較複雜,但能夠更進一步的了解useState實際的運作方式
可以更方便的將尚未登入的user給導向keycloak登入頁面
要使用react-keycloak這個套件會需要安裝npm install @react-keycloak/web keycloak-js
首先要去設定要連線到的keycloak server的相關資訊
// keycloak.js
import Keycloak from 'keycloak-js'
const keycloak = new Keycloak({
realm: "gatsby", // realm as configured in Keycloak
url: "http://localhost:8080/auth/", // URL of the Keycloak server
clientId: "gatsby-ui", // client id as configured in the realm in Keycloak
})
export default keycloak
透過ReactKeycloakProvider可以讓React吃到keycloak-js中的連線設定
// App.js
import React from 'react'
import { ReactKeycloakProvider } from '@react-keycloak/web'
import keycloak from './keycloak'
const Loading = () => <div>Loading...</div>
export const wrapRootElement = ({ element }) => {
return (
<ReactKeycloakProvider
authClient={keycloak}
initOptions={{
onLoad: "login-required",
}}
LoadingComponent={<Loading />}
>
{element}
</ReactKeycloakProvider>
)
}
useKeycloak這個hook可以讓我們方便的去取得登入狀態
// Home.js
import React from "react"
import { useKeycloak } from '@react-keycloak/web'
const Home = () => {
const { keycloak, initialized } = useKeycloak()
return (
<>
<div>The user is {keycloak.authenticated ? '' : 'NOT'} authenticated</div>
{keycloak.authenticated && (
<button type="button" onClick={() => keycloak.logout()}>
Logout
</button>
)}
</>
)
}
export default Home
連線到有受到keycloak認證保護的backend server
// User.js
import React, {useEffect, useState} from 'react'
import axios from 'axios'
import {useKeycloak} from '@react-keycloak/web'
const Users = () => {
const {keycloak, initialized} = useKeycloak()
const [users, setUsers] = useState([])
//load users when the component loads
useEffect(() => {
//call your backend api to load users
axios.get("http://YOUR_BACKEND/users", {
headers: {
//put the keycloak access token in the Authorization header
'Authorization': `Bearer ${keycloak.token}`
}
}).then((response) => {
setUsers(response.data)
})
}, [])
//render users...
}
之後的檔案架構會類似底下
├─ backend/
├─ Dockerfile
├─ docker-compose.yml
└─ frontend/
├─ src/
├─ public/
└─ build/
之前所寫的Dockerfile只有考慮到後端code的部分,這次會包含到build好的前端
RUN cd frontend && npm i && npm run build
RUN cd backend && npm i
WORKDIR /app/backend
不要用COPY * /app,不然會破壞掉原有的資料夾結構
由於fastify-static只能使用絕對路徑,因此我們要把code做一些小調整
讓不論在container內或是開發時都可以吃到正確的路徑
// server.js
const path = require('path')
fastify.register(require('@fastify/static'), {
root: path.join(__dirname, '../frontend/build'), // must use absolute path
})
再來就可以用docker build -t johnny12150/first_mern .來build image
這裡會用docker-compose.yml來設定我們要一次啟動的container
由於keycloak會需要一些手動設定無法在container一跑起來就用,這裡會先忽略他
# docker-compose.yml
version: "3"
services: # define containers
frontend: # container name
image: johnny12150/first_mern
ports: # expose ports
- "8000:8000"
environment:
# - MONGO_HOST=db
- MONGO_HOST=host.docker.internal
用docker compose up -d可以同時啟動剛剛定義的兩個container
用docker compose down -v可以刪掉剛剛的container並清除volume
# docker-compose.yml
# previous page...
db:
image: mongo:5.0.9
ports:
- "27018:27017"
environment: # env variables
- MONGO_INITDB_ROOT_USERNAME=mongoadmin
- MONGO_INITDB_ROOT_PASSWORD=secret
如果遇到原本react router用的url會找不到,就得去修改後端code
fastify.setNotFoundHandler(function (request, reply) {
reply.sendFile('index.html')
})
這樣會讓所有endpoint找不到時都被重新導向前端