[框架課程] React v18 教學(三)- 動態路由

隨著 React 的了解越多,我們需要大量練習於不同頁面。為了更好的統一實作於同一份專案上,我們需要額外使用 React 生態下的另一個特別套件 React Router。這是一個能夠很好的動態出不同路由有不同的 React 檔案,但本體網頁上還是同一份 SPA 下的渲染差異輸出。

React Router v7

React 作為 SPA(Single-Page Application,單頁應用)的用途之一,能夠在一個 HTML 文件中動態渲染多個不同的畫面內容。這是透過 React 指定的一組 DOM 元素進行動態更新,從而實現畫面的變換。如果需要在多個不同的畫面中展示各自的 React 內容,傳統的方法是創建多個 HTML 文件,每個 HTML 文件下都有其對應的 ReactDOM 實例。然而,這種方法會導致應用程序的複雜度增加,維護成本提高。

為了解決這個問題,可使用 ReactRouter 是一種動態路由的延伸套件,能夠在一個 HTML 文件上實現多個畫面的動態路由切換。它允許在同一個 HTML 文件中,透過虛擬的動態路由,實現大畫面的渲染替換。這種方法的優點是,實際上仍然是同一份 HTML 文件進行替換,減少了應用程序的複雜度,提高了應用程序的可維護性和性能。

在 React Router v7 中,發生了顯著的變革。相比過去多提供了 SSR 環境的 framework。library 方式適合小型 React SPA 專案,能夠輕鬆地添加路由功能。然而,隨著專案規模的擴大,許多團隊開始選擇使用 Next.JS 或 Remix 等 framework 來部署新專案。這些 framework 使用 React 作為編寫語言,並提供了完整的專案環境系統,包括動態路由功能等。因此,選擇 Next 或 Remix 的團隊不會特地的額外安裝使用 React Router。從第七版開始,React Router 提供了 framework 方式的支持,保留了 library 方式。

根據目前的 7.1.4 版本來說與未來性考量。我們將選擇 library 作為 SPA 專案的傳統選擇,跟隨以下方式完成安裝設定:

pnpm add react-router

調整 main.jsx

接著來到最上層元件調整,利用 BrowserRouter 包覆整個 App,使得整個 APP 都是被 BrowserRouter 所影響判定渲染。

  • 使用BrowserRouter將要規劃路由的根元件包覆。
  • 修改 css 檔案名稱與路徑其一致,更改index.cssmain.css,調整部分 css 代碼
src\main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router';
import './main.css';
import App from './App';

createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
);
src\main.css
:root {
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
}

* {
box-sizing: border-box;
}

body {
padding-left: 300px;
margin: 0;
}

a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}

h1 {
font-size: 3.2em;
line-height: 1.1;
}

button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.5rem 1rem;
font-size: 1rem;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;

&:hover {
border-color: #646cff;
}

&:focus,
&:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
}

@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

複製 App.jsx 成為子路由 base.jsx

原本的 App 元件保留下來另外下放到路由元件,之後重新將 App 元件作為我們路由主要處理的單元並配置路由。

  • 修改 App.css 命名為 base.css
  • 複製 App.jsx 為 base.jsx,注意調整部分命名
  • 將相關的 component 目錄、jsx、css 都搬移到 pages/lesson01 目錄下,整理目錄也很重要。
src\pages\lesson01\base.jsx
import { useState } from 'react';
import './base.css';
import MyLogo from './component/MyLogo/MyLogo';
import MyH1 from './component/MyH1/MyH1';
import MyButton from './component/MyButton/MyButton';
import MyForm from './component/MyForm/MyForm';
import MyGallery from './component/MyGallery/MyGallery';

