---
# System prepended metadata

title: Building a Muti-page SPA with React Router
tags: ['Javascript, React']

---

# Building a Muti-page SPA with React Router

###### tags: `Javascript, React`

# Module Content

Add Mutiple Pages in SPA

What is Client-Side Routing (third party package)

Using React-Router

Advanced Features: Dynamic & Nested Routes 


# What is Routing ?

> Routing 指的是藉由不同的 URL path 可以看到不同的內容呈現在網頁中


## Muti-page routing in conventional way


在現實世界中， URL path 的改變意味著我們可以看見的內容也跟著改變，我們藉著不同的  path 載入不同的頁面，並且每一個 page 都有自己的 path 它們組合成我們看到的網頁

![](https://i.imgur.com/nws3X8M.png)

而傳統的 muti-page routing ，每次換不同的 path 都會需要跟 server 重新 request 一次並且重新下載網頁內容，這樣的速度會比較慢也不是這門課的重點

![](https://i.imgur.com/DeSoWtI.png)


為什麼要把 Routing 加進去 React Applicaiton 裡面?

因為傳統的方式需要重新送出 request 以及下載網頁內容速度比較慢



## Build SPAs with Muti-page Routing

把 Muti-page Routing 使用在 SPA 內

1. 只會需要一個 html 以及一次的 request 
2. 頁面的改變（URL 改變） 會在 client-side（React） 執行
3. 改變頁面不需要重新 fetch 新的 html 檔案

![](https://i.imgur.com/HuwAl4e.png)



## 使用 third-party package  React Router

[react router 官網](https://reactrouter.com/)
[react router github](https://github.com/ReactTraining/react-router)

安裝 React Router

進入專案資料夾後

`npm install react-router-dom`

# React-Router

## Defining & Using Routes

> 所謂使用 Routes 代表可以藉由切換 URL 後方的參數來切換呈現的 component 
```
our-domain.com/ => component A
our-domain.com/product => component B
```

這邊我會使用兩個簡單的頁面來呈現不同的 path 呈現不同的內容

分別是 Welcome.js/ Products.js

操作方式如下：

### index.js

在裡面引入了 BrowserRouter 並且包住 App

```javascript=
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom'
import './index.css';
import App from './App';

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);
```

### Welcome.js/ Products.js

Products.js 內容一樣只是換字樣

```javascript=
const Welcome = ()=>{
  return <h1>The Welcome Page</h1>;
}

export default Welcome;
```

### App.js

1. 引入 Route 包裹住想要呈現的 components
1. 並且帶入特殊的 property path 並且輸入 path 名稱，就可以在 URL 上面使用並且找到該內容摟！

```javascript=
import {Route} from 'react-router-dom';
import Products from './components/Products';
import Welcome from './components/Welcome';

export default function App() { 

  return (
   <div>
     <Route path='/welcome'>
       <Welcome></Welcome>
     </Route>
     <Route path='/products'>
       <Products></Products>
     </Route>
   </div>
  );
}
```

經過上面的操作之後就可以操作 URL 的 path  來做到更換 component 的功能摟！

![](https://i.imgur.com/hXIiXIg.png)

![](https://i.imgur.com/e0UShSx.png)


## Working with Links

正常來說使用者操作網頁並不會自己手動更改 URL path 而是透過點擊 Links 的方式

```javascript=
import {Route} from 'react-router-dom';
import Products from './pages/Products';
import Welcome from './pages/Welcome';
import MainHeader from './components/MainHeader';

export default function App() { 

  return (
   <div>
     <MainHeader></MainHeader>
     <main>
      <Route path='/welcome'>
        <Welcome></Welcome>
      </Route>
      <Route path='/products'>
        <Products></Products>
      </Route>
     </main>
   </div>
  );
}
```

```javascript=
const MainHeader = () =>{
  return (
    <header>
      <nav>
        <ul>
          <li>
            <a href ='/welcome'>Welcome</a>
          </li>
          <li>
            <a href ='/products'>Products</a>
          </li>
        </ul>
      </nav>
    </header>
  )
}
export default MainHeader;
```
現在可以透過這兩個連結來切換 components 了
![](https://i.imgur.com/JsxFveY.png)


但是這個方法有個缺陷

就是他會重新發出 request 給 server 並且會重新載入頁面，這樣一來就不是 SPA 並且沒有達到我們想要的效果解決的方式：

 加入 muti-pages 進去 SPA

我們可以引入 Link component 從 react-router-dom ，並且使用其特殊的 property 'to' 來導向不同的 path

```javascript=
import { Link } from "react-router-dom";

const MainHeader = () =>{
  return (
    <header>
      <nav>
        <ul>
          <li>
            <Link to ='/welcome'>Welcome</Link>
          </li>
          <li>
            <Link to ='/products'>Products</Link>
          </li>
        </ul>
      </nav>
    </header>
  )
}
export default MainHeader;
```

如此一來頁面就算切換了也不需要跟 server 重新做出 request 摟

（下方圖片展示就算已經切到 products 頁面 下方的載入狀態沒有更新一樣停在 welcome 代表沒有觸發新的頁面只是切換 components 而已）

![](https://i.imgur.com/kIsAznR.png)

所以 react-router-dom Link component 這邊做了幾件事情

1. 停止頁面的 default 讓頁面不會刷新
2. 監聽 click 事件
3. 並且幫助我們更新 URL path

## Using NavLinks

我們可以使用 NavLink 來實作一些小的 UI 更完善使用者體驗

引入使用 NavLink 可以使用它特殊的 property : activeClassName 並且來擷取 css 檔案中針對 active 的效果加在 Link 上面，相當方便

實際使用只要使用 css 檔名加上 active 字樣即可

```JSX=
<li>
    <NavLink activeClassName={classes.active} to ='/welcome'>Welcome</NavLink>
</li>
<li>
    <NavLink activeClassName={classes.active} to ='/products'>Products</NavLink>
</li>
```

css 設定

撰寫的時候記得要寫 a.active 

```css=
.header a.active {
  color: #95bcf0;
  padding-bottom: 0.25rem;
  border-bottom: 4px solid #95bcf0;
}
```

實際效果

當停留在其中一個頁面時，就會順利的有出現 active 特效摟

![](https://i.imgur.com/tJ4m7w3.png)

## Adding Dynamic Routes with Params


一般我們使用 products page 都會有商品清單，並且商品清單每個都可以個別點擊，並且分別進去不同的 product detail 頁面

![](https://i.imgur.com/V0sDi2C.png)

點書本會出現書本介紹
點球會出現球的介紹等等

### ProductDetail.js

這邊是 detail 頁面主要呈現，當點擊 product page 的 link 時的跳轉頁面

```javascript=
 const ProductDetail = () =>{
   return (
     <section>
       <h1>
         Product Detail
       </h1>
     </section>
   )
 };
 export default ProductDetail;
```

為了把這些 li 變成 link 必須先把他們註冊成 Routes ，然而至這邊的 path 會是重點，因為我們必須呈現點擊出現的 detail 頁面會跳出對應的商品 detail 這個時候就可以使用 Dynamic routes  來處理摟！

使用 /: 後面的識別器（identifier）可以自訂（這邊我取名 productId），並且裡面放入任何內容都可以，到達 ProductDetail 頁面

並且我們可以在 ProductDetail 頁面抓取資料不管是從後端或是資料庫 

```javascript=
<Route path='/product-detail/:productId'>
        <ProductDetail></ProductDetail>
</Route>
```

### useParams

為了對應動態的 identifier 並連結到對的商品，我們會在 ProductDetail.js 頁面使用 useParams 這個由 react-router-dom 提供的方法來對應

這邊我們就可以抓取到我們設置的 identifier 也就是 productId 並且呈現到我們的頁面上，不過真實使用上面則會把抓到的 identifier 用去抓取正確的 API 內容

```javascript=
 import {useParams} from 'react-router-dom'
 
 const ProductDetail = () =>{

  const params = useParams();
   return (
     <section>
       <h1>
         Product Detail
       </h1>
       {params.productId}
     </section>
   )
 };
 export default ProductDetail;
```

## Using 'Switch' and 'Exact' for Configuring

> URL 的結構基本上就像是資料夾的結構

所以像是商品 detail 頁面的 URL 會長這樣

`http://domainname/product/productname`

這邊我們針對 product page 的 li 放入連結並且設定好 to 的路徑

```javascript=
<li><Link to='/products/p1'>a book</Link></li>
<li><Link to='/products/p2'>a thing</Link></li>
<li><Link to='/products/p3'>a ball</Link></li>
```

照理來說就會在我們點擊連結時，跳出 product detail 頁面對嗎？

是跳出來了沒錯，但他們怎麼出現在同一頁呢？

(如果你要設置他們都出現在同一頁可以這樣使用)

![](https://i.imgur.com/rnZ78RA.png)

因為 react-router 的辨識 path 的方式是只要符合路徑的內容都會一次被呈現在頁面上面，所以 /product/p1 符合了但是 /product 也符合喔！所以他們就一起呈現了

### 這時候 Swtich  react-router-dom 的方法就可以登場了！

用它包住所有的 Routes 就可以達到一次只在頁面上呈現一個 component 的效果摟

```JSX=
<Switch>
        <Route path='/welcome'>
          <Welcome></Welcome>
        </Route>
        <Route path='/products'>
          <Products></Products>
        </Route>
        <Route path='/products/:productId'>
          <ProductDetail></ProductDetail>
        </Route>
</Switch>
```

可是目前這樣的操作卻還是在點擊連結之後頁面並沒有跳轉到 detail 畫面，卻是停留在原地，原因是 react-router 辨識 path 是由上到下一個一個確認，一命中就停止所以我們可以看到 products 的順序比 products/:productId 還要前面所以直接被選走了，因此我們點擊連結畫面也沒有跳轉

### exact 就是這個時候可以用摟！

這個特殊的屬性是為了確保 path 一定要完全一樣才會通過 switch 所以現在，就算當下的路徑是 /products/p1 他也不會因為 /product 的順序比較靠前就呈現而是可以正常顯示 product detail 的頁面摟！

```JSX=
<Switch>
        <Route path='/welcome'>
          <Welcome></Welcome>
        </Route>
        <Route path='/products' exact >
          <Products></Products>
        </Route>
        <Route path='/products/:productId'>
          <ProductDetail></ProductDetail>
        </Route>
</Swit

```

正常顯示畫面

![](https://i.imgur.com/bQfru0g.png)

## Working with nested Routes

> 你可以把 Routes 放在任何地方，不限縮在一個檔案中，可以放在多個檔案沒問題

這邊我們把 Route 再次鑲嵌進去 Welcome.js 裏面，做出 nested routes ，但要注意的是裡面的 to 必須要填入合適的 path 

比方說如果你在 to 填入 product 就不會出現東西，因為其外面的路徑是 welcome ，這個設置好的 nested route 可以在內部使用新的 component 或是 JSX 都可以！

```javascript=
import {Route} from 'react-router-dom'

const Welcome = ()=>{
  return (
  <section>
    <h1>The Welcome Page</h1>
      <Route to='/welcome/new-user'>
        Welcome, new-user!
      </Route>
  </section>
  )
}
export default Welcome;
```
呈現畫面

![](https://i.imgur.com/HOxqZBq.png)

## Redirecting The User

這邊我們可以做一個導向功能使用到 Redirect 

* 引入 Redirect 
* 新增一個 Route 並且前往 `'/'`
* 使用 Redirect 把它導向 welocome 頁面

```javascript=
import {Route,Switch, Redirect} from 'react-router-dom';
import Products from './pages/Products';
import Welcome from './pages/Welcome';
import MainHeader from './components/MainHeader';
import ProductDetail from './pages/ProductDetail';

export default function App() { 

  return (
   <div>
     <MainHeader></MainHeader>
     <main>
       <Switch>
         <Route path='/' exact>
           <Redirect to='/welcome' ></Redirect>
         </Route>
        <Route path='/welcome'>
          <Welcome></Welcome>
        </Route>
        <Route path='/products' exact>
          <Products></Products>
        </Route>
        <Route path='/products/:productId'>
          <ProductDetail></ProductDetail>
        </Route>
      </Switch>
     </main>
   </div>
  );
}
```

經過這樣操作之後我們在 URL 輸入 `'/'` 就會自動幫助我們導向 welcome 頁面摟！或是任何我們想要操作的頁面


# Time to practice : onward to a new project 

這邊的 DEMO 會做一個 quotes 的 app 會有以下頁面包含 Routes 

1. 頁面包含了所有的 quotes
2. quotes details 頁面
3. 新增 qoute 頁面

```javascript=
import {Route, Switch, Redirect} from 'react-router-dom'
import AllQuotes from './pages/AllQuotes'
import QuoteDetail from './pages/QuoteDetail';
import NewQuote from './pages/NewQuote';

function App() {
  return (
      <Switch>
        <Route path='/' exact>
          <Redirect to='/quotes'></Redirect>
        </Route>
        <Route path='/quotes' exact>
          <AllQuotes></AllQuotes>
        </Route>
        <Route path='/quotes/:quoteId'> // 這邊設置 dynamic route
          <QuoteDetail></QuoteDetail>
        </Route>
        <Route path='/new-quote' exact>
          <NewQuote></NewQuote>
        </Route>
      </Switch>
  );
}
export default App;

```

接下來針對每一篇 quote 點進去都會有 quote detail 路徑以及頁面的部分設定 dynamic path / useParams

```javascript=
import React,{Fragment} from 'react'
import { useParams } from 'react-router'

export default function QuoteDetail() {
  const params = useParams()
  
  return (
    <Fragment>
      <h1>  Quotes Details</h1>
      <p>{params.quoteId}</p>
    </Fragment>
  )
}
```

接下來會在 quote detail 頁面操作 nested route 把 comments 內容加在每一篇 quote detail 裏面

## QuoteDetail.js 

這邊內部在 nest 一個 Route 來達到在 quote detail 頁面內部使用另一個 component 的需求（使用 comment component）

並且  JSX 中是可以使用 template literal 的也就可以使用動態的參數呈現網址
你的 paramId 輸入什麼你的 URL 就會是什麼，也可以讓 comment 出現在正確的 quotes detail 內

這邊使用 Fragment 是為了在 JSX 中遷入 json 做使用

```javascript=
import React,{Fragment} from 'react'
import { useParams, Route } from 'react-router-dom'
import Comments from '../components/comments/Comments'

export default function QuoteDetail() {
  const params = useParams()
  return (
    <Fragment>
      <h1>  Quotes Details</h1>
      <p>{params.quoteId}</p>
      <Route path={`/quotes/${params.quoteId}/comments`}>
        <Comments></Comments>
      </Route>
    </Fragment>
  )
}
```

## Adding a Layout Wrapper Component 

到目前為止 app 只能手動更改 URL ，所以這邊我們會建立 MainNavigation.js 來做出簡單的 navbar 

一樣簡單的操作 NavLink 並且導向正確的 URL path 後，再加上 active class 做使用


MainNavigation.js
```javascript=
import React from 'react'
import classes from './MainNavigation.module.css'
import { NavLink } from 'react-router-dom'

export default function MainNavigation() {
  return (
    <header className={classes.header}>
      <div className={classes.logo}>Great quote</div>
      <nav className={classes.nav}>
        <ul>
          <NavLink to='/quotes' activeClassName={classes.active}>
            All quotes
          </NavLink>
          <NavLink to='/new-quote' activeClassName={classes.active}>
            Add a quote
          </NavLink>
        </ul>
      </nav>
    </header>
  )
}
```

雖然我們可以直接把 MainNavigation.js 直接加進去 App.js 的 Switch 內，不過我們這邊會使用一個 layout component 來當作一個外包裝來包裹著

Layout.js

這個檔案裡面的 main 會包裹住全部的 route 所以之後加進來的 route 都會吃到這邊的 classes 

```javascript=
import React,{Fragment} from 'react'
import MainNavigation from './MainNavigation'
import classes from './Layout.module.css'

export default function Layout(props) {
  return (
    <Fragment>
      <MainNavigation></MainNavigation>
      <main className={classes.main}>
        {props.children}
      </main>
    </Fragment>
  )
} 
```

App.js

引入 Layout.js 後包裹住所有的 route 就可以更簡單的設定整體的 css 摟！

```javascript=
function App() {
  return (
    <Layout>
      <Switch>
        <Route path='/' exact>
          <Redirect to='/quotes'></Redirect>
        </Route>
        <Route path='/quotes' exact>
          <AllQuotes></AllQuotes>
        </Route>
        <Route path='/quotes/:quoteId'>
          <QuoteDetail></QuoteDetail>
        </Route>
        <Route path='/new-quote' exact>
          <NewQuote></NewQuote>
        </Route>
      </Switch>
    </Layout>
  );
}
```

簡單的 navbar 就出現了包含所有的 NavLink 

![](https://i.imgur.com/1UIAq6d.png)


## Adding Dummy data & more Content 


### Adding Dummy 頁面

首先我們要把 寫死的 quote dummy quotes 先放進去 all quote 頁面，並且把 Dummy quote 當作 props 傳入 quoute list 內做呈現（暫時）

```javascript=
import React from 'react'
import QuoteList from '../components/quotes/QuoteList'

const DUMMY_QUOTES = [
  { id:'q1', author:'max', text:'first quote', },
  { id:'q2', author:'chieh', text:'second quote', }
]

export default function AllQuotes() {
  return (
    <div>
      <QuoteList quotes={DUMMY_QUOTES}/>
    </div>
  )
}
```
就可以在頁面上看到假的 quote 摟
![](https://i.imgur.com/ymF7rUB.png)

### 接下來我們要處理 add new quote 頁面


#### NewQuote.js 

首先我們會引入 QuoteForm 來把 Form 呈現在 new quote 頁面
因為 quteForm 裡面有 onAddQuote function 所以這邊要創造它並且把它當做 props 傳進去，它的作用主要是擷取 author 以及 text 的內容

```javascript=
import React from 'react'
import QuoteForm from '../components/quotes/QuoteForm'

export default function NewQuote() {

  const addQuoteHandler = quoteData =>{
    console.log(quoteData)
  }

  return (
    <QuoteForm onAddQuote={addQuoteHandler} />
  )
}
```

#### QuoteForm.js

主要使用 useRef 來擷取兩個 input 區域的內容  author, text ， 並且當 submit 時透過 prop 帶進來的函式 onAssQuote 把這兩個內容送回去 NewQuote 中

JSX 的部分主要是呈現 form 的內容

```javascript=
import { useRef } from 'react';

import Card from '../ui/Card';
import LoadingSpinner from '../ui/LoadingSpinner';
import classes from './QuoteForm.module.css';

const QuoteForm = (props) => {
  const authorInputRef = useRef();
  const textInputRef = useRef();

  function submitFormHandler(event) {
    event.preventDefault();

    const enteredAuthor = authorInputRef.current.value;
    const enteredText = textInputRef.current.value;

    // optional: Could validate here

    props.onAddQuote({ author: enteredAuthor, text: enteredText });
  }

  return (
    <Card>
      <form className={classes.form} onSubmit={submitFormHandler}>
        {props.isLoading && (
          <div className={classes.loading}>
            <LoadingSpinner />
          </div>
        )}

        <div className={classes.control}>
          <label htmlFor='author'>Author</label>
          <input type='text' id='author' ref={authorInputRef} />
        </div>
        <div className={classes.control}>
          <label htmlFor='text'>Text</label>
          <textarea id='text' rows='5' ref={textInputRef}></textarea>
        </div>
        <div className={classes.actions}>
          <button className='btn'>Add Quote</button>
        </div>
      </form>
    </Card>
  );
};

export default QuoteForm;
```

順利加入成功的 quotes 並且成功回傳 author, text 回去 New quote 裏面 console 到瀏覽器中摟！

![](https://i.imgur.com/KEoeoEl.png)

接下來要做的事情是點擊 view Fullscreen 會進入 quote detail 頁面

![](https://i.imgur.com/4wMgJQI.png)

## Outputing Data on Quote Detail page

### QuoteItem.js

這邊針對 View Fullscreen 這邊做好 Link 的設置，設置好路徑前往 `props.id` 也就是從 AllQuotes.js 的 Dummy quote 傳下去 props 給 QuoteList 然後在 QuoteItem 內部做使用，就可以確保點擊 view Fullscreen 可以到達正確的 quoteId 的頁面

內部嵌入使用 template literal 來動態放入參數使用 `props.id` 

```javascript=
import classes from './QuoteItem.module.css';
import {Link} from 'react-router-dom'

const QuoteItem = (props) => {
  return (
    <li className={classes.item}>
      <figure>
        <blockquote>
          <p>{props.text}</p>
        </blockquote>
        <figcaption>{props.author}</figcaption>
      </figure>
      <Link className='btn' to={`/quotes/${props.id}`}>
        View Fullscreen
      </Link>
    </li>
  );
};

export default QuoteItem;
```

Link 設置好之後，接下來要處理 quote detail 頁面的資料呈現


### QuoteDetail.js

這邊針對 quote 內容以及 author 有特別設置 component HightlightedQuote.js 來包裹著 text, author 並且在此加入 style 後再輸出回去給 QuoteDetail.js 內做呈現

#### HightlightedQuote.js
```javascript=
import classes from './HighlightedQuote.module.css';

const HighlightedQuote = (props) => {
  return (
    <figure className={classes.quote}>
      <p>{props.text}</p>
      <figcaption>{props.author}</figcaption>
    </figure>
  );
};

export default HighlightedQuote;

```

#### QuoteDetail.js

把 Dummy quote 拉到這邊處理，變成參數放進去操作，找出符合 `params.quoteId` 的 Dummy quote 並且做判斷如果 quote 為 false 的話，則把 JSX 導入 not found 字樣取代 HighlightQuote 以及 Comments 

```javascript=
import React,{Fragment} from 'react'
import { useParams, Route } from 'react-router-dom'
import Comments from '../components/comments/Comments'
import HighlightedQuote from '../components/quotes/HighlightedQuote'

const DUMMY_QUOTES = [
  { id:'q1', author:'max', text:'first quote', },
  { id:'q2', author:'chieh', text:'second quote', }
] 

export default function QuoteDetail() {
  const params = useParams()
  const quote = DUMMY_QUOTES.find(quote =>quote.id === params.quoteId)

  if(!quote){
    return (
      <p>No quote found!</p>
    )
  }

  return (
    <Fragment>
      <HighlightedQuote text={quote.text} author={quote.author}></HighlightedQuote>
      <Route path={`/quotes/${params.quoteId}/comments`}>
        <Comments></Comments>
      </Route>
    </Fragment>
  )
}
```

## Adding a Not found page

上一篇的最後有針對如果 quotes 不存在的頁面( URL path: /quotes/輸入不存在的 quoteId)會導向找不到 quote 頁面

然而，這邊要處理的是如果首頁也就是 `'/'` URL path 輸入不對會導入的 not found page 

比方說 `https:domainname/jsdiofjiodsf亂碼`

### App.js

在 Route 最後加上路徑為 * ，並且放入我們創立好的 Notfound.js 頁面即可

這邊因為 Route 以及 Switch 的篩選是有命中就停下來，所以如果上面的 Route 如果都沒有命中的話，則使用 ＊ 這個 Route 它代表全部的 URL 內容都會採用的意思，所以如果上面設置好的條件都沒有命中一直篩選到最後的這個 * 號 Route 時就會被導入 NotFound 頁面摟

```javascript=
function App() {
  return (
    <Layout>
      <Switch>
        <Route path='/' exact>
          <Redirect to='/quotes'></Redirect>
        </Route>
        <Route path='/quotes' exact>
          <AllQuotes></AllQuotes>
        </Route>
        <Route path='/quotes/:quoteId'>
          <QuoteDetail></QuoteDetail>
        </Route>
        <Route path='/new-quote' exact>
          <NewQuote></NewQuote>
        </Route>
        <Route path='*'>
          <NotFound></NotFound>
        </Route>
      </Switch>
    </Layout>
  );
```

### NotFound.js

```javascript=
 import React from 'react'
 
 export default function NotFound() {
   return (
     <div className='center'>
       <p>Page not Found!</p>
     </div>
   )
 }
```

印出結果

可以看到輸入不符合設置好的 route 會導入錯誤頁面

![](https://i.imgur.com/xcGWEXS.png)


## Implementing Programmatic Navigation

這邊我們會針對 add new quote 的 form 處理點擊 submit 新的 quote 後，會發生的事情，比方說會發生跳出送出成功或是回到 AllQuote 頁面可以看到剛剛自己新增的 quote 等等

針對 QuoteForm 的按鈕我們當可以設置 Link 就可以直接跳轉，但是這邊我們有 submit 功能所以必須保留 button 因此這時候就可以使用 Programmatic Navigatoin 來操作

### NewQuote.js

因為 submit 按鈕的函式的來源在 NewQuote.js 內所以從這邊做操作

引入 useHistoru 從 react-router-dom
使用後會回傳一個物件 history 並且有兩個常用的方法可以操作
1. push 可以去到某個頁面
2. replace 可以取代現在的頁面

差別在於 push 可以回到上一頁
replace 比較像是 redirect 重新導向的感覺


```javascript=
import React from 'react'
import QuoteForm from '../components/quotes/QuoteForm'
import { useHistory } from 'react-router-dom'

export default function NewQuote() {

  const history = useHistory()

  const addQuoteHandler = quoteData =>{
    console.log(quoteData)

    history.push('/quotes')
  }

  return (
    <QuoteForm onAddQuote={addQuoteHandler} />
  )
}
```

所以現在 submit 完新增的 quote 後就可以切換頁面到 AllQuote 頁面看到已經新增的 quote 摟！

## Preventing Possibly Unwanted Route Transitions with the "Prompt" Component

你一定有遇過一些網頁在填寫表單時，因為跳轉頁面導致回來繼續填寫表單時，整個內容因為消失所以要重新輸入，這邊要實作的就是一個 Prompt 視窗跟你做確認你是否要離開 form 頁面

### QuoteForm.js

為了操作 Promp 以及確認是否要離開頁面必須要引入

* Prompt
* useState 來操作是否離開頁面的 state

1. 再來操作 useState 來監聽是否正在輸入中
1. 再來操作加上 onFocus 來會觸發 formFocusHandler 這時候就會 state 的狀態就會被設定成 true 也就代表正在輸入中摟(代表只要碰進去 input 就會修改 inEntering 狀態) 
3. 所以只要在 isEntering true 的狀態下，這時候只要切換頁面就會跳出我們設置好的 Prompt 視窗提醒使用者是否真的要切換頁面
4. 最後為了避免連 submit 內容都問你要不要切換，所以針對 submit 按鈕做 onClick 事件監聽並且觸發 finishEnteringHandler 來把 isEntering 狀態切回false 就不會跳出提醒摟

這邊有出現一個問題，為什麼不能把 setisEntering 放在 onSubmit 裡面呢？

因為 UseState 內部返回用來設定 state 的函式是非同步的，因此會等 submitFormHandler 內所有的程式跑完之後才會輪到 setIsEntering 因此把它拉出去做 onClick 讓他早於 onSubmit 觸發就可以成功修改 isEntering 狀態摟


```javascript=
import { Fragment, useRef, useState } from 'react';
import {Prompt} from 'react-router-dom';

import Card from '../UI/Card';
import LoadingSpinner from '../UI/LoadingSpinner';
import classes from './QuoteForm.module.css';

const QuoteForm = (props) => {
  const authorInputRef = useRef();
  const textInputRef = useRef();
  const [isEntering, setIsEntering] = useState(false)

  function submitFormHandler(event) {
    event.preventDefault();

    const enteredAuthor = authorInputRef.current.value;
    const enteredText = textInputRef.current.value;

    // optional: Could validate here

    props.onAddQuote({ author: enteredAuthor, text: enteredText });
  }

    const formFocusHandler = () =>{
      setIsEntering(true)
    }
    
    const finishEnteringHandler = () =>{
      setIsEntering(false)
    }

  return (
    <Fragment>
      <Prompt when={isEntering} message={(location)=>'Are you sure you want to leave ? All your data will be lost!'}></Prompt>
      <Card>
        <form className={classes.form} onSubmit={submitFormHandler} onFocus={formFocusHandler}>
          {props.isLoading && (
            <div className={classes.loading}>
              <LoadingSpinner />
            </div>
          )}

          <div className={classes.control}>
            <label htmlFor='author'>Author</label>
            <input type='text' id='author' ref={authorInputRef} />
          </div>
          <div className={classes.control}>
            <label htmlFor='text'>Text</label>
            <textarea id='text' rows='5' ref={textInputRef}></textarea>
          </div>
          <div className={classes.actions}>
            <button onClick={finishEnteringHandler} className='btn'>Add Quote</button>
          </div>
        </form>
      </Card>
    </Fragment>
  );
};

export default QuoteForm;
```
使用 Prompt 出現的警告視窗
![](https://i.imgur.com/HYTtJfq.png)

## Working with Query Parameters

> Query Parameters 跟一般的路徑參數不同的是他是選擇性的不是強制的，沒有使用的話就顯示 default 頁面
> 可以操作頁面行為
> 會記住當下 URL path 的狀態，當傳給他人使用時也會是一樣的狀態

### 一般的路徑顯示

最後的 params 是一定要輸入才能進入到特地的頁面

`https://domainname/quotes/:quoteId `

### Query Parameters 在路徑上面則是不一定得輸入

並且他對於進去哪個 route 並沒有影響，而是針對可以符合條件的 route 才可以取得 Query Parameters 內的資料也因此才能改變頁面的行為

`https://domainname/quotes/？sort=asc `

那麼會是什麼樣的頁面行為呢？

以上方的網址為例子

我們想要在 Allquote 頁面設置排序功能並且按照 ascending or descending 的排序就可以在 ? 後方加入 sort=asc 就是 ascending 來改變頁面的行為

如果沒有使用 query parameters 的話則就會正常顯示 default 的頁面

### 操作加入 query parameters 

我們會在 QueryList.js 操作把排序功能(sort)實作一個按鈕上去

#### QueryList.js

1. 首先加上按鈕的區塊並且套用 style 上去
1. 接下來給按鈕加上 onClick 觸發函式 changeSortingHandler
1. 這邊待會會動態的切換 ascding/ descending 
1. 引入 usehistory 來實作切換 URL path 這部分待會也會操作成動態的做切換

```javascript=
import { Fragment } from 'react';
import { useHistory } from 'react-router-dom';
import QuoteItem from './QuoteItem';
import classes from './QuoteList.module.css';

const QuoteList = (props) => {
  const history = useHistory();

  const changeSortingHandler = () =>{
    history.push('/quotes?sort=asc')
  }

  return (
    <Fragment>
      <div className={classes.sorting}>
        <button onClick={changeSortingHandler}>Sort Ascending</button>
      </div>
      <ul className={classes.list}>
        {props.quotes.map((quote) => (
          <QuoteItem
            key={quote.id}
            id={quote.id}
            author={quote.author}
            text={quote.text}
          />
        ))}
      </ul>
    </Fragment>
  );
};
export default QuoteList;
```

這邊就會在點擊按鈕後呈現 URL path 的跳轉到我們在 `history.push` 操作的 path

![](https://i.imgur.com/vCvjchU.png)

### 下一步我們會讀取 query parameters 的值並且操作

* 改變 sorting 方式
* 改變 button label 比方說現在式 asc 按鈕就顯示相反

首先必須在 QuoteList.js 內部引入 useLocation 從 react-router-dom 
接下來它會返回一個物件 location ，我們先印出來看看內容

![](https://i.imgur.com/WoBAICT.png)

我們可以從中理解一些事情

* 畫面在每次點擊 Sort 按鈕後都會 re-render 因為改變了 history 
* 待會會運用這個特點做操作

### 接下來會操作回傳回來的物件中 search 的屬性

使用 JS 內建的 function constructor 

`new URLSearchParams()` 它主要處理的是傳入的 URL 的 string 的操作

1. 我們把 location.search 傳入其中
1. 並且使用 URLSearchParams 內建的 get 方法取得 'sort' 的 value 
1. 並且做判斷如果為 asc 返回 ture 不是則 false
1. 這邊有狀態之後我們就可以填入 history 以及 按鈕中做條件宣篩選摟
1. 所以現在按鈕以及 URL 都會動態呈現內容摟

```javascript=
import { Fragment } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import QuoteItem from './QuoteItem';
import classes from './QuoteList.module.css';

const QuoteList = (props) => {
  const history = useHistory();
  const location = useLocation();
  const queryParams = new URLSearchParams(location.search);
  const isSortingAscending = queryParams.get('sort') ==='asc';

  const changeSortingHandler = () =>{
    history.push('/quotes?sort='+(isSortingAscending ? 'desc' :'asc'))
  }

  return (
    <Fragment>
      <div className={classes.sorting}>
        <button onClick={changeSortingHandler}>Sort {isSortingAscending ? 'Descending' : 'Ascending'}</button>
      </div>
      <ul className={classes.list}>
        {props.quotes.map((quote) => (
          <QuoteItem
            key={quote.id}
            id={quote.id}
            author={quote.author}
            text={quote.text}
          />
        ))}
      </ul>
    </Fragment>
  );
};
export default QuoteList;
```

這樣子的動態呈現按鈕跟 URL 就成功摟！

![](https://i.imgur.com/zF5stKM.png)


下一步要使用 sorting 功能摟！

### 使用 sorting 功能

首先我們建立好 sortQuotes 函式等著做使用想法是：

1. 首先傳入 quotes 也就是 quotes 本身
1. 傳入 ascending 這是布林值 表達是否為 ascending 
1. 針對 quotes 做 sort （ascending 是上升的意思所以數字大的 id 會往下放）
* 這邊的邏輯是  id 比較大的會往下放，因為回傳為 1 的話則 放置方式為 B => A
* 如果回傳為 -1 的話則 放置方式為 A => B

```javascript=
const sortQuotes = (quotes, ascending) =>{
  return quotes.sort((quoteA, quoteB) =>{
    if(ascending){
      return quoteA.id> quoteB.id ? 1:-1;
    }else {
      return quoteA.id< quoteB.id ?1:-1;
    }
  })
}
```

接下來把排序完成的資料傳進去 JSX 中 map 出來

首先必須先建立變數把 sortQuotes 指派給新的變數 sortedQuotes 並且帶入參數後呼叫

```javascript=
  const sortedQuotes = sortQuotes(props.quotes, isSortingAscending);
```

接下來就可以把他丟進去 JSX 中摟

```javascript=
<Fragment>
      <div className={classes.sorting}>
        <button onClick={changeSortingHandler}>Sort {isSortingAscending ? 'Descending' : 'Ascending'}</button>
      </div>
      <ul className={classes.list}>
        {sortedQuotes.map((quote) => (
          <QuoteItem
            key={quote.id}
            id={quote.id}
            author={quote.author}
            text={quote.text}
          />
        ))}
      </ul>
    </Fragment>
```

最後的成品如下

因為每次頁面在修改 URL path 時都會 re-render 畫面，因此都會重新觸發所有的函式，讓這個 sort 功能可以正常切換

* 就可以看到 URL 處顯示的 des 代表順序是 ”下降“ 所以會是由大到小排序
* 在看向按鈕處 sort 按鈕後方顯示的 Ascending 也是正確的因為目前是 des
* 最後我們來看排序狀態 也確實是由大到小 second => first 

![](https://i.imgur.com/hxMQx1r.png)

最後這個 URL path 的狀態如果完整的複製給他人使用的話，假設你給他的時候狀態是 asc 那麼他開起的時候也會是一樣的狀態，這也是 query parameters 的特性喔！

## Getting Creative With Nested Routes

接下來會操作 Nested Routes 來實作 quotes details 頁面的 comments 功能

我們會在 quote detail 頁面做出 跳轉 comment 頁面的按鈕

引入 Link 之後路徑直接使用下方 comments 的路徑即可

```JSX=
<Fragment>
      <HighlightedQuote text={quote.text} author={quote.author}></HighlightedQuote>
      <Route path={`/quotes/${params.quoteId}`} exact>
        <div className='centered'>
          <Link className='btn--flat' to={`/quotes/${params.quoteId}/comments`}>Load Comments</Link>
        </div>
      </Route>
      <Route path={`/quotes/${params.quoteId}/comments`}>
        <Comments></Comments>
      </Route>
</Fragment>
```

這篇的重點在於使用 Route 的 URL 來條件判斷呈現 component 

我們針對 Load Comments 按鈕的 Route 設置的路徑為 detail page 的路徑並且使用 exact （因此它不會跟 comment 同時出現因為路徑不同了）

這樣就是使用 URL 來做條件判斷 UI 呈現

![](https://i.imgur.com/orYWHNx.png)

點擊 Load Comments 後，因為 URL 的改變因此 UI 改變

![](https://i.imgur.com/SEtSO7s.png)

## Writing More Flexible Routing Code

這邊我們使用 useRouteMatch 來擷取 Nested Route 外層的 URL 以及 path ，藉由這樣的方式幫助 Nested Route 內層 Route 就可以使用 useRouteMatch 產生的物件設立變數做操作，就不會寫死 URL 讓他可以用變數的方式插入更為方便摟！

上述文字沒看懂嗎？ 很正常可以看看範例比較好理解


### QuoteDetail.js

這邊就是 Nested Route 用來處理 quote detail 內部的 comment 的連結，我們針對裡面的 path 作處理

1. 引入 useRouteMath 後，使用他的屬性來帶入 Route 內的 URL
2. 記得使用動態的方式插入使其變得更靈活

match 物件內的屬性

![](https://i.imgur.com/GtNg2p0.png)

```jsx=

 const match = useRouteMatch();

  return (
    <Fragment>
      <HighlightedQuote text={quote.text} author={quote.author}></HighlightedQuote>
      <Route path={match.path} exact>
        <div className='centered'>
          <Link className='btn--flat' to={`${match.url}/comments`}>Load Comments</Link>
        </div>
      </Route>
      <Route path={`${match.url}/comments`}>
        <Comments></Comments>
      </Route>
    </Fragment>
  )
}
```

所以現在來看，如果我們想要修改 Root Route 的 URL 時，因為 Nested Route 使用 useRouteMatch 的方式所以不需要一個一個修改，會直接被抓進去非常方便

### 練習一下使用 QuoteList.js 內部的 URL 做操作

#### QuoteList.js

一樣引入 match 後動態放入 path 做操作即可

```javascript=
const changeSortingHandler = () =>{
    history.push(`${match.path}?sort=`+(isSortingAscending ? 'desc' :'asc'))
  }
```

但是不覺得有點難閱讀讀嗎？

這時候就可以操作 react-router 的特色把 push 內部的 url 改個呈現方式

* pathname 放入 match 傳回的 path 屬性
* search 則使用 query parameters 

```javascript=
history.push({
    pathname:match.path,
    search:`${isSortingAscending ? 'desc' :'asc'}`
})
```

這樣就可以讓 URL 好閱讀很多喔！

## Sending & Getting Quote Data via Http

到目前為止，主要的 react-router 的功能已經做完，剩下就是串接資料也就是 quote 上去畫面而不是用手 key 的摟！

### 首先我們處理 NewQuote.js 來新增新的 quote 到 firebase 

#### NewQuote.js

詳細的 useHttp, addQuote 內容不是重點因此不多加解釋

* 引入 useHttp 這個 custom hook 來傳入 sendRequest 方法, status 狀態，主要處理 addQuote 之後的狀態
* 引入 addQuote 從 lib ，主要發送 POST 方法上去 firebase
* firsebase 要使用 Realtime Database 來操作即可
* ![](https://i.imgur.com/N2YbBWL.png)

* 使用 useEffect ，當 status 為 completed 時觸發頁面跳轉並且監聽 status, history（主要是 status）
* 使用 status 來操作 QuoteForm 內部的 isLoading 來判斷是否觸發 loading 特效 

loading 特效

![](https://i.imgur.com/C7Cba9K.png)


```javascript=
import React,{useEffect} from 'react'
import QuoteForm from '../components/quotes/QuoteForm'
import { useHistory } from 'react-router-dom'
import useHttp from '../hooks/use-http'
import {addQuote} from '../lib/api' 
// 這邊的 addQuote 會發送 post request 給 firebase

export default function NewQuote() {
  const {sendRequest, status} = useHttp(addQuote);
  const history = useHistory()

  useEffect(() => {
    if(status ==='completed'){
      history.push('/quotes')
    }
  }, [status,history])

// 這裡的 quoteData 就是使用者傳入 text, author 的內容會從 QuoteForm 回傳
  const addQuoteHandler = (quoteData) =>{
    sendRequest(quoteData);
  }

  return (
    // 這邊透過 status 狀態來判斷是否觸發轉圈 loading 特效
     <QuoteForm isLoading={status === 'pending'} onAddQuote={addQuoteHandler} />
  )
}
```

### 抓下 firebase 真正的資料取代剛剛手輸入的 quotes 操作在 AllQuotes.js 頁面

#### AllQuotes.js

* 引入 useHttp 來操作 getAllQuotes 並且設定起始狀態為 pending 以及解構初 sendRequest, ...httpState 做使用
* 引入 getAllQuotes 抓取 firebase 資料 會返回 內涵目前 quotes 資料物件的陣列
* 引入 LoadingSpinner 在等待狀態時出現旋轉條
* 引入 NoQuotesFound 在 status 為 error 時顯示的 component 

1. 使用 useHttp 並且帶入 getAllQuotes 來抓取 firebase 資料回來做操作
2. 從 useHttp 中解構初 sendRequest 以及 httpState 也就是 status,data,error
3. 使用 useEffect 來監聽 sendRequest 並且使用它
4. 用判斷式來操作畫面呈現主要判斷 status 的狀態
5. 最後如果判斷是都沒有跑則 return QuotesList 帶入 props data 的內容 loadedQuotes 正確的呈現資料庫所有的內容

這時候剛剛填入的 DUMMY QUOUTES 都可以砍掉了

```javascript=
import React,{useEffect} from 'react'
import QuoteList from '../components/quotes/QuoteList'
import LoadingSpinner from '../components/UI/LoadingSpinner'
import NoQuotesFound from '../components/quotes/NoQuotesFound'
import useHttp from '../hooks/use-http'
import { getAllQuotes } from '../lib/api' 

export default function AllQuotes() {
  const {sendRequest, status, data:loadedQuotes,error} = useHttp(getAllQuotes,true)

  useEffect(() => {
    sendRequest();
  }, [sendRequest])

  if(status === 'pending'){
    return (
      <div className='centered'>
        <LoadingSpinner></LoadingSpinner>
      </div>
    )
  }
  if(error){
    return <p className='centered focused'>{error}</p>
  }

  if(status ==='completed'&&(!loadedQuotes||loadedQuotes.length ===0)){
    return <NoQuotesFound></NoQuotesFound>
  }

  return <QuoteList quotes={loadedQuotes}/>
}
```

顯示資料庫所有的內容

使用了一大堆 test 

![](https://i.imgur.com/MtuAwXC.png)


如果我把 firebase 內空砍光則顯示 no found 頁面

這邊記得操作把中間的 Add a Quote 使用 Link 連結到 /new-quote 頁面才能正常使用按鈕

```javascript=
const NoQuotesFound = () => {
  return (
    <div className={classes.noquotes}>
      <p>No quotes found!</p>
      <Link className='btn' to='/new-quote'>
        Add a Quote
      </Link>
    </div>
  ); 
};
```
![](https://i.imgur.com/RoEWf0v.png)

### 接下來操作 singleQuote 在 QuoteDetail.js 頁面

剛剛操作完全部的 quote 的呈現，現在要操作 quote detail 頁面單一個 quote

#### QuoteDetail.js

基本上操作都跟 AllQuote.js 內容一樣

不同的是本篇是必須擷取單一個 id 所以必須從 Params 中解構出來 quoteId 做操作

```javascript=
import React,{Fragment, useEffect} from 'react'
import { useParams, Route, Link, useRouteMatch } from 'react-router-dom'
import Comments from '../components/comments/Comments'
import HighlightedQuote from '../components/quotes/HighlightedQuote'
import LoadingSpinner from '../components/UI/LoadingSpinner'
import useHttp from '../hooks/use-http'
import { getSingleQuote } from '../lib/api'

export default function QuoteDetail() {
  const params = useParams()
  const {quoteId} = params;
  const match = useRouteMatch();
  const {sendRequest, status, data:loadedQuote,error} = useHttp(getSingleQuote,true)

  useEffect(() => {
    sendRequest(quoteId)
  }, [sendRequest,quoteId])

  if(status === 'pending'){
    return (
      <div className='centered'>
        <LoadingSpinner></LoadingSpinner>
      </div>
    )
  }

  if(error){
      return <p className='centered focused'>{error}</p>
  }

  if(!loadedQuote.text){
    return <p>No Quote Found!</p>
  }

  return (
    <Fragment>
      <HighlightedQuote text={loadedQuote.text} author={loadedQuote.author}></HighlightedQuote>
      <Route path={match.path} exact>
        <div className='centered'>
          <Link className='btn--flat' to={`${match.url}/comments`}>Load Comments</Link>
        </div>
      </Route>
      <Route path={`${match.url}/comments`}>
        <Comments></Comments>
      </Route>
    </Fragment>
  )
}

```
正確抓到資料庫中的 123test 摟
![](https://i.imgur.com/T79TvhJ.png)

## Adding the "Comments" Features

最後剩下 Comments 功能來實作，把寫好的 comments 送到 firebase 儲存

### NewCommentForm.js

* 一樣使用到 useHttp 
* api.js 內部的函式 addComment 
* LoadingSpinner

這篇主要的邏輯處理

1. 使用 useRef 抓取 commentText 
2. 抓到之後放入 sendRequest 傳上去 firebase
3. 解構 onAddComment 從 props 使用在當 status 為 completed 或是沒有錯誤時

這邊的 onAddComment 會被定義在 parent Comment.js 內，主要的功能是把 quoteId 傳上去 firebase 

```javascript=
import { useRef, useEffect } from 'react';

import useHttp from '../../hooks/use-http';
import { addComment } from '../../lib/api';
import LoadingSpinner from '../UI/LoadingSpinner';
import classes from './NewCommentForm.module.css';

const NewCommentForm = (props) => {
  const commentTextRef = useRef();

  const { sendRequest, status, error } = useHttp(addComment);

  const { onAddedComment } = props;

  useEffect(() => {
    if (status === 'completed' && !error) {
      onAddedComment();
    }
  }, [status, error, onAddedComment]);

  const submitFormHandler = (event) => {
    event.preventDefault();

    const enteredText = commentTextRef.current.value;

    sendRequest({ commentData: { text: enteredText }, quoteId: props.quoteId });
  };

  return (
    <form className={classes.form} onSubmit={submitFormHandler}>
      {status === 'pending' && (
        <div className='centered'>
          <LoadingSpinner />
        </div>
      )}
      <div className={classes.control} onSubmit={submitFormHandler}>
        <label htmlFor='comment'>Your Comment</label>
        <textarea id='comment' rows='5' ref={commentTextRef}></textarea>
      </div>
      <div className={classes.actions}>
        <button className='btn'>Add Comment</button>
      </div>
    </form>
  );
};

export default NewCommentForm;
```

### Comment.js

接下來就可以到 Comment.js 內部處理條件顯示 Comments 摟

為了擷取剛剛傳上去 NewComments 所以一樣得使用 useHttp, getAllComments 函式

* 當回傳的 status 為 pending 則顯示 loading 圖示
* 為 completed 以及內容真的有東西後 把 loadedComments 當 props 傳入 CommentList 做呈現
* 為 completed 但是沒有內容的話則顯示 no found 

多使用了 useCallback 針對使用在 addedCommentHandler 並且監聽 sendRequst,quoteId 才不會一 re-render 就重新創造函式並且再送一次資料上去 firebase 

```javascript=
import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'react-router-dom';

import classes from './Comments.module.css';
import NewCommentForm from './NewCommentForm';
import useHttp from '../../hooks/use-http';
import { getAllComments } from '../../lib/api';
import LoadingSpinner from '../UI/LoadingSpinner';
import CommentsList from './CommentsList';

const Comments = () => {
  const [isAddingComment, setIsAddingComment] = useState(false);
  const params = useParams();

  const { quoteId } = params;

  const { sendRequest, status, data: loadedComments } = useHttp(getAllComments);

  useEffect(() => {
    sendRequest(quoteId);
  }, [quoteId, sendRequest]);

  const startAddCommentHandler = () => {
    setIsAddingComment(true);
  };

  const addedCommentHandler = useCallback(() => {
    sendRequest(quoteId);
  }, [sendRequest, quoteId]);

  let comments;

  if (status === 'pending') {
    comments = (
      <div className='centered'>
        <LoadingSpinner />
      </div>
    );
  }

  if (status === 'completed' && loadedComments && loadedComments.length > 0) {
    comments = <CommentsList comments={loadedComments} />;
  }

  if (
    status === 'completed' &&
    (!loadedComments || loadedComments.length === 0)
  ) {
    comments = <p className='centered'>No comments were added yet!</p>;
  }

  return (
    <section className={classes.comments}>
      <h2>User Comments</h2>
      {!isAddingComment && (
        <button className='btn' onClick={startAddCommentHandler}>
          Add a Comment
        </button>
      )}
      {isAddingComment && (
        <NewCommentForm
          quoteId={quoteId}
          onAddedComment={addedCommentHandler}
        />
      )}
      {comments}
    </section>
  );
};

export default Comments;
```

firebase 都可以正常接收到資料

![](https://i.imgur.com/6d865bq.png)

並且正常呈現在頁面中摟！

![](https://i.imgur.com/qDa7QD4.png)
