# Next.js 關鍵特性解析,專案與環境搭建及核心功能說明
## 第一章:Next.js 概述
Next.js 由 Vercel 開發,是一個基於 React 的框架,提供:
1. **伺服器端渲染(SSR)**:在伺服器先產出 HTML,改善 SEO 與首屏速度。
2. **靜態生成(SSG)**:建置時產出靜態頁面,速度快、安全性高。
3. **自動路由**:`pages/` 下的檔案即是路由。
4. **API 路由**:`pages/api/` 直接寫後端端點,快速做全端應用。
5. **增量靜態再生(ISR)**:靜態頁可在背景重生,兼顧即時性與效能。
6. **內建樣式支援**:CSS、Sass、CSS Modules 皆可用。
> 總結:Next.js 比起純 React,多了 **SSR/SSG/路由/API** 等能力,能更快做出高效能、SEO 友善的網站。
<br/>
## 第二章:建立 Next.js 專案(Pages Router)
### 建立專案
```bash
npx create-next-app@latest
```
依互動提示(示例):
```
✔ What is your project named? … next-project
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … No
✔ Would you like to use `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … No # 本教材走 Pages Router
✔ Would you like to customize the default import alias (@/*)? … Yes
✔ What import alias would you like configured? … @/*
```
**參考範例專案**:[https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect](https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect)
### 專案結構重點
* `pages/`
* `index.tsx`:首頁,對應 `/`。
* `_app.tsx`:自訂 App 根組件(全域樣式、狀態、版型包裹)。
* `_document.tsx`:自訂 HTML 結構(`<html>`, `<body>`, `<Head>` 等)。
* `public/`:靜態資源(favicon、圖片…)。
* `styles/`:全域或 CSS Modules 樣式。
* `components/`:可重用的 React 元件。
* **`node_modules`資料夾**:
- 這個資料夾包含了專案的所有依賴包,由 npm 或 yarn 安裝。
* **`package.json`**:
- 這個檔案定義了專案的基本資訊和依賴包。
* **`next.config.js`**:
- 這個檔案用來設定 Next.js 應用程式的設定。
* **`tsconfig.json`**:
- 這個檔案用來設定 TypeScript 的設定。
**_app.tsx 範例**
```tsx
// pages/_app.tsx
import type { AppProps } from 'next/app'
import '@/styles/globals.css'
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
```
**_document.tsx 範例**
```tsx
// pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
```
<br/>
## 第三章:頁面與路由(Pages Router)
### 基本路由
* `pages/index.tsx` → `/`
* `pages/about.tsx` → `/about`
```tsx
// pages/index.tsx
export default function Home() {
return <h1>Welcome to the Home Page</h1>
}
```
```tsx
// pages/about.tsx
export default function About() {
return <h1>About Us</h1>
}
```
### 動態路由
`pages/posts/[id].tsx` → `/posts/:id`
```tsx
// pages/posts/[id].tsx
import { useRouter } from 'next/router'
export default function Post() {
const { query } = useRouter()
const { id } = query
return <h1>Post ID: {id}</h1>
}
```
### 連結導覽
```tsx
// pages/index.tsx
import Link from 'next/link'
export default function Home() {
const postId = 123
return (
<div>
<h1>Welcome</h1>
<Link href="/about">Go to About</Link>
<br />
<Link href={`/posts/${postId}`}>Go to Post {postId}</Link>
</div>
)
}
```
### 練習:列表頁 + 動態詳情(假資料)
**fixtures/fakeData.ts**
```ts
export const postData = [
{ id: 1, title: 'Hello World', content: 'This is my first post' },
{ id: 2, title: 'Hello Again', content: 'This is my second post' },
{ id: 3, title: 'Hello Again', content: 'This is my third post' },
{ id: 4, title: 'Hello Again', content: 'This is my fourth post' },
]
```
**pages/posts/index.tsx**
```tsx
import { postData } from '@/fixtures/fakeData'
import Link from 'next/link'
export default function PostsPage() {
return (
<div>
{postData.map((post) => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
<Link href={`/posts/${post.id}`}>Read more</Link>
</div>
))}
</div>
)
}
```
**pages/posts/[id].tsx**
```tsx
import { useRouter } from 'next/router'
import { postData } from '@/fixtures/fakeData'
import Link from 'next/link'
export default function Post() {
const { id } = useRouter().query
const current = postData.find((p) => String(p.id) === String(id))
return (
<div>
<h1>Post ID: {id}</h1>
<h2>{current?.title}</h2>
<p>{current?.content}</p>
<Link href="/posts">返回列表</Link>
</div>
)
}
```
參考專案:[https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect-router](https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect-router)
<br/>
## 第四章:SSR 與 SSG(Pages Router)
### SSR:`getServerSideProps`
每次請求都在伺服器抓資料並渲染。
```tsx
// pages/time.tsx
export default function TimePage({ currentTime }: { currentTime: string }) {
return (
<div>
<h1>Current Server Time</h1>
<p>{currentTime}</p>
</div>
)
}
export async function getServerSideProps() {
return { props: { currentTime: new Date().toLocaleString() } }
}
```
### SSG:`getStaticProps` + `getStaticPaths`
建置時產靜態頁。
```ts
// fixtures/fakeData.ts
export const postData = [
{ id: 1, title: 'Hello World', content: 'This is my first post' },
{ id: 2, title: 'Hello Again', content: 'This is my second post' },
{ id: 3, title: 'Hello Again', content: 'This is my third post' },
{ id: 4, title: 'Hello Again', content: 'This is my fourth post' },
]
```
```tsx
// pages/posts/[id].tsx
import { postData } from '@/fixtures/fakeData'
import type { GetStaticPropsContext } from 'next'
import Link from 'next/link'
type Post = { id: number; title: string; content: string }
export default function BlogPost({ post }: { post: Post | null }) {
if (!post) return <div>Post not found</div>
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
<Link href="/posts">Back to posts</Link>
</div>
)
}
export async function getStaticProps({ params }: GetStaticPropsContext) {
const post = postData.find((p) => String(p.id) === String(params?.id)) || null
return { props: { post } }
}
export async function getStaticPaths() {
const paths = postData.map((p) => ({ params: { id: String(p.id) } }))
return { paths, fallback: false }
}
```
參考專案:[https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect-SSR-SSG](https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect-SSR-SSG)
<br/>
## 第五章:API 路由(Pages Router)
### 基本 API 範例
```ts
// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.status(200).json({ message: 'Hello, Next.js API!' })
}
```
前端呼叫:
```tsx
import { useEffect, useState } from 'react'
export default function Home() {
const [data, setData] = useState<null | { message: string }>(null)
useEffect(() => {
(async () => {
const r = await fetch('/api/hello')
const json = await r.json()
setTimeout(() => setData(json), 1000)
})()
}, [])
if (!data) return <h1>Loading...</h1>
return <h1>{data.message}</h1>
}
```
### 表單 POST / 驗證
```ts
// pages/api/submitForm.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
const { name, email } = req.body
if (!name || !email) return res.status(400).json({ error: 'Name and email are required' })
return res.status(200).json({ message: `Form submitted successfully, ${name}!` })
}
res.setHeader('Allow', ['POST'])
res.status(405).end(`Method ${req.method} Not Allowed`)
}
```
```tsx
// pages/form.tsx
export default function FormPage() {
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const fd = new FormData(e.currentTarget)
const name = String(fd.get('name') || '')
const email = String(fd.get('email') || '')
const r = await fetch('/api/submitForm', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, email }) })
console.log(await r.json())
}
return (
<form onSubmit={onSubmit}>
<label>Name: <input type="text" name="name" /></label>
<br />
<label>Email: <input type="email" name="email" /></label>
<button type="submit">Submit</button>
</form>
)
}
```
參考專案:[https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect-api](https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect-api)
<br/>
## 第六章:綜合實作 A —— Todo List(API 路由 + TS)
### 前端頁面
```tsx
// pages/todos.tsx
import { useEffect, useState } from 'react'
type Todo = { id: number; title: string; completed: boolean }
export default function TodoPage() {
const [todos, setTodos] = useState<Todo[]>([])
const [newTodo, setNewTodo] = useState('')
useEffect(() => {
(async () => {
const r = await fetch('/api/todos')
setTodos(await r.json())
})()
}, [])
const addTodo = async (): Promise<void> => {
const r = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTodo }),
})
const newItem: Todo = await r.json()
setTodos((t) => [...t, newItem])
setNewTodo('')
}
const delTodo = async (id: number): Promise<void> => {
await fetch(`/api/todos?id=${id}`, { method: 'DELETE' })
setTodos((t) => t.filter((x) => x.id !== id))
}
const toggle = async (id: number) => {
const cur = todos.find((t) => t.id === id)
if (!cur) return
const r = await fetch(`/api/todos?id=${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: cur.title, completed: !cur.completed }) })
const updated: Todo = await r.json()
setTodos((t) => t.map((x) => (x.id === id ? updated : x)))
}
return (
<div>
<h1>Todo List</h1>
<input value={newTodo} onChange={(e) => setNewTodo(e.target.value)} placeholder="Add a new todo" />
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }} onClick={() => toggle(todo.id)}>
{todo.title}
</span>
<button onClick={() => delTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
)
}
```
### API 路由
```ts
// pages/api/todos.ts
// pages/api/todos.ts
import type { NextApiRequest, NextApiResponse } from 'next'
type Todo = { id: number; title: string; completed: boolean }
let todos: Todo[] = [
{ id: 1, title: 'Learn Next.js', completed: false },
{ id: 2, title: 'Build a Next.js app', completed: false },
]
export default function handler(req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case 'GET':
return res.status(200).json(todos)
case 'POST': {
const { title } = req.body
const newTodo: Todo = { id: todos.length + 1, title, completed: false }
todos.push(newTodo)
return res.status(201).json(newTodo)
}
case 'PUT': {
const id = Number(req.query.id)
const { title, completed } = req.body
const i = todos.findIndex((t) => t.id === id)
if (i === -1) return res.status(404).json({ message: 'Todo not found' })
todos[i] = { ...todos[i], title, completed }
return res.status(200).json(todos[i])
}
case 'DELETE': {
const id = Number(req.query.id)
todos = todos.filter((t) => t.id !== id)
return res.status(200).json({ message: 'Todo deleted' })
}
default:
res.setHeader('Allow', ['GET', 'POST', 'PUT', 'DELETE'])
return res.status(405).end(`Method ${req.method} Not Allowed`)
}
}
```
> 教學用記憶體陣列僅供示範;正式環境請改用資料庫或外部 API。
<br/>
## 第七章:樣式與版型(CSS Modules + Layout)
**components/Navbar/index.tsx**
```tsx
import styles from './Navbar.module.css'
export default function Navbar() {
return <div className={styles.navbar}>Navbar</div>
}
```
**components/Navbar/Navbar.module.css**
```css
.navbar { background-color: blue; }
```
**pages/_app.tsx(版型包裹)**
```tsx
import type { AppProps } from 'next/app'
import Layout from '@/components/Layout'
export default function App({ Component, pageProps }: AppProps) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}
```
**components/Layout/index.tsx**
```tsx
export default function Layout({ children }: { children?: React.ReactNode }) {
return (
<>
<nav>I am nav</nav>
{children}
</>
)
}
```
**pages/about.tsx**
```tsx
export default function AboutPage() {
return <div>I AM AboutPage page</div>
}
```
VS Code 自動整理匯入(選用):
```json
{
"editor.codeActionsOnSave": { "source.organizeImports": "explicit" }
}
```
**範例**:[https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-project-demo](https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-project-demo)
<br/>
、