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


<<:  D-11, Ruby 正规表达式(三) 字符 && Regular Expression Matching

>>:  Day11-pod.jpg Pod建立与使用

5. bind, call, apply 的差异

在回答问题前,我们可以先了解他们是做什麽用的,为什麽总是拿来被比较? 这里要先回忆一个观念: JS里...

【左京淳的JAVA WEB学习笔记】第十六章 分页功能(查询用户购买纪录)

後台 管理员能在後台页面查询用户购买纪录及明细 第一次进入此页面时无参数,在表单填入以下资讯後返回结...

【Day20】浅层复制及深层复制

在 JavaScript 中,物件型别是利用传参考的方式来传递它的值, 因此当我们要复制出独立的物件...

JAVA 语言

https://wolkesau.medium.com/java-语言-8e8158d75b5d J...

Day 5: AWS上的NIST资安五大面向

美国政府在2014年为了加强公部门的资安防护能力,颁布了NIST (NIST Cybersecuri...