目錄
- 什麼是 Next.js?
- 核心概念
- 快速開始
- 專案結構與檔案約定
- 路由與導航入門
- 頁面、Layout、Loading、Error
- Server / Client Components 心智模型
- 資料擷取入門
- 系列導讀
- 常見問題
- 總結
- 參考資源
什麼是 Next.js?
Next.js 是基於 React 的全端 Web 框架,由 Vercel 維護。它在 React 的基礎上補齊了路由、伺服器渲染、資料擷取、優化、部署等實務需求,讓開發者可以用一個工具完成從前端 UI 到後端 API 的完整應用。
核心特點
- 🎯 檔案系統路由:把資料夾結構當作 URL 結構,無需手動配置路由
- ⚡ Server Components 預設:頁面預設在伺服器端渲染,產出更小的 JS bundle
- 🔧 內建優化:圖片、字型、腳本、Bundle 都有自動優化機制
- 📦 全端整合:同一專案內寫 Route Handlers、Server Actions,前後端共用型別
- 🚀 多種渲染模式:SSG、SSR、ISR、PPR 任你選,甚至可在同一頁混用
為什麼選擇 Next.js?
純 React 的問題:
- 沒有官方路由方案(需自選 React Router 等)
- 沒有 SSR/SSG 開箱即用
- 圖片、字型、SEO meta 都要自己處理
- 部署需要自己配置 Webpack、SSR server
Next.js 的優勢:
- 上述問題全部內建解決
- SEO 友善(首屏 HTML 直接由伺服器產生,爬蟲拿得到完整內容)
- 部署到 Vercel 是 zero-config,自架也支援 Docker standalone 模式
- 大量內建效能優化(自動 code split、image lazy load、font subset)
核心概念
理解 Next.js 之前,先建立 4 個心智模型:
1. App Router vs Pages Router
Next.js 路由演進:
Pages Router(舊版) App Router(13+ 推薦)
├── pages/ ├── app/
│ ├── index.tsx │ ├── page.tsx
│ ├── about.tsx │ ├── about/
│ └── api/hello.ts │ │ └── page.tsx
└── 用 getServerSideProps │ └── api/hello/route.ts
getStaticProps 取資料 └── 直接在 Server Component 內 await fetch
本系列只談 App Router(Next.js 13+ 推薦,14/15/16 持續強化)。
2. 四種渲染模式
| 模式 | 縮寫 | 何時渲染 | 適用場景 |
|---|---|---|---|
| 靜態生成 | SSG | 建構時 | 部落格、文件、行銷頁 |
| 伺服器渲染 | SSR | 每次請求 | 個人化頁面、需即時資料 |
| 增量靜態再生 | ISR | 建構 + 背景重生 | 半靜態(商品頁、新聞) |
| 部分預渲染 | PPR | 建構 + Streaming | 上述混合(Next.js 15+ 實驗) |
→ 深入解析見 rendering-and-cache.md
3. Server Components 與 Client Components
- Server Component(預設):在伺服器跑、不會送 JS 到瀏覽器、可以直接讀資料庫 / 環境變數
- Client Component(需
"use client"):傳統 React 元件、有 state、可用 hooks、會被打包送到瀏覽器
→ 邊界判斷見本篇 Server / Client Components 心智模型
4. 全端框架 = 前端 + Route Handlers + Server Actions
Next.js 不只是前端工具,它包含:
- 前端頁面(
page.tsx) - HTTP API(
route.ts— Route Handlers) - 直接從前端呼叫伺服器函式(Server Actions)
→ Server Actions 完整介紹見 server-actions.md
快速開始
環境需求
- Node.js 18.18+(建議 20 LTS 以上)
- npm / pnpm / yarn / bun 任一
建立新專案
# 使用官方 CLI(推薦)
npx create-next-app@latest my-app
# 互動式選項(推薦選擇)
✔ Would you like to use TypeScript? Yes
✔ Would you like to use ESLint? Yes
✔ Would you like to use Tailwind CSS? Yes
✔ Would you like your code inside a `src/` directory? Yes
✔ Would you like to use App Router? Yes
✔ Would you like to use Turbopack? Yes
✔ Would you like to customize the import alias? No
啟動開發伺服器
cd my-app
npm run dev
# 瀏覽 http://localhost:3000
基本指令
| 指令 | 用途 |
|---|---|
npm run dev |
啟動開發伺服器(含 HMR) |
npm run build |
建構正式版 |
npm start |
啟動正式伺服器 |
npm run lint |
執行 ESLint 檢查 |
第一個範例
新建 src/app/hello/page.tsx:
export default function HelloPage() {
return (
<main className="p-8">
<h1 className="text-2xl font-bold">Hello, Next.js</h1>
<p>這是我的第一個 Next.js 頁面</p>
</main>
);
}
瀏覽 http://localhost:3000/hello 即可看到結果。注意:建立檔案後 URL 立即可用,不需要任何路由設定。
專案結構與檔案約定
標準目錄結構
my-app/
├── src/
│ └── app/ # App Router 核心目錄
│ ├── layout.tsx # 根 Layout(必須)
│ ├── page.tsx # 首頁 /
│ ├── globals.css # 全域樣式
│ ├── about/
│ │ └── page.tsx # /about
│ ├── blog/
│ │ ├── page.tsx # /blog
│ │ └── [slug]/
│ │ └── page.tsx # /blog/:slug
│ └── api/
│ └── hello/
│ └── route.ts # API 端點 /api/hello
├── public/ # 靜態檔案(圖片、favicon)
├── next.config.ts # Next.js 設定檔
├── tsconfig.json
└── package.json
特殊檔名約定
App Router 用檔名而非設定來定義行為,每個 app/* 資料夾下可以有這些檔案:
| 檔名 | 用途 |
|---|---|
page.tsx |
該路由的 UI(讓路由可被存取) |
layout.tsx |
包裹子頁面的共用 Layout |
loading.tsx |
載入中的 fallback UI(自動套 Suspense) |
error.tsx |
錯誤邊界 UI(自動套 ErrorBoundary) |
not-found.tsx |
404 UI |
route.ts |
HTTP API 端點(Route Handler) |
template.tsx |
類似 Layout 但每次導航重新 mount |
default.tsx |
Parallel Routes 的預設 slot |
app/
└── blog/
├── layout.tsx # /blog/* 共用
├── page.tsx # /blog
├── loading.tsx # /blog 載入時顯示
├── error.tsx # /blog 發生錯誤時顯示
└── [slug]/
├── page.tsx # /blog/:slug
└── not-found.tsx # 找不到該文章時顯示
路由與導航入門
基本路由(File-based Routing)
資料夾名稱 = URL segment,內部一定要有 page.tsx 才會變成可訪問路由。
app/
├── page.tsx → /
├── about/page.tsx → /about
├── blog/page.tsx → /blog
└── contact/page.tsx → /contact
動態路由 [param]
app/blog/[slug]/page.tsx → /blog/anything
// app/blog/[slug]/page.tsx
type Props = {
params: Promise<{ slug: string }>;
};
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
return <h1>文章:{slug}</h1>;
}
Next.js 15+ 變更:
params變成 Promise,必須await。舊版直接params.slug不再適用。
Catch-all 路由 [...slug]
app/docs/[...slug]/page.tsx
匹配 /docs/a、/docs/a/b、/docs/a/b/c...
const { slug } = await params; // slug: string[] = ["a", "b", "c"]
選擇性 Catch-all [[...slug]]
雙層方括號代表「也匹配無參數」,所以 /docs 本身也會被吃到。
Route Groups (name)
用括號包資料夾 → 不會出現在 URL,純粹組織用:
app/
├── (marketing)/
│ ├── about/page.tsx → /about
│ └── pricing/page.tsx → /pricing
└── (shop)/
├── cart/page.tsx → /cart
└── checkout/page.tsx → /checkout
用途:
- 為不同區塊套用不同 Layout
- 純粹分類用,不影響 URL
導航:<Link> 與 useRouter
靜態跳轉用 <Link>(Server Component 也能用):
import Link from "next/link";
export default function Nav() {
return (
<nav>
<Link href="/">首頁</Link>
<Link href="/about">關於</Link>
<Link href="/blog/hello">文章</Link>
</nav>
);
}
程式化跳轉用 useRouter(限 Client Component):
"use client";
import { useRouter } from "next/navigation";
export default function LoginButton() {
const router = useRouter();
return (
<button onClick={() => router.push("/dashboard")}>
登入
</button>
);
}
進階路由功能(Parallel Routes、Intercepting Routes、Middleware、Route Handlers 細節)→ 詳見 routing-advanced.md
頁面、Layout、Loading、Error
Page
page.tsx 是該 URL 的進入點:
// app/page.tsx
export default function HomePage() {
return <h1>歡迎</h1>;
}
Layout
layout.tsx 包裹同層與子層所有頁面,不會在導航時重新 mount:
// app/layout.tsx — 根 Layout(必須有)
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-TW">
<body>
<header>共用 header</header>
<main>{children}</main>
<footer>共用 footer</footer>
</body>
</html>
);
}
Layout 可以巢狀:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<aside>側邊選單</aside>
<section>{children}</section>
</div>
);
}
Loading
loading.tsx 會被 Next.js 自動包進 <Suspense>,當該路由的 Server Component 還在取資料時顯示:
// app/blog/loading.tsx
export default function Loading() {
return <div>載入文章中...</div>;
}
Error
error.tsx 必須是 Client Component(要用 React error boundary):
// app/blog/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>出錯了!</h2>
<p>{error.message}</p>
<button onClick={reset}>重試</button>
</div>
);
}
Server / Client Components 心智模型
兩種元件並存
App Router 裡,元件預設是 Server Component。要變成 Client Component,要在檔案最上方加 "use client":
// app/products/page.tsx — Server Component(預設)
import db from "@/lib/db";
export default async function ProductsPage() {
const products = await db.product.findMany(); // 直接連資料庫
return <ProductList products={products} />;
}
// app/components/Counter.tsx — Client Component
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
邊界判斷指南
何時要 "use client":
- 用到
useState/useEffect/ 其他 hooks - 處理使用者互動(onClick、onChange)
- 使用瀏覽器 API(window、localStorage)
- 整合純客戶端套件(如某些圖表庫)
何時保持 Server Component:
- 純展示資料的元件
- 需要直接 fetch / 讀資料庫 / 讀環境變數
- 想要更小的 JS bundle
- 內容對 SEO 重要(伺服器產生 HTML)
邊界注意事項
// ❌ 錯誤:Client Component 不能直接 import Server Component 並執行
"use client";
import ServerComponent from "./ServerComponent"; // 編譯時就會錯
// ✅ 正確:Server Component 可以放 Client Component
// app/page.tsx (Server)
import ClientCounter from "./Counter";
export default function Page() {
return (
<div>
<h1>伺服器渲染的標題</h1>
<ClientCounter /> {/* OK,Client 元件可被 Server 引用 */}
</div>
);
}
// ✅ 正確:透過 children 把 Server 元件「傳」進 Client 元件
"use client";
export default function ClientWrapper({
children,
}: {
children: React.ReactNode;
}) {
return <div className="border">{children}</div>;
}
// 使用方
<ClientWrapper>
<ServerOnlyContent /> {/* OK */}
</ClientWrapper>
完整邊界規則、Streaming、PPR、四層快取 → 詳見 rendering-and-cache.md
資料擷取入門
Server Component 直接 await fetch
App Router 最大的改變:Server Component 可以是 async function,直接在元件內 await:
// app/posts/page.tsx
type Post = { id: number; title: string };
export default async function PostsPage() {
const res = await fetch("https://api.example.com/posts");
const posts: Post[] = await res.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
fetch 的 Next.js 擴充
Next.js 對 fetch 加了三個關鍵選項:
// 1. 靜態快取(預設)
fetch("/api/data", { cache: "force-cache" });
// 2. 不快取(每次請求都打)
fetch("/api/data", { cache: "no-store" });
// 3. 定時重新驗證
fetch("/api/data", { next: { revalidate: 60 } }); // 60 秒重抓
Client Component 取資料
Client Component 用標準 React 方式(useEffect 或 SWR / TanStack Query):
"use client";
import { useEffect, useState } from "react";
export default function ClientPosts() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch("/api/posts")
.then((r) => r.json())
.then(setPosts);
}, []);
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}
完整快取機制(4 層快取、revalidateTag、revalidatePath、unstable_cache)→ 見 rendering-and-cache.md
資料寫入(表單、Server Actions、樂觀更新)→ 見 server-actions.md
系列導讀
本筆記是 7 篇系列的第一篇,建立基礎心智模型後,依需求選讀:
| 想了解什麼 | 該讀哪篇 |
|---|---|
| 渲染模式怎麼運作?四層快取是什麼? | rendering-and-cache.md |
| 怎麼處理表單與資料寫入? | server-actions.md |
| 怎麼做 SEO、OG 圖、sitemap? | metadata-and-seo.md |
| Parallel Routes、Middleware 進階用法? | routing-advanced.md |
| 圖片、字型優化、Bundle 分析? | assets-and-performance.md |
| 部署到 Vercel / Docker / 環境變數管理? | deployment-and-runtime.md |
推薦閱讀順序:
- 本篇(基礎)→ 跑得起 hello world
rendering-and-cache.md→ 理解效能與資料流核心server-actions.md→ 學會寫互動表單- 其他四篇依專案需求挑讀
常見問題
問題 1:建立檔案後 URL 顯示 404
症狀:在 app/foo/ 建了資料夾卻 404
原因:資料夾內必須有 page.tsx 才會變成可訪問路由
解決方案:
# 加上 page.tsx
touch app/foo/page.tsx
問題 2:用了 useState 卻噴錯
症狀:
You're importing a component that needs `useState`. This React Hook only works in a Client Component.
原因:Server Component(預設)不能用 hooks
解決方案:檔案最上方加 "use client":
"use client";
import { useState } from "react";
問題 3:dynamic route 的 params 報錯
症狀:
A param property was accessed directly with `params.slug`. `params` should be awaited...
原因:Next.js 15+ 把 params 改成 Promise
解決方案:
// ❌ 舊版
export default function Page({ params }: { params: { slug: string } }) {
return <h1>{params.slug}</h1>;
}
// ✅ Next.js 15+
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
}
問題 4:環境變數在 Client 抓不到
症狀:process.env.MY_VAR 在瀏覽器 console 是 undefined
原因:只有 NEXT_PUBLIC_ 前綴的環境變數才會暴露到 Client
解決方案:
# .env.local
DATABASE_URL=... # 只在伺服器可用
NEXT_PUBLIC_SITE_URL=... # 兩邊都可用
→ 完整環境變數規則見 deployment-and-runtime.md
問題 5:跨 Layout 共用狀態
問題:如何在多個頁面之間共用 client state?
解決方案:在 app/layout.tsx 包一個 Client Provider:
// app/providers.tsx
"use client";
import { createContext } from "react";
export const MyContext = createContext(null);
export default function Providers({ children }: { children: React.ReactNode }) {
return <MyContext.Provider value={...}>{children}</MyContext.Provider>;
}
// app/layout.tsx
import Providers from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html><body><Providers>{children}</Providers></body></html>
);
}
總結
核心要點
Next.js = React + 檔案路由 + Server Components + 全端整合 + 內建優化
關鍵優勢:
- ✅ 檔案系統路由零設定
- ✅ Server Components 預設,bundle 更小
- ✅ 多種渲染模式自由切換
- ✅ 從前端到 API 一站搞定
快速決策指南
| 情境 | 建議 | 理由 |
|---|---|---|
| 純 SPA、不需 SEO | ⚠️ | 純 React + Vite 可能更輕 |
| 內容型網站、部落格 | ✅ | SSG/ISR 是強項 |
| 後台儀表板(需登入) | ✅ | Server Components + Route Handlers 完整覆蓋 |
| 即時聊天、遊戲 | ⚠️ | 即時通訊需要額外架設 WebSocket |
| 全端整合需求 | ✅ | Server Actions + API 一站完成 |
核心檔名速查
| 檔名 | 角色 |
|---|---|
page.tsx |
路由 UI |
layout.tsx |
共用 Layout |
loading.tsx |
載入 fallback |
error.tsx |
錯誤邊界 |
not-found.tsx |
404 |
route.ts |
API 端點 |
middleware.ts |
中介層(放 src/ 或 app/ 同層) |
相關閱讀
本系列 7 篇地圖:
- basics.md ← 你在這裡
- rendering-and-cache.md — 渲染模式與四層快取
- server-actions.md — Server Actions 與表單
- metadata-and-seo.md — Metadata API 與 SEO
- routing-advanced.md — 進階路由
- assets-and-performance.md — 資產與效能
- deployment-and-runtime.md — 部署與 Runtime
參考資源
- 官方網站:https://nextjs.org
- 官方文件:https://nextjs.org/docs
- GitHub:https://github.com/vercel/next.js
- Vercel 部署平台:https://vercel.com
- App Router 學習路徑:https://nextjs.org/learn
建立日期:2026-05-16 最後更新:2026-05-16