# 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/> 、