Day 17 我們介紹了 App Router 的基本路由定義方式,簡單來說,當 /app
中的資料夾裡有 page.tsx
時,Next.js 就會以該資料夾名稱建立一個 route segment,page.tsx
裡 export default 的 component 即會是該 route segment 的 UI。假如還不熟悉的讀者可以先閱讀 Day 17 的文章。
但開發時可能會碰到一種狀況:
我們無法事先確認 route segment 的名稱,或是同一層類似的 route segment 可能有多個,不太可能一個一個建立資料夾。
以電商網站舉例,我希望 /products 後可以加入一個 id 的 segment,每頁的排版、互動邏輯都差不多,主要差在商品資訊 api 的 endpoint ( id )。假如商城裡有 10,000 個產品,很顯然建 app/products/1
~ app/products/10000
不是個明智的選項,那該怎麼辦呢?
這時就可以使用動態路由 ( Dynamic Routes )!
使用方法很簡單,只需要在 dynamic segment 的資料夾名稱加上中括號 []。以上面的範例來說,dynamic segment 是 id,因此我們將資料夾名稱取為 [id],結構會長這樣:
app/
├─ products/
│ ├─ [id]/
│ │ ├─ page.tsx
那要怎麼取得 dynamic segment 的值呢? Next 會透過 params
這個 prop 將 dynamic segments 傳到 Page component 中。以上述例子來說,id 就會是 page.tsx
中的 params.id
:
/* /products/[id] */
export default function Products({ params }: { params: { id: string } }) {
return <div>Product ID: {params.id}</div>;
}
所以當你進到 /products/188 時,params.id
就會是 188;/products/256 時會是 256,以此類推。
假如 dynamic segments 不只一層,比方說 /books/fiction/sci-fi,其中 /fiction 和 /sci-fi 是 dynamic routes,那我就一定要建兩個 dynamic segments 嗎?例如:
app/
├─ books/
│ ├─ [category1]/
│ │ ├─ [category2]/
│ │ │ ├─ page.tsx
假如覺得 [category1]
和 [category2]
重複性太高,只想建一個 dynamic segment [category]
,但同時要能 catch 到 /fiction 和 /sci-fi,甚至 /sci-fi 子層的 dynamic segments,這時可以使用 catch-all 的方式來取得 children segments 中所有的 dynamic segments。
要使用 catch-all,只需要將資料夾名稱改為 [...資料夾名稱]
就好。
以上述圖書的例子,就可以將資料夾結構改為:
app/
├─ books/
│ ├─ [...category]/
│ │ ├─ page.tsx
catch-all 的 dynamic segments 會是一個 array,比方說進到 /books/fiction/sci-fi 時,params.category
就會是 [‘fiction’, ‘sci-fi’]
:
/* /books/[...category] */
export default function Books({ params }: { params: { category: string[] } }) {
console.log(params); // { category: ['fiction', 'sci-fi'] }
}
但這時你可能會發現,不管是單層的 dynamic segment 或是使用 catch-all,假如網址沒有輸入對應 [] 的 segment,就會找不到頁面。 以前面兩個案例為例,進到 /products 和 /books 就會顯示 404。
怎麼解決呢?假如是單層的 dynamic segment,那我們需要在 dynamic segment 這層也建一個 page.tsx
,負責子層沒有 route segment 時的 UI:
app/
├─ product/
│ ├─ [id]/
│ │ ├─ page.tsx
│ ├─ page.tsx
當使用者進入 /product 時,就會顯示 /product/page.tsx
中的內容。那假如子層有不只一層 dynamic segments,我們又想使用 catch-all 呢?
假如有多層的 dynamic segments,例如書城的案例,想使用 catch-all 同時也希望 /books 能讀到頁面,這時我們可以使用 optional catch-all segments!
方法很簡單,我們只需要將資料夾名稱的中括號 []
改成雙中括號 [[]]
,catch-all segments 就會變成 optional!以書城的案例來說,我們可以把 [...category]
改成 [[...category]]
:
app/
├─ books/
│ ├─ [[...category]]/
│ │ ├─ page.tsx
這時 /books/fiction/sci-fi 的 params 一樣是 [‘fiction’, ‘sci-fi’]
,而 /books 也會被 catch 到,它的 params 會是一個空的 object {}
。假如還是不太理解,可以參考下圖官方提供的範例:
( 圖片來源:https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes )
Dynamic route segment 的 params 預設會在 server 每次收到 client request 時生成,假如效能考量,希望 params 在 build time 靜態生成,可以使用官方提供的 generateStaticParams()
function,讓 params 在 build time 生成。我們來看個範例:
/* /product/[id]/page.tsx */
export function generateStaticParams() {
return [{ id: '1' }, { id: '2' }, { id: '3' }];
}
export default function Page({ params }: { params: { id: string } }) {
return <div>Product ID: {params.id}</div>;
}
這時 /product/1、/product/2、/product/3 的 params 就會在 build time 時生成,其他 id 的 params 則會在 run time server 收到 request 後生成。
假如我們希望使用者進到非 static params 的頁面時 ( 以上述例子,1,2,3 以外的 id ),會回傳 404 error 呢?我們可以調整 route segment 的設定,將 dynamicParams
設定為 false ( 預設是 true ):
/* /product/[id]/page.tsx */
export const dynamicParams = false;
export function generateStaticParams() {
return [{ id: '1' }, { id: '2' }, { id: '3' }];
}
export default function Page({ params }: { params: { id: string } }) {
return <div>Product ID: {params.id}</div>;
}
調整完後,我們 npm run dev
到 /product/99 看一下會不會顯示 404。結果沒有,網頁還是正常顯示且 params.id
也有讀到,怎麼會這樣?
不用慌張!那是因為在 dev mode 時,每次切換 route generateStaticParams() 都會被呼叫,所以就算設定 dynamicParams 為 false,依然還是讀得到 id。
假如網頁部署後 ( 超連結網頁我是用 Vercel 部署服務 ),/product/1、/product/2、/product/3 會有內容,但到 /product/99,這時就會顯示 404。
除了 dynamicParams
外,Route Segment 還有其他設定可以自訂,因為時間的關係就先不介紹,有興趣的朋友可以參考官方文件。
用 generateStaticparams()
生成的 dynamic segments 也可以不只一層:
/* /product/[category]/[id]/page.tsx */
export function generateStaticParams() {
return [
{ category: 'men', id: '1' },
{ category: 'women', id: '2' },
{ category: 'accessories', id: '3' },
];
}
export default function Page({
params,
}: {
params: { category: string; id: string };
}) {
return (
<div>
Category:{params.category} Product ID: {params.id}
</div>
);
}
也可以使用 catch-all,或是根據 API response 設定 id,如官方範例:
/* app/blog/[slug]/page.tsx */
// Return a list of `params` to populate the [slug] dynamic segment
export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then((res) => res.json())
return posts.map((post) => ({
slug: post.slug,
}))
}
// Multiple versions of this page will be statically generated
// using the `params` returned by `generateStaticParams`
export default function Page({ params }) {
const { slug } = params
// ...
}
以上就是動態路由的使用方法。學完基本和動態的路由定義後,下一步會帶大家來認識如何在 App Router 中切換路由。這部分就留到明天分享囉!
謝謝大家耐心的閱讀,我們明天見!