iT邦幫忙

2021 iThome 鐵人賽

DAY 4
0
Modern Web

從零開始學習 Next.js系列 第 4

Day04 - Next.js 的 file-based routing

Page

Next.js 的 routing 跟一般常見的 react + react-route-dom 的組合不太一樣,是採用 file-based routing。在 Next.js 中最基本的單位是 page,一個 page 就是一個 react component,component 會被放置在 pages 的資料夾中,而其檔案名稱將會決定路由的名稱。

在 Next.js 可以分為三種的 routing 方式,分別為:

  • static routes
  • dynamic routes
  • catch all routes

catch all routes 屬於 dynamic routes 的一種

Static Routes

首先是 static routes,舉一個例子,像是我們在前一天使用 create-next-app 產生的專案中,裡面有個 pages/index.tsx ,所以在瀏覽網頁時使用的路徑就會是 / 。再舉另一個例子,如果有個檔案是被放置在 pages/post.tsx ,而路徑就會是 /post;如果是 page/post/index.tsx 跟前面一樣的意思,同樣路徑會是 /post

這是最基本定義 page 的方式,用資料夾層級的方式來決定 url 會長什麼樣子,使用這種方式就不用像是 react-router-dom 需要在檔案中定義路由,還可以讓整體的程式碼更乾淨。

要注意的是,如果是 component 是一個 page,則它必須用 default export 而不是 named export

export default function Home({ post }: HomeProps) {
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </div>
  );
}

Dynamic Routes

當讀者看到這邊,肯定會疑惑像是 /post/<post-id> 這種 url 該怎麼定義呢? <post-id> 是動態的,每新增一篇貼文難道就要新增一個 component 嗎?這樣的設計未免太不人性化了吧!

Next.js 當然有相對應的解決方案,動態的 url 可以用 dynamic routes 來處理。這也跟 file-based routing 有關係,因為就是透過檔案名稱的定義方式來實現 dynamic routes,以 /post/<post-id> 這個 url 來說,可以用 [postId].tsx 定義一個 page 的名稱來達成 dynamic routes。

以下是 /pages/post/[postId].tsx 的範例程式:

import { useRouter } from "next/router";

const Post = () => {
  const router = useRouter();
  const { postId } = router.query;

  return <p>Post: {postId}</p>;
};

export default Post;

在預設的情況下,像是 /post/123 或是 /post/abc 皆可以滿足 /pages/post/[postId].tsx ,而 123abc 就會被當作是 url 的 query string,被併入到 router.query 裡面。

舉例來說, /post/123 併入到 router.query 的物件如下:

{
  postId: 123;
}

如果在 url 加上一個 query string,例如 /post/123?hello=world ,則 router.query 這個物件就會長得像這個樣子:

{ postId: 123, hello: 'world' }

而這邊要特別注意的是,假設 query string 跟路由的名稱一樣,例如 /post/123?postId=abc ,則 abc 會被 123 蓋過去,最終得到的結果如下:

{
  postId: 123;
}

多層級的路由一樣,也是透過 file-based routing 的方式定義,例如以 /post/123/abc 來說,我們就可以定義 /pages/post/<postId>/<commentId>.tsx 來匹配這個路由,在這個層級,也可以拿到前幾個層級的參數,會被統一合併到 router.query 中:

{ postId: 123, commentId: 'abc' }

catch all routes - 我全都要的路由模式

有時候在設計 url 時會遇到一種情況,並必須要在每一個層級都有代表的 component,雖然這樣很彈性,但是就要花費比較多心思撰寫更多的 component。

同樣用上方 post 的例子來說,有時候會希望 post 的 url 能夠以「年月日」來設計,所以一篇 post 的設 url 會設計成這個樣子 /pages/post/<year>/<month>/<day> ,接下來讀者可能會頭痛了,難道要定義多層級的資料夾嗎?而且最後可能還只有一個 /pages/.../day.tsx ,這樣感覺挺麻煩的。

pages/
└── posts/
    └── [year]/
        └── [month]/
            └── [day].js

這個問題 Next.js 也有相對應的解決方案,可以使用官方稱作為「catch all routes」的定義方式,一次拿到所有層級的參數。

以上方的例子,只要定義 /pages/[...date].tsx 就可以匹配「年月日」的參數,而且甚至可以無限地加上新的參數,例如顆粒度想要細到小時、分鐘、秒,都是可以的,因為 [...date] 的資料最後會以陣列被儲存在 router.query 中,例如 /post/2021/12/31 會拿到以下這個物件:

{
  date: [2021, 12, 31];
}

而再把 post 的顆粒度在切得更小的話,因為可能一天不止一篇貼文,像是 /post/2021/12/31/12/30/00 ,最終就可以拿到這樣子的物件:

{
  date: [2021, 12, 31, 12, 30, 00];
}

dynamic routes 的地雷

Next.js 是一個很棒的框架,提供了完善的 file-based routing,可以用三種不同的 pattern 定義路由的規則。但是,在使用 router.query 時要注意「第一次 render 時拿不到值」的問題,因為 Next.js 有 Automatic Static Optimization 的機制,在第一個階段 (第一次渲染) 會先執行 pre-rendering 產生靜態的 HTML,這時候 router.query 會是空的 {} ,在第二個階段 (第二次渲染) 時才能夠從 router.query 中拿到值。

以下方這個範例來說,我們可以嘗試在 pages/post/[postId].tsx 中簡單地用 console.log 檢查 postId 是否有值。

import { useRouter } from "next/router";

const Post = () => {
  const router = useRouter();
  const { postId } = router.query;
  console.log(postId);

  return <p>Post id: {postId}</p>;
};

export default Post;

從結果上來看, postId 沒辦法在第一階段時就拿到值,如果想要操作 postId 就要特別小心這個問題。

解決辦法

router.query returns undefined parameter on first render in Next.js · Discussion #11484 · vercel/next.js

在 Next.js 官方的 GitHub 上也有些人在討論這個問題,在看過大家的討論後,筆者整理了三種可行的解法,我們同樣用上從 postId 的情境。

  1. 判斷 postId 有沒有值

    const { postId } = router.query;
    
    if (!postId) {
      return <Loading />;
    }
    
  2. 使用 isReady 判斷是否能從 useRouter() 中取值 (官方不推薦將 isReady 使用在 conditionally rendering,只能被用於 useEffect 中)

    const { query, isReady } = useRouter();
    
    if (!isReady) {
      return <Loading />;
    }
    
  3. 從另一個參數 asPath 中用 regex 取值

    const { asPath } = useRouter();
    const { postId } = asPath.match(/\/post\/(?<postId>.*)/);
    

前兩種方式是判斷是否能夠從 router.query 取得值,而第三種方式則是不用 Next.js 提方的方式,改用自己寫 regex 的方法取值,但如果非必要得在第一次渲染時就取得值,否則不推薦使用第三種方式。

File-based routing Code-based routing
Next.js react + react-router-dom
不需要在程式中定義 routing 需要在程式中設定 <Router><Switch> ...
直覺地用檔案階層定義 routing 檔案放的位置不必按照規範,可能 Route 會出現在意料之外的地方
使用 <Link> 作為 routing 的 component 使用 <Link> 作為 routing 的 component

Reference


上一篇
Day03 - 深入淺出 CSR、SSR 與 SSG
下一篇
Day05 - 使用 Link 實作換頁
系列文
從零開始學習 Next.js30

尚未有邦友留言

立即登入留言