export default function Base() {
const [count, setCount] = useState(0);
const [toShow, setToShow] = useState(true);

const h1Title = 'Vite + React';

const onPasswordSubmit = (e) => {
e.preventDefault();
console.log('submit');
};
const onPasswordChange = (e) => {
console.log(e.target.value);
};

return (
<>
<MyLogo />
<MyH1>{h1Title}</MyH1>
<MyGallery toShow={toShow} setToShow={setToShow} />
<MyGallery {...{ toShow, setToShow }} />
<div className="card" style={{ color: 'red', background: 'black' }}>
<MyForm onLokiSubmit={onPasswordSubmit} onLokiChange={onPasswordChange} />
<MyButton>Click Me!</MyButton>
<button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
<p>
Edit <code>src/App.jsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">Click on the Vite and React logos to learn more</p>
</>
);
}
src\pages\lesson01\base.css
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}

.card {
padding: 2em;
}

.read-the-docs {
color: #888;
}

調整 App.jsx 為路由動線

現在 App.jsx 可以整個重新改寫,主要需要透過渲染進行配置並將 URL 段與 UI 元素耦合。

  • Routes 為所有路由的集合群組,任何指定的 path 都會在這層切換。
  • Route 為單一路由,他可以巢狀的疊合下層子路由。在這個設計來說,我們使用一個沒有 path 屬性作為父路由,也就是這三個子 Route 都會吃到父路由的 UI。
  • 希望沒輸入網址以及 base 都是指向到 Base 元件。
  • Layout 會作為分割畫面使用,左側拿來用路由導向連結,右側為路由變化的 DOM 替換。稍後解釋。
  • 為了測試,除了剛搬移過的 Base 元件,多規畫一個 MyGallery 元件做稍後的路由測試準備。
src\App.jsx
import { Routes, Route } from 'react-router';
import Layout from './template/layout';
import Base from './pages/lesson01/base';
import MyGallery from './pages/lesson01/component/MyGallery/MyGallery';

export default function App() {
return (
<Routes>
<Route element={<Layout />}>
<Route index element={<Base />} />
<Route path="base" element={<Base />} />
<Route path="gallery" element={<MyGallery />} />
</Route>
</Routes>
);
}

設計 Layout 元件

Layout 作為整個路由模板,希望換頁面只有右側區域。

  • 左側使用 Link 方式指定一個 url 位置。
  • 右側需要設定一個 Outlet 出口位置,其根據 App.jsx 路由的設定,該三個子路由都會在這裡切換畫面內容。
  • 同時需要花心思設計一下 css,把 layout 相關檔案放在 template 目錄,分類整理目錄。
src\layout\template.jsx
import { Link, Outlet, NavLink } from 'react-router';
import './layout.css';

export default function Layout() {
return (
<>
<nav>
<h2>選單</h2>
<ul>
<li>
<NavLink to="/base">基礎學習</NavLink>
</li>
<li>
<NavLink to="/gallery">幻燈片</NavLink>
</li>
</ul>
</nav>

<main>
<div className="container">
<Outlet />
</div>
<footer>本專案為 Loki Jiang 課程教材使用</footer>
</main>
</>
);
}

src\template\layout.css
nav {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 300px;
background: #f4f4f4;
padding: 20px;
overflow-y: auto;

ul {
padding-left: 20px;

a {
font-weight: bold;
display: block;

&:hover {
color: #f65252;
}

&.active {
background: #4dacff;
color: white;
}
}
}
}

main {
padding: 20px;
position: relative;
min-height: 100vh;

.container {
max-width: 960px;
margin: 0 auto;
}

footer {
background: #343434;
color: white;
text-align: center;
position: absolute;
left: 0;
right: 0;
bottom: 0;
}
}

檢查

請檢查你的目錄結構是否如下,並點選檢查畫面是否如路由導向拿到指定畫面。包含http://localhost:5173/http://localhost:5173/gallery


React Router 還有許多完整功能與實用設計,可以自行參考官方文件。例如:

  • 如何讀取 useParams 值,例如 http://localhost:5173/blog/1 情況下取得 id=1 對 API 發送資料請求取得文章 1 的資料。
  • 所有未匹配的路徑下,拜訪指定的 404 網頁顯示錯誤。
  • 使用 useNavigate 在 js 代碼下進行跳轉路由跳轉。

參考文獻