[前端筆記]NextJS App 路由好像有點奇特?

NextJS App Routing Cheatsheet

Posted by 李定宇 on Friday, April 5, 2024

Cheatsheet

  • app/folderA/folderB/page.js, app路由,該router為domain/folderA/folderB
  • app/folderA/[id]/page.js,動態路由,可以匹配domain/folderA/{any id}
  • app/folderA/[...id]/... catch all下級路由,獲取任意層級的路由,等於可以匹配domain/folderA/{any id}/.../...,但如果進入domain/folderA,會得到404
  • app/folderA/[[...id]]/... ,除了catch all下級路由,也包含該級的路由(domain/folderA),不過參數 params 會為空
  • (folderA),單純加個 folderA 目錄,不包括在路由裡。
  • @name 平行路由,可以讓 layout.js 渲染多個 page.js。
  • (.)name,攔截路由,讓router和直接access的路由展示不一樣。而前面括號中的是代表相對路徑

各自舉個🌰

Basic App Routes

./app/...目錄下,每個目錄就是子route,所以可以直接用目錄、子目錄的形式直接創建嵌套路由。

Dynamic Routes-general

而在許多實際情況下,路由可能不是預先定義好、而是動態生成的,例如blog中的文章ID。如果是以https://{domain}/blog/{blogID}來進入blog文章頁面,那麼在NextJS中的App Routing便是:

.
└── app
    └── blog
        └── [blogID]
            └── page.tsx

Dynamic Routes-catch all

catch all 對動態路由提供了很大的靈活性,例如在一個電商網站,用戶可以訪問/products/electronics/phones或者更深一層的/products/electronics/phones/smartphones,都可以用單一組件、然後以catch all來捕捉到、然後根據路由來動態展示。以上述為例,如果app routing為:

.
└── app
    └── products
        └── [...data]
            └── page.tsx

那麼在/products/electronics/phones路由中,params便為

{ data: ["electronics", "phones"] }

如果是products/electronics/phones/smartphones,那麼params則是:

{ data: ["electronics", "phones", "smartphones"] }

但是,如果是以這個目錄來說,直接在browser鍵入{demain}/products,會是 404

Dynamic Routes-optional catch all

動態路由中的 Optional Catch-all,就是為了解決這個問題。如果把目錄改為:

.
└── app
    └── products
        └── [[...data]]
            └── page.tsx

那麼就可以直接進入 {demain}/products,只不過此時的params為空對象{}

Routes Group

這算是方便專案folder組織管理。假設現在是有一個需要登入的dashboard專案,sign-in、sign-up是用同一個layout,而dashboard、services、settings是用同一個layout,在不用新增主路由的要求下(都是一級路由),那麼目錄結構應該如下:

.
└── app
    ├── dashboard
    ├── services
    ├── settings
    ├── sign-in
    └── sign-up

然而,如果再加上 layout 的要求,就會有很多重複程式碼:

.
└── app
    ├── dashboard
    │   ├── layout.tsx
    │   └── page.tsx
    ├── services
    │   ├── layout.tsx
    │   └── page.tsx
    ├── settings
    │   ├── layout.tsx
    │   └── page.tsx
    ├── sign-in
    │   ├── layout.tsx
    │   └── page.tsx
    └── sign-up
        ├── layout.tsx
        └── page.tsx

在以上情境下,如果使用Routes Group的方式來寫的話,可讀性就會改善很多,同時也可以減少重複的 layout.tsx

.
└── app
    ├── (auth)
    │   ├── layout.tsx
    │   ├── sign-in
    │   │   └── page.tsx
    │   └── sign-up
    │       └── page.tsx
    └── (main)
        ├── dashboard
        │   └── page.tsx
        ├── layout.tsx
        ├── services
        │   └── page.tsx
        └── settings
            └── page.tsx

Parallel Routes

我覺得平行路由這個部分,寫起來很像是Vue中的<slot/>,是可以在同級的layout.tsx(最近的父layout)中獲取從目錄中定義好的@folderName,然後從props中解構獲取到,以下面的目錄結構為例:

.
└── app
    ├── @auth
    │   └── page.tsx
    ├── @user
    │   └── page.tsx
    ├── layout.tsx
    └── page.tsx

然後便可以在 layout.tsx 中獲取到該命名插槽:

export default function Layout({
  children,
  auth,
  user,
}: {
  children: ReactNode
  auth: ReactNode
  user: ReactNode
}) {
  return (
    <>
      {children}
      {auth}
      {user}
    </>
  )
}

這樣一來,authuser 如果有自己的loading.tsx,就可以各自執行各自的stream ssr;另外,也可以在layout.tsx中進行條件渲染(假設 auth 和 user需要不同的佈局)

export default function Layout({
  children,
  auth,
  user,
}: {
  children: ReactNode
  auth: ReactNode
  user: ReactNode
}) {
  return (
    <>
      {children}
      { condition? auth : user}
    </>
  )
}

不過我認為,這算是要熟悉NextJS的寫法才會覺得比較方便,不然其實直接寫 Suspense 和 條件渲染,其實也不是不行吧?例如把目錄變為

.
└── app
    ├── _components
    │   ├── auth-loding.tsx
    │   ├── auth.tsx
    │   ├── user-loading.tsx
    │   ├── user.tsx
    │   └── index.tsx
    ├── layout.tsx
    └── page.tsx

然後在layout.tsx中自己寫<Suspense/>

import { Suspense } from 'react'
import { Auth, AuthLoading, User, UserLoading } from './_components'
 
export default function Layout({children}:{children:ReactNode}) {
  return (
    <>
      {children}
      <Suspense fallback={<AuthLoading/>}>
        <Auth />
      </Suspense>
      <Suspense fallback={<UserLoading/>}>
        <User />
      </Suspense>
    </>
  )
}

應該也是可以的吧?而條件渲染就更不用說了。這個部分可能要再多研究一下。

Intercepting Routes

目前筆者還沒有使用到過,就先簡單demo一下:

  • 目錄結構:

    .
    └── app
    └── folderA
        ├── (.)folderB
        │   └── page.tsx
        ├── folderB
        │   └── page.tsx
        └── page.tsx
    
  • folderA/page.tsx:

    "use client";
    import { useRouter } from "next/navigation";
    
    export default function Page() {
    const router = useRouter();
    const goToFolderB = () => {
    router.push("folderA/folderB");
    };
    return (
    <div>
      <h1>FolderA</h1>
      <button onClick={goToFolderB}>route to folderB</button>
    </div>
    );
    }
    
  • folderA/folderB/page.tsx

    export default function Page() {
    return <h1>Original FolderB</h1>;
    }
    
  • folderA/(.)folderB/page.tsx

    export default function Page() {
    return <h1>(.) Intercepted FolderB</h1>;
    }
    

在上述demo專案中,如果是在{domain}/folderA 的頁面中,點擊按鈕跳轉到{domain}/folderA/folderB 頁面到話,內文會顯示 (.) Intercepted FolderB;但如果是直接從瀏覽器中輸入{domain}/folderA/folderB而進入該頁面的話,則會顯示 Original FolderB,代表在有上下文的情況下(路由跳轉之類的),路由會被攔截而渲染 (.)folder/page.tsx

結語

使用NextJS的App Routing,前期需要點時間習慣一下該邏輯(如果照自己的邏輯來寫的話,可能會有些重複性程式碼),習慣過後會發現NextJS真的為工程師使用者體驗、前端頁面情境等等做了很多設想及優化。希望可以用NextJS來寫更多有趣的專案:)

Ref

ChangeLog

  • 20240405-初稿