第 20 章:Next.js 入门
学习目标
- 理解 Next.js 解决了什么问题
- 创建 Next.js 14 项目(App Router)
- 区分 Server Components vs Client Components
- 避开 Next.js 的 4 个常见坑
一、Next.js 是什么?
Next.js 是基于 React 的全栈框架,提供:
- 文件系统路由
- SSR / SSG / ISR
- API Routes
- 优化(图片、字体、Script)
- 部署到 Vercel 一键上线
二、为什么用 Next.js 14?
| 维度 | Vite + React | Next.js |
|---|---|---|
| 路由 | 手动配 | 文件系统 |
| SSR | 额外配置 | 内置 |
| SEO | 弱 | 强 |
| 部署 | 任意静态 | Vercel 一键 |
| Server Components | ❌ | ✅ |
三、创建项目
bash
npx create-next-app@latest my-app
# 选项:
# ✅ TypeScript
# ✅ ESLint
# ✅ Tailwind CSS
# ✅ src/ 目录
# ✅ App Router(推荐)
# ❌ import alias 默认
cd my-app
npm run dev访问 http://localhost:3000。
⚠️ 坑 1:不用
--typescript、--tailwind等参数会被询问;用--yes全默认。
四、目录结构(App Router)
my-app/
├── src/
│ ├── app/
│ │ ├── layout.tsx # �️ 根布局
│ │ ├── page.tsx # / 路由
│ │ ├── globals.css
│ │ └── about/
│ │ └── page.tsx # /about 路由
│ └── components/
├── public/ # 静态资源
├── next.config.js
├── tailwind.config.ts
└── tsconfig.json五、第一个页面
src/app/page.tsx:
tsx
export default function Home() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">Hello, Next.js!</h1>
<p>当前时间:{new Date().toLocaleString()}</p>
</main>
);
}⚠️ 坑 2:App Router 默认Server Component(
page.tsx无"use client"),不能直接用useState等 hooks。Server Component 不会跨域——因为 fetch 是服务端 Node.js 发请求,不是浏览器,没有 CORS:
tsx// ✅ Server Component:服务端 fetch,无 CORS 限制 async function Page() { const r = await fetch('http://localhost:8080/secret') // ✅ 内网也行 return <div>{r.data}</div> }"不能用 await"是误读——两种组件都能
await fetch,差别在写法位置:tsx// ① Server Component:顶层直接 await(函数本身要 async) async function Page() { const data = await fetch('/api/users') // ✅ OK return <div>{data.name}</div> } // ② Client Component:await 必须包在 useEffect 或事件里 'use client' function Page() { const [data, setData] = useState(null) useEffect(() => { async function load() { const res = await fetch('/api/users') // ✅ OK(也能 await) setData(await res.json()) } load() }, []) return <div>{data?.name}</div> }"后端能力" ≠ 写后端接口——Server Component 的"后端能力"就三件事:
能力 例子 ① 调内部 API(无 CORS) fetch('http://内网/secret')② 读文件 / 读数据库 await db.query()/import data from './x.json'③ 用 API Key 等机密 process.env.SECRET_KEY(不进浏览器)写接口在
app/api/xxx/route.ts,不在 page.tsx。💡 Server Component 不是写后端,是"前端代码能直接用后端资源"——调内部 API / 读文件 / 用 Key 都不暴露给浏览器。Vite + React 想要这些能力,得自己搭后端 + 处理 CORS + 保护 API Key。
六、Server vs Client Components
Server Component(默认)
- 在服务器渲染,不发到浏览器
- 可以直接
await fetch() - 不能用 hooks、事件、浏览器 API
- bundle 体积更小,SEO 更好
tsx
// page.tsx(默认)
async function getData() {
const r = await fetch('https://api.example.com/data');
return r.json();
}
export default async function Page() {
const data = await getData(); // ⚠️ 顶层 await
return <div>{data.title}</div>;
}Client Component(交互用)
- 加
"use client"指令 - 可以用 hooks、事件
- 在浏览器运行
tsx
'use client';
import { useState } from 'react';
export default function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}⚠️ 坑 3:
"use client"是边界标识,该组件及其 children 都成 Client Component,树最底层加最划算。
七、嵌套 Server + Client
tsx
// app/page.tsx(Server)
import Counter from './Counter';
export default async function Page() {
const data = await fetch('...').then(r => r.json());
return (
<main>
<h1>{data.title}</h1>
<Counter /> {/* 客户端组件,嵌入服务端树 */}
</main>
);
}八、布局(layout.tsx)
tsx
// app/layout.tsx(必须有)
import './globals.css';
export const metadata = {
title: 'My App',
description: 'Next.js 14 教程',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>{children}</body>
</html>
);
}九、字体与图片优化
tsx
import { Inter } from 'next/font/google'; // ① Google 字体加载器(避免 FOIT 闪烁)
import Image from 'next/image'; // ② Next.js 优化版 <img>(懒加载 + WebP)
const inter = Inter({ subsets: ['latin'] }); // ③ 加载 Inter 字体,只下 latin 子集(体积小)
export default function Page() {
return (
<main className={inter.className}> // ④ 给 main 加 className,继承字体样式
<Image
src="/hero.png" // 图片路径(public 目录下)
alt="hero" // 必填!无障碍 + SEO
width={800} // 必须传宽高(布局稳定,不抖动)
height={400}
priority // 关键图:不懒加载,首屏优先加载
/>
</main>
);
}next/font 的好处:
- 自动自托管字体(build 时下载,运行时零请求 Google)
- 消除 FOIT(字体加载时的闪烁)
- 按
subsets切片,只下需要的字符(体积小)
next/image 的好处(vs 普通 <img>):
- 自动懒加载(滚到才加载)
- 自动转 WebP(体积小)
- 响应式 srcset(不同屏幕不同尺寸)
- 占满宽度的图:
fill+ 父元素position: relative
⚠️ 坑 4:不用
next/image直接用<img>不会自动优化(懒加载、WebP)。
十、metadata SEO
tsx
// 静态 metadata(写死)
export const metadata = {
title: '关于我', // 页面标题(<title>)
description: '我的个人介绍', // 描述(<meta name="description">)
};
// 动态 metadata(根据参数生成,如详情页)
export async function generateMetadata(
{ params }: { params: { id: string } } // 解构路由参数(动态路由 [id])
) {
return {
title: `用户 ${params.id}`, // /user/123 → 标题"用户 123"
description: `这是用户 ${params.id} 的主页`,
};
}两种用法区别:
| 写法 | 适用 |
|---|---|
export const metadata = {...} | 静态页面(关于我、首页) |
export async function generateMetadata({ params }) | 动态页面(详情页 /user/[id]) |
Next.js 自动做的事:
- 把
title塞到<title>标签 - 把
description塞到<meta name="description"> - 还能配
openGraph(分享卡片)、twitter、icons等
tsx
export const metadata = {
title: '关于我',
description: '我的个人介绍',
openGraph: {
title: '关于我',
description: '分享时的描述',
images: ['/og.png'], // 分享时显示的图
},
}对比传统 HTML:
html
<!-- 传统写法:每个页面写一遍 -->
<head>
<title>关于我</title>
<meta name="description" content="我的个人介绍">
</head>
<!-- Next.js:在 page.tsx 里 export 一个对象就完事 -->十一、本章小结
| 要点 | 关键 |
|---|---|
| 创建 | create-next-app@latest |
| App Router | app/ 目录即路由 |
| 默认类型 | Server Component |
| Client | 文件顶部加 "use client" |
| 边界 | 客户端边界放树最底层 |
| 优势 | SSR/SEO/优化/Vercel 部署 |
| 优化 | next/image、next/font |
动手练习
- 初始化:创建
my-first-next,跑通默认页 - 双页面:
/显示标题,/about显示介绍,加导航<Link> - 客户端组件:在
/about加一个 Counter 组件(交互) - 找坑:以下代码运行报什么错?tsx
export default function Page() { const [n, setN] = useState(0); return <div>{n}</div>; }
下一章:第 21 章:Next.js 路由 →