Skip to main content

Next - Page 與 Layout

Layouts 與 pages

pages 與巢狀路由

---| app/
------| /app/dashboard/
---------| /app/dashboard/invoices

每個 page.tsx 都代表一個頁面,如 /app/page.tsx 代表主頁,對應的路由是 /
同理,在 dashboard 資料夾中也建立一個 page.tsx,就可以形成路由 /dashboard
invoices 中又建立一個 page.tsx 就會形成巢狀路由 /dashboard/invoices

新增一個頁面

實作在 /app 下面新增一個資料夾 dashboard 並在其中新增 page.tsx,這樣就可以形成 /dashboard 這個路由與頁面:

/app/dashboard/page.tsx
export default function Page() {
return <p>Dashboard Page</p>;
}

新增一個 layout

---| app/
------| /app/dashboard/
---------| /app/dashboard/invoices
---------| /app/dashboard/customers

依官方範例,若在 dashboard 新增一個 layout.tsx

/app/dashboard/layout.tsx
import SideNav from '@/app/ui/dashboard/sidenav';

export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
<div className="w-full flex-none md:w-64">
<SideNav />
</div>
<div className="flex-grow p-6 md:overflow-y-auto md:p-12">{children}</div>
</div>
);
}

則此佈局套用到 dashboard 往下層次的頁面與佈局。

Root layout

在最上層的 lauout,也就是 /app/layout.tsx,又叫做 root layout,他是一個必須得存在的佈局,用來調整 <html><body>

/app/layout.tsx
import '@/app/ui/global.css';
import { inter } from '@/app/ui/fonts';

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={`${inter.className} antialiased`}>{children}</body>
</html>
);
}

路由導航

Next 提供 <Link> 元件來取代 <a> 在客戶端做路由導航。

/app/ui/dashboard/nav-links.tsx
import {
UserGroupIcon,
HomeIcon,
DocumentDuplicateIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';

// ...

export default function NavLinks() {
return (
<>
{links.map((link) => {
const LinkIcon = link.icon;
return (
<Link
key={link.name}
href={link.href}
className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3"
>
<LinkIcon className="w-6" />
<p className="hidden md:block">{link.name}</p>
</Link>
);
})}
</>
);
}
info

我覺得官方說的這一段非常有趣:

Furthermore, in production, whenever <Link> components appear in the browser's viewport, Next.js automatically prefetches the code for the linked route in the background. By the time the user clicks the link, the code for the destination page will already be loaded in the background, and this is what makes the page transition near-instant!

如果需求在畫面上顯示當前路由,或需求當前路由做一些事情 (比如下判別式),可以使用 Next 提供的 usePathname() hook 來捕獲當前路由。
但因為 usePathname() 是個 hook,因此必須在元件前面加上 'use client' 宣告它是個 client component (Next 預設是 server component)。

依官方範例,就是依照現在所在路由來改變畫面的顏色:

/app/ui/dashboard/nav-links.tsx
'use client';

import {
UserGroupIcon,
HomeIcon,
InboxIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import clsx from 'clsx';

export default function NavLinks() {
const pathname = usePathname();

return (
<>
{links.map((link) => {
const LinkIcon = link.icon;
return (
<Link
key={link.name}
href={link.href}
className={clsx(
'flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3',
{
'bg-sky-100 text-blue-600': pathname === link.href,
},
)}
>
<LinkIcon className="w-6" />
<p className="hidden md:block">{link.name}</p>
</Link>
);
})}
</>
);
}

經由函式改變路由,需使用 useRouter hook:

'use client'

import { useRouter } from 'next/navigation'

export default function Page() {
const router = useRouter()

return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}

路由種類

動態路由

基本款

[ ] 把資料夾包起來即可。

| app/
---| blog/
------| [slug]/

如要取用動態路由資訊,使用 params.slug 即可。

app/blog/[slug]/page.tsx
export default function Page({ params }: { params: { slug: string } }) {
return <div>My Post: {params.slug}</div>
}

產生多個動態路由:

export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then((res) => res.json())

return posts.map((post) => ({
slug: post.slug,
}))
}

Catch-all Segments

可以通過在括號內添加省略號 [...folderName] 來擴展捕獲所有後續的段落。

| app/
---| blog/
------| [...slug]/

官方例子為 app/shop/[...slug]/page.js 可以同時對應到:

  • /shop/clothes
  • /shop/clothes/tops
  • /shop/clothes/tops/t-shirts

Optional Catch-all Segments

與 Catch-all Segments 相似,但它允許匹配只有基礎路徑而沒有任何子路徑的路由。
以上數例子來講就是 app/shop/[[...slug]]/page.js 可以多匹配 /shop 這個路由。

平行路由

這個概念指的是在同一個網頁布局中,根據特定的條件,可以同時顯示來自不同路由的內容。
這種方式允許在不跳轉頁面的情況下,在一個頁面上展示多個不同的內容區塊或組件,適合於動態性高的應用場景,比如儀表板或社交網站的信息流。

| app/
---| @analytic/
---| @team/

官方使用範例:

app/layout.tsx
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<>
{children}
{team}
{analytics}
</>
)
}

攔截路由

Intercepting Routes 可以讓使用者在瀏覽網頁的時候,能夠在不離開當前頁面的情況下,彈出或加載另一部分的內容,比如點擊一張照片,照片以彈出視窗的形式出現,而不是跳轉到另一個頁面。
以官方例子來看,feed 資料夾下有一個 (..photo) 資料夾,可以再這裡做彈出視窗樣式,他會自動攔截 photo 這個資料夾產生的路由。

| app/
---| (..photo)/
---| feed/
------| photo/
  • (.): 用來匹配同一層級的段落。
  • (..): 用來匹配上一層級的段落。
  • (..)(..): 用來匹配兩層以上的段落。
  • (...): 用來匹配從根目錄開始的所有段落。
info

平行路由與攔截路由差別在哪?

前者更多關注於一次性展示多樣化的內容,後者則是在用戶操作中靈活切換顯示的內容,但都不會讓用戶感覺自己已經離開了原本的頁面。
基本上兩者可以組合使用。

Middleware

這是一個跟 app 資料夾同層級的檔案:

---| app/
---| middleware.ts

基本上 middleware 允許在伺服器接收請求並完成處理之前先執行一些操作,比如說登入驗證。
以官方範例為例,就是將所有訪問 /about 路徑(及其所有子路徑)的請求重定向到 /home 路徑。

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}

export const config = {
matcher: '/about/:path*',
}