我們今天接續的做每個角色自己的頁面,來理解 Dynamic Routes 的實作層面。
這部分我們可以先借一下 Next 官方的範例來解說:
export default function Page({ params }: { params: { slug: string } }) {
return <div>My Post: {params.slug}</div>
}
在這範例中,部落格可以包含以下路由 app/blog/[slug]/page.js
,其中 [slug]
是部落格貼文的動態分段,下面的表格應該可以讓大家更快理解:
Route | Example URL | params |
---|---|---|
app/blog/[slug]/page.js |
/blog/a | { slug: 'a' } |
app/blog/[slug]/page.js |
/blog/b | { slug: 'b' } |
app/blog/[slug]/page.js |
/blog/c | { slug: 'c' } |
那麼我們接著將上面的概念融入我們的功能當中,首先我們在同為 charas
路徑下,新增 [charName]
的資料夾來處理我們的頁面,如下:
// src/app/charas/page.tsx
import React from 'react'
type PageProps = {
params: {
charID: string
}
}
const CharPage = ({params: { charID }}:PageProps) => {
return (
<div>
this is {charID}
</div>
)
}
export default CharPage
這樣就能夠取到路徑上的 segment,透過它再去 fetch API 拿取角色資料:
import React from 'react'
import { RickandmortyCharacter } from '../../../../typings'
import Link from 'next/link'
type PageProps = {
params: {
charID: string
}
}
const fetchCharacter = async (id: string) => {
try{
const res = await fetch(
`https://rickandmortyapi.com/api/character/${id}`,
{ next: { revalidate: 60 } } // 這裡保有 ISR 的作法
);
if(!res.ok){
const message = `An error occured: ${res.status}`;
throw new Error(message);
}
const characterInfo: RickandmortyCharacter = await res.json();
return characterInfo
} catch(err) {
console.log(err);
}
}
const CharPage = async ({params: { charID }}:PageProps) => {
const character = await fetchCharacter(charID)
return (
<div>
<h2>here is {charID}</h2>
<div>
<p>Name: {character?.name}</p>
<p>status: {character?.status}</p>
<p>species: {character?.species}</p>
<p>gender: {character?.gender}</p>
<p>origin: {character?.origin.name}</p>
<p>location: {character?.location.name}</p>
</div>
<Link
className="p-2 mx-2 rounded-lg shadow-md bg-blue-500 hover:bg-blue-300 disabled:bg-gray-100 min-w-40"
href={'/charas'}
>back</Link>
</div>
)
}
export default CharPage
那我們來處理找不到 ID 角色時的 Error Page,客製化我們的 Not found,首先在同路下新增not-found.tsx
如下:
import React from 'react'
const NotFound = () => {
return (
<div>
Character is not exist!
當然,這邊可以弄的更漂亮一點,Demo 請見諒
</div>
)
}
export default NotFound
然後我們回到 page.tsx
引入 notFound
來使用,如下:
import React from 'react'
import { RickandmortyCharacter } from '../../../../typings'
import Link from 'next/link'
import { notFound } from 'next/navigation'
type PageProps = {
params: {
charID: string
}
}
const fetchCharacter = async (id: string) => {
try{
const res = await fetch(
`https://rickandmortyapi.com/api/character/${id}`,
{ next: { revalidate: 60 } } // 這裡保有 ISR 的作法
);
if(!res.ok){
const message = `An error occured: ${res.status}`;
throw new Error(message);
}
const characterInfo: RickandmortyCharacter = await res.json();
return characterInfo
} catch(err) {
console.log(err);
}
}
const CharPage = async ({params: { charID }}:PageProps) => {
const character = await fetchCharacter(charID);
if (!character) notFound();
return (
<div>
<h2>here is {charID}</h2>
<div>
<p>Name: {character.name}</p>
<p>status: {character.status}</p>
<p>species: {character.species}</p>
<p>gender: {character.gender}</p>
<p>origin: {character.origin.name}</p>
<p>location: {character.location.name}</p>
</div>
<Link
className="p-2 mx-2 rounded-lg shadow-md bg-blue-500 hover:bg-blue-300 disabled:bg-gray-100 min-w-40"
href={'/charas'}
>back</Link>
</div>
)
}
export default CharPage
這時候如果角色 ID 找不到就會看到我們客製化的頁面了,既然保留 ISR 的操作,那麼要預先生成頁面也是沒有問題的吧!
沒錯!透過 generateStaticParams
可以讓你預先生成第一頁的畫面資料,如下:
import React from 'react'
import { RickandmortyCharacter, RickandmortyCharacterRes } from '../../../../typings'
import Link from 'next/link'
import { notFound } from 'next/navigation'
type PageProps = {
params: {
charID: string
}
}
const fetchCharacter = async (id: string) => {
try{
const res = await fetch(
`https://rickandmortyapi.com/api/character/${id}`,
{ next: { revalidate: 60 } } // 這裡保有 ISR 的作法
);
if(!res.ok){
const message = `An error occured: ${res.status}`;
throw new Error(message);
}
const characterInfo: RickandmortyCharacter = await res.json();
return characterInfo
} catch(err) {
console.log(err);
}
}
const CharPage = async ({params: { charID }}:PageProps) => {
const character = await fetchCharacter(charID);
if (!character) notFound();
return (
<div>
<h2>here is {charID}</h2>
<div>
<p>Name: {character.name}</p>
<p>status: {character.status}</p>
<p>species: {character.species}</p>
<p>gender: {character.gender}</p>
<p>origin: {character.origin.name}</p>
<p>location: {character.location.name}</p>
</div>
<Link
className="p-2 mx-2 rounded-lg shadow-md bg-blue-500 hover:bg-blue-300 disabled:bg-gray-100 min-w-40"
href={'/charas'}
>back</Link>
</div>
)
}
export default CharPage
// 這邊為了預先產出靜態檔案而做的處理
export const generateStaticParams = async () => {
const res = await fetch('https://rickandmortyapi.com/api/character');
const listData: RickandmortyCharacterRes = await res.json();
return listData.results.map((character) => ({charID: character.id}))
}
我們來嘗試多練習差不多概念的搜尋功能,邏輯是透過 input
蒐集輸入的名字參數,送出時利用 Rick and Morty API 的 filter 來實現快速搜尋角色的功能,結構上仍是利用 Dynamic Routes 的概念,那我們先在 NavBar 下新增搜尋頁面如下:
import Link from 'next/link'
import React from 'react'
const Navbar = () => {
return (
<nav className="flex justify-between items-center p-2 min-h-60 bg-blue-400">
<div><Link href={`/`}>home</Link></div>
<ul className="flex items-center">
<li className="mx-4 bg-white rounded-lg p-2 shadow-sm"><Link href={`/charas`}>角色</Link></li>
<li className="mx-4 bg-white rounded-lg p-2 shadow-sm"><Link href={`/searchCharas`}>搜尋角色</Link></li>
<li className="mx-4 bg-white rounded-lg p-2 shadow-sm"><Link href={`/locations`}>地點</Link></li>
</ul>
</nav>
)
}
export default Navbar
既然路徑開出來了,我們就接著把 page
生出來吧!
// src/app/searchCharas
import React from 'react'
const SearchCharas = () => {
return (
<div>
search page
</div>
)
}
export default SearchCharas
這次我們來練習透過 layout.tsx
來調整路徑下的頁面,那我們一樣在 /searchCharas
下面把檔案生出來如下:
import React from 'react'
import Search from './Search'
const SearchCharasLayout = ({
children,
}: {
children: React.ReactNode,
}) => {
return (
<main className="flex space-x-4 divide-x-2 p-5">
<div>
<h1>Search Characters</h1>
<Search/>
</div>
<div className="flex-1 pl-5">
<div>{children}</div>
</div>
</main>
)
}
export default SearchCharasLayout
這樣 Search
就不會因為路由的關係被清掉了,那麼我們接著處理 Search
component,如下:
"use client";
import { useRouter } from 'next/navigation'
import { FormEvent, useRef } from 'react'
const Search = () => {
const searchTerm = useRef<HTMLInputElement>(null);
const router = useRouter();
const handleSearch = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const str = searchTerm.current
router.push(`/searchCharas/${str?.value}`)
}
return (
<form onSubmit={handleSearch}>
<div>
<input
className='p-2 border-b-2 border-gary-200 mx-2'
type="text"
placeholder="Insert character's name"
ref={searchTerm}
/>
</div>
<button
type='submit'
className='bg-blue-500 text-white font-bold py-2 px-4 rounded-lg my-2'
>
Search
</button>
</form>
)
}
export default Search
這邊就如同先前文章的操作,是轉成 client component 應該很好懂,簡單來說就是在這路徑下再新增一個 Dynamic Route,那麼我們就先把資料夾生出來,這次我讓參數叫做 searchTerm
,除了 page
和 not-found
以外我多處理了一個 loading
來讓頻繁搜尋的時候不要那麼醜(這邊只是Demo,大家可以自行發揮美感處理),那麼如下:page.tsx
import React from 'react'
import { RickandmortyCharacterRes } from '../../../../typings';
import { notFound } from 'next/navigation';
import Image from 'next/image';
import Link from 'next/link';
type PageProps = {
params: {
searchTerm: string,
},
}
const searchCharacters = async (params: string) => {
try{
const res = await fetch(
`https://rickandmortyapi.com/api/character/?name=${params}`,
{ next: { revalidate: 60 } }
);
if(!res.ok){
const message = `An error occured: ${res.status}`;
throw new Error(message);
}
const characterInfo: RickandmortyCharacterRes = await res.json();
return characterInfo
} catch(err) {
console.log(err);
}
}
const SearchResults = async ({params: { searchTerm }}: PageProps) => {
const searchResult = await searchCharacters(searchTerm)
if(!searchResult) return notFound();
return (
<div>
{searchResult.results?.map((char) => (
<div className="flex items-center w-full my-2 p-4 shadow-xl rounded-lg" key={char.id}>
<div className="">
<Image src={char.image} alt={char.name} width={100} height={100}/>
</div>
<div>
<p>{char.name}</p>
<Link href={`/charas/${char.id}`}>{char.id}</Link>
</div>
</div>
))}
</div>
)
}
export default SearchResults
not-found.tsx
import React from 'react'
const NotFound = () => {
return (
<div>
Whoops... we can not find the character.
</div>
)
}
export default NotFound
loading.tsx
import React from 'react'
const Loading = () => {
return (
<div>
loading character...
</div>
)
}
export default Loading
那麼,你的列表現在就多出了一個搜尋的功能了!
今天的內容我相信也差不多了,我會把範例留在我的 GitHub 連結裏面,大家在練習的時候可以再想想怎麼讓其他功能也更加完善。