目錄
- 什麼是進階路由?
- Parallel Routes(@slot)
- Intercepting Routes
- Route Handlers 進階
- Middleware 基礎
- matcher 與條件
- Edge Runtime 限制
- Cookies / Headers / Redirects
- 實戰範例
- 常見問題
- 總結
- 參考資源
什麼是進階路由?
「進階路由」涵蓋 App Router 提供的非基礎檔案路由功能:
| 功能 | 解決什麼問題 |
|---|---|
| Parallel Routes | 同一頁面並排顯示多個獨立路由(儀表板、雙側欄) |
| Intercepting Routes | 攔截導航用 modal 開啟而非真的跳頁 |
| Route Handlers | 寫 HTTP API(取代 Pages Router 的 /pages/api) |
| Middleware | 在 request 到達頁面之前全域攔截(auth、A/B test、語系) |
基礎路由(file-based、dynamic、catch-all、route group)見 basics.md 路由與導航入門。
Parallel Routes(@slot)
概念
讓 layout 可以同時 render 多個獨立的路由區塊,每個區塊有自己的 loading / error 狀態。
檔案結構
app/dashboard/
├── layout.tsx
├── page.tsx ← 預設 children
├── @analytics/
│ ├── page.tsx ← @analytics slot
│ ├── loading.tsx
│ └── error.tsx
└── @team/
├── page.tsx ← @team slot
├── loading.tsx
└── error.tsx
Layout 接收 slot 作為 prop
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div className="grid grid-cols-2 gap-4">
<div>{children}</div>
<div>{analytics}</div>
<div>{team}</div>
</div>
);
}
每個 slot 都是獨立路由:
@analytics/page.tsx對應/dashboard@analytics/loading.tsx只影響@analytics區塊
default.tsx
當 slot 沒匹配到任何路由時,會 render default.tsx:
app/dashboard/
├── @analytics/
│ ├── page.tsx ← 對應 /dashboard
│ ├── views/page.tsx ← 對應 /dashboard/views
│ └── default.tsx ← 其他路徑下 fallback
當 URL 是 /dashboard/team-settings 但 @analytics 沒對應路由時,會 render @analytics/default.tsx。
使用情境
- 儀表板的並排 widgets
- 兩個獨立載入速度的區塊(一個快、一個慢)
- 條件式渲染(登入/未登入顯示不同 slot)
Intercepting Routes
概念
當使用者點擊連結時,攔截導航行為,改成 modal 開啟。但直接打 URL 仍會走完整頁面。
約定符號
| 符號 | 意義 |
|---|---|
(.) |
同層攔截 |
(..) |
上一層攔截 |
(..)(..) |
上兩層攔截 |
(...) |
從 root 攔截 |
範例:相簿 modal
app/
├── photos/
│ └── [id]/
│ └── page.tsx ← 完整頁面 /photos/123
└── feed/
├── page.tsx ← feed 頁
└── @modal/
└── (..)photos/
└── [id]/
└── page.tsx ← 從 feed 攔截 /photos/123 → 開 modal
配合 Parallel Routes
通常 Intercepting 跟 @modal slot 一起用:
// app/feed/layout.tsx
export default function FeedLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<>
{children}
{modal}
</>
);
}
// app/feed/@modal/default.tsx
export default function Default() {
return null; // 沒 modal 時不 render 任何東西
}
// app/feed/@modal/(..)photos/[id]/page.tsx
import Modal from "@/components/Modal";
export default async function PhotoModal({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const photo = await getPhoto(id);
return (
<Modal>
<img src={photo.url} alt={photo.title} />
</Modal>
);
}
使用者體驗
- 從 feed 點縮圖 → modal 開啟(不離開 feed)
- 直接訪問
/photos/123→ 看到完整頁面 - 在 modal 重整頁面 → 自動 fallback 到完整頁面
- 分享 URL 給朋友 → 朋友看到完整頁面
Route Handlers 進階
基本結構
app/api/foo/route.ts 可以 export 多個 HTTP method handler:
// app/api/posts/route.ts
import { NextResponse } from "next/server";
export async function GET() {
const posts = await db.post.findMany();
return NextResponse.json(posts);
}
export async function POST(request: Request) {
const body = await request.json();
const post = await db.post.create({ data: body });
return NextResponse.json(post, { status: 201 });
}
export async function DELETE(request: Request) {
const { id } = await request.json();
await db.post.delete({ where: { id } });
return NextResponse.json({ ok: true });
}
動態 Route Handler
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const post = await db.post.findUnique({ where: { id } });
return NextResponse.json(post);
}
Request / Response 進階
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
// 1. Headers
const auth = request.headers.get("authorization");
// 2. Query params
const query = request.nextUrl.searchParams.get("q");
// 3. Cookies(read-only 在 GET,可寫於 response)
const token = request.cookies.get("token");
// 4. Body 多種讀法
const json = await request.json();
// const text = await request.text();
// const form = await request.formData();
// 5. Response 設定
return NextResponse.json(
{ ok: true },
{
status: 200,
headers: {
"X-Custom": "value",
"Cache-Control": "no-store",
},
}
);
}
Streaming Response
export async function GET() {
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
controller.enqueue(new TextEncoder().encode(`chunk ${i}\n`));
await new Promise((r) => setTimeout(r, 100));
}
controller.close();
},
});
return new Response(stream, {
headers: { "Content-Type": "text/plain" },
});
}
Route Handler 與 Server Action 的選擇
| 情境 | 用 |
|---|---|
| 同站表單 mutation | Server Actions |
| 對外開放 API(第三方 / mobile app) | Route Handlers |
| Webhook 接收端 | Route Handlers |
| 檔案上傳 / streaming | Route Handlers |
| 內部 BFF(給自己 client 用) | Server Actions 或 Route Handlers 都行 |
Server Actions 細節見 server-actions.md
Middleware 基礎
是什麼
Middleware 在 request 到達頁面/route handler 之前執行,可以:
- 重寫 URL(rewrite)
- 重新導向(redirect)
- 設定 / 讀取 cookies & headers
- 拒絕請求
檔案位置
middleware.ts 放在專案根目錄(與 app/ 同層,若用 src/ 則在 src/middleware.ts):
project/
├── src/
│ ├── app/
│ └── middleware.ts ← 在這裡
├── next.config.ts
└── package.json
基本範例
// src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
// 1. 讀 cookies
const token = request.cookies.get("token")?.value;
// 2. 未登入訪問受保護路徑 → redirect
if (request.nextUrl.pathname.startsWith("/dashboard") && !token) {
return NextResponse.redirect(new URL("/login", request.url));
}
// 3. 其他情況放行
return NextResponse.next();
}
三種回應方式
// 放行
return NextResponse.next();
// 重新導向(會更新 URL)
return NextResponse.redirect(new URL("/login", request.url));
// 重寫(URL 不變,內部 render 其他路徑)
return NextResponse.rewrite(new URL("/maintenance", request.url));
在 Middleware 設定 headers / cookies
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// 設 response header
response.headers.set("x-custom-header", "value");
// 設 cookie
response.cookies.set("visited", "true", {
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24,
});
return response;
}
matcher 與條件
config.matcher 過濾要套用的路徑
// src/middleware.ts
export const config = {
matcher: ["/dashboard/:path*", "/admin/:path*"],
};
多個 matcher
export const config = {
matcher: [
"/api/:path*",
"/dashboard/:path*",
"/((?!_next/static|_next/image|favicon.ico).*)", // 排除特定路徑
],
};
條件式判斷(matcher 內部)
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 不同路徑做不同處理
if (pathname.startsWith("/api/")) {
// API 路徑加 rate limit
return handleRateLimit(request);
}
if (pathname.startsWith("/admin/")) {
return handleAdminAuth(request);
}
return NextResponse.next();
}
配合 has / source 條件
export const config = {
matcher: [
{
source: "/api/:path*",
has: [{ type: "header", key: "x-api-key" }],
},
],
};
Edge Runtime 限制
Middleware 預設在 Edge Runtime 執行(不是 Node.js)。Edge 環境有限制:
不可用 / 受限的 API
| 不可用 | 替代 |
|---|---|
fs、path |
Edge 上沒檔案系統 |
crypto(Node 版本) |
Web Crypto API (globalThis.crypto) |
| 部分 npm 套件(依賴 Node API) | 找 Edge-compatible 替代 |
setTimeout 長時間 |
Edge function 執行時間有限 |
| 直接連 PostgreSQL(傳統 TCP) | 用 HTTP-based DB(Neon、Supabase REST) |
容量限制
- Middleware bundle size 通常需 < 1MB(部署平台規定)
- 執行時間 < 30s(Vercel 預設)
何時切換到 Node Runtime
需要用 Node API 的話,可在 page / route handler 內指定:
// page.tsx 或 route.ts
export const runtime = "nodejs"; // 預設
// 或
export const runtime = "edge";
但 Middleware 目前只能在 Edge(Next.js 16 仍是如此)。
Cookies / Headers / Redirects
在 Server Component 讀 cookies / headers
import { cookies, headers } from "next/headers";
export default async function Page() {
// Next.js 15+ 是 async
const cookieStore = await cookies();
const headerStore = await headers();
const userId = cookieStore.get("uid")?.value;
const userAgent = headerStore.get("user-agent");
return <div>UID: {userId}</div>;
}
一旦用了
cookies()或headers(),整頁變 dynamic(詳見 rendering-and-cache.md)
在 Server Action 內寫 cookies
"use server";
import { cookies } from "next/headers";
export async function login(formData: FormData) {
// ... 驗證
const cookieStore = await cookies();
cookieStore.set("token", "xxx", {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7,
});
}
redirect 與 permanentRedirect
import { redirect, permanentRedirect } from "next/navigation";
// 302 暫時導向
redirect("/login");
// 308 永久導向(SEO 用)
permanentRedirect("/new-url");
notFound
import { notFound } from "next/navigation";
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await getPost(id);
if (!post) notFound(); // 顯示最近的 not-found.tsx
return <article>{post.title}</article>;
}
實戰範例
範例 1:完整登入流程(Middleware + Route Handler + Server Action)
// src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const PROTECTED = ["/dashboard", "/settings", "/admin"];
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const isProtected = PROTECTED.some((p) => pathname.startsWith(p));
if (!isProtected) return NextResponse.next();
const token = request.cookies.get("token")?.value;
if (!token) {
const url = request.nextUrl.clone();
url.pathname = "/login";
url.searchParams.set("from", pathname);
return NextResponse.redirect(url);
}
// 驗證 token(簡化版 — 實際應該驗 signature)
try {
await verifyToken(token);
return NextResponse.next();
} catch {
return NextResponse.redirect(new URL("/login", request.url));
}
}
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*", "/admin/:path*"],
};
// app/login/actions.ts
"use server";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
export async function login(prevState: unknown, formData: FormData) {
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const user = await db.user.findUnique({ where: { email } });
if (!user || !(await verifyPassword(password, user.passwordHash))) {
return { error: "帳密錯誤" };
}
const token = await signToken({ uid: user.id });
(await cookies()).set("token", token, {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7,
});
redirect("/dashboard");
}
範例 2:A/B Test Middleware
// src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname !== "/") return NextResponse.next();
// 已經分組過
let variant = request.cookies.get("ab-variant")?.value;
if (!variant) {
variant = Math.random() < 0.5 ? "A" : "B";
}
// Rewrite 到對應 variant
const url = request.nextUrl.clone();
url.pathname = `/variants/${variant}`;
const response = NextResponse.rewrite(url);
response.cookies.set("ab-variant", variant, { maxAge: 60 * 60 * 24 * 30 });
return response;
}
export const config = { matcher: "/" };
範例 3:相簿 Modal(Intercepting Routes)
app/
├── layout.tsx
├── @modal/
│ ├── default.tsx ← null
│ └── (..)photos/[id]/page.tsx
├── photos/[id]/page.tsx ← 完整頁面
└── feed/page.tsx ← 點縮圖觸發攔截
// app/layout.tsx
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<html><body>
{children}
{modal}
</body></html>
);
}
// app/@modal/default.tsx
export default function Default() {
return null;
}
// app/@modal/(..)photos/[id]/page.tsx
import Modal from "@/components/Modal";
export default async function PhotoModal({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const photo = await getPhoto(id);
return (
<Modal>
<img src={photo.url} alt={photo.title} />
<p>{photo.caption}</p>
</Modal>
);
}
常見問題
問題 1:Middleware 沒被執行
檢查清單:
- 檔案位置正確?(
src/middleware.ts或middleware.ts在 project root,不是src/app/middleware.ts) matcher配置是否包含目標路徑?- 是否有
export const config?
問題 2:Middleware 內無法用 Prisma / DB
原因:Edge Runtime 不支援傳統 DB driver
解決方案:
- 改用 HTTP-based DB(Neon、Supabase)
- 把 DB 操作移到 page / route handler(Node runtime)
- Middleware 只驗 JWT,不查 DB
問題 3:Parallel Routes slot 顯示舊內容
原因:缺少 default.tsx,導航後該 slot 沒匹配新路徑
解決方案:每個 slot 都加 default.tsx:
// 至少要 export 一個 default function
export default function Default() {
return null;
}
問題 4:Intercepting Routes 路徑層數搞錯
規則:
(.)= 同層(..)= 上一層(..)(..)= 上兩層(...)= 從 root
⚠️ route group 不算層級:app/(marketing)/photos 的「上一層」對攔截來說還是 root。
問題 5:Route Handler 在 dev 模式吃快取
原因:開發環境某些 Route Handler 預設不快取,但 production 會
解決方案:明確設定
export const dynamic = "force-dynamic";
// 或
export const revalidate = 0;
總結
核心要點
App Router 進階 = Parallel Routes + Intercepting Routes + Middleware + Route Handlers
使用情境速查:
- 並排儀表板 → Parallel Routes (
@slot) - 點 thumbnail 開 modal → Intercepting Routes (
(..)) - 全站 auth / A/B / 語系 → Middleware
- 對外 API / webhook / streaming → Route Handlers
符號速查
| 符號 | 用途 |
|---|---|
@name |
Parallel Routes slot |
(.)foo |
攔截同層 foo |
(..)foo |
攔截上一層 foo |
(...)foo |
攔截 root 層 foo |
(name) |
Route Group(不影響 URL,跟攔截無關) |
Middleware 設計準則
- ✅ 短小、純判斷邏輯
- ✅ 配合
matcher限制範圍 - ✅ 用 HTTP-based service 取代直連 DB
- ❌ 不要做重量級資料處理(Edge runtime 有時間限制)
- ❌ 不要存放秘密金鑰(會打包進 edge bundle)
相關閱讀
- Next.js 入門指南 — 基礎路由
- 渲染與快取 —
cookies()/headers()對渲染的影響 - Server Actions 與表單 — Route Handler vs Server Action 取捨
- 部署與 Runtime — Edge vs Node runtime 深入
參考資源
- 官方路由文件:https://nextjs.org/docs/app/building-your-application/routing
- Parallel Routes:https://nextjs.org/docs/app/building-your-application/routing/parallel-routes
- Intercepting Routes:https://nextjs.org/docs/app/building-your-application/routing/intercepting-routes
- Middleware:https://nextjs.org/docs/app/building-your-application/routing/middleware
- Route Handlers:https://nextjs.org/docs/app/building-your-application/routing/route-handlers
建立日期:2026-05-16 最後更新:2026-05-16