基于实战项目的 Next.js 16 学习指南
本文基于真实的「名犬鉴赏局」AI 品相鉴定项目编写,所有代码示例都来自实际项目代码。
作为一个长期写业务代码的开发者,我最近开始学习 Next.js 16,但发现市面上的教程要么太基础(只讲 Hello World),要么太深入(直接讲源码原理)。于是我决定基于自己正在开发的项目,写一篇实战导向的学习指南。
本文不追求面面俱到,而是围绕项目实际用到的功能展开讲解。读完这篇,你能够:
- 理解 Next.js 的核心概念和架构
- 掌握 App Router 的路由设计
- 学会创建 API 接口
- 能够独立开发一个完整的页面
目录
- Next.js 基础概念
- App Router 路由系统
- 客户端与服务端组件
- API 路由 (Route Handlers)
- 样式方案 (Tailwind CSS)
- 项目核心模式解析
- 实战技巧与最佳实践
- 快速查阅 Cheat Sheet
1. Next.js 基础概念
1.1 什么是 Next.js?
Next.js 是一个 React 框架,用于构建全栈 Web 应用。
| 特性 | 说明 |
|---|---|
| 渲染方式 | 支持 SSR(服务端渲染)、SSG(静态生成)、CSR(客户端渲染) |
| 路由系统 | 基于文件系统的 App Router |
| API 能力 | 内置 API 路由,无需独立后端 |
| 优化 | 自动代码分割、图片优化、字体优化 |
1.2 本项目使用的技术栈
Next.js 16 (App Router) + TypeScript + Tailwind CSS v4
项目依赖(package.json):
{
"next": "^16.2.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"recharts": "^2.15.3",
"html2canvas": "^1.4.1",
"@supabase/supabase-js": "^2.104.0"
}
1.3 常用命令
npm run dev # 启动开发服务器(Turbopack 加速)
npm run build # 构建生产版本
npm run start # 运行生产版本
开发服务器默认访问:http://localhost:3000
2. App Router 路由系统
2.1 路由基础
Next.js 16 使用 App Router,路由由文件夹结构决定:
app/
├── page.tsx → 路由: /
├── studio/
│ └── page.tsx → 路由: /studio
├── processing/
│ └── page.tsx → 路由: /processing
├── report/
│ └── [id]/
│ └── page.tsx → 路由: /report/:id (动态路由)
└── share/
└── [id]/
└── page.tsx → 路由: /share/:id (动态路由)
2.2 动态路由参数
在 app/report/[id]/page.tsx 中获取 URL 参数:
"use client"; // 客户端组件才能使用 hook
import { useParams } from "next/navigation";
export default function ReportPage() {
const params = useParams();
const id = params.id as string; // 获取 :id 参数
// 使用 id 发送 API 请求获取报告数据
useEffect(() => {
fetch(`/api/report/${id}`)
.then(r => r.json())
.then(data => console.log(data));
}, [id]);
return <div>报告 ID: {id}</div>;
}
2.3 页面跳转
import { useRouter } from "next/navigation";
export default function MyComponent() {
const router = useRouter();
// 编程式导航
const handleClick = () => {
router.push("/studio"); // 跳转页面
router.replace("/studio"); // 跳转(替换历史记录)
router.back(); // 返回上一页
};
}
3. 客户端与服务端组件
3.1 概念对比
| 特性 | 客户端组件 (Client) | 服务端组件 (Server) |
|---|---|---|
| 声明方式 | "use client" | 默认就是 |
| 执行位置 | 浏览器 | 服务器 |
| 交互能力 | 可用 useState、useEffect | 不可用 |
| 数据获取 | useEffect + fetch | 直接 async/await |
| 渲染性能 | 需下载 JS | HTML 已渲染好 |
3.2 如何选择?
// 需要交互?用 "use client"
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// 纯展示?用服务端组件(无需声明)
export default function Header() {
return <h1>我的网站</h1>; // 默认服务端渲染
}
3.3 项目中的实践
本项目所有页面都是客户端组件("use client"),原因:
- 页面需要用户交互(表单、按钮、文件上传)
- 需要管理状态(useState、useEffect)
- 需要路由跳转(useRouter)
// app/page.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function GatePage() {
const router = useRouter();
const [code, setCode] = useState("");
// ... 业务逻辑
}
3.4 混合使用技巧
服务端组件嵌套客户端组件:
// app/stats/page.tsx (服务端组件)
import UserList from "./UserList"; // 客户端组件
export default function StatsPage() {
const data = fetchData(); // 服务端获取数据
return (
<div>
<h1>统计数据</h1>
<UserList initialData={data} /> // 传入客户端
</div>
);
}
4. API 路由 (Route Handlers)
4.1 创建 API 路由
在 app/api/ 目录下创建文件:
app/api/
├── activate/
│ └── route.ts → POST /api/activate
├── upload/
│ └── route.ts → POST /api/upload
├── analyze/
│ └── route.ts → POST /api/analyze
└── report/
└── [id]/
└── route.ts → GET /api/report/:id
4.2 Route Handler 基本结构
// app/api/activate/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
// 1. 解析请求体
const { code } = await req.json();
// 2. 业务逻辑
if (!code) {
return NextResponse.json(
{ valid: false, error: "激活码不能为空" },
{ status: 400 }
);
}
// 3. 返回响应
return NextResponse.json(
{ valid: true, session_token: "xxx" },
{ status: 200 }
);
}
4.3 支持的 HTTP 方法
// app/api/example/route.ts
export async function GET(req: NextRequest) {
// 处理 GET 请求
}
export async function POST(req: NextRequest) {
// 处理 POST 请求
}
export async function PUT(req: NextRequest) {
// 处理 PUT 请求
}
export async function DELETE(req: NextRequest) {
// 处理 DELETE 请求
}
4.4 获取 URL 参数
// app/api/report/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params; // 获取动态参数
return NextResponse.json({ report_id: id });
}
4.5 前端调用 API
// 在页面组件中调用
const handleSubmit = async () => {
const res = await fetch("/api/activate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: "ABC123DEF456" }),
});
const data = await res.json();
if (data.valid) {
router.push("/studio");
} else {
setError(data.error);
}
};
5. 样式方案 (Tailwind CSS)
5.1 本项目配置
项目使用 Tailwind CSS v4,配置非常简洁:
// tailwind.config.ts
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [],
};
export default config;
实际样式都在 app/globals.css 中定义。
5.2 Tailwind 基础语法
// 基础类名
<div className="flex items-center justify-center">
<div className="w-full max-w-sm">
<div className="text-4xl font-bold">
<div className="bg-black text-white">
<div className="p-4 m-2 rounded-lg">
<div className="mt-8 mb-4">
// 响应式
<div className="text-sm md:text-base lg:text-lg">
// 条件样式(结合 clsx 或模板字符串)
<button className={`btn-gold w-full ${loading ? 'opacity-50' : ''}`}>
5.3 项目自定义样式
在 app/globals.css 中使用 CSS 变量和自定义类:
/* CSS 变量定义 */
:root {
--color-bg: #0a0a0a;
--color-surface: #141414;
--color-gold: #c9a84c;
--color-gold-light: #f0d080;
}
/* 自定义组件类 */
.card-gold {
background: var(--color-surface);
border: 1px solid var(--color-border);
}
.btn-gold {
background: linear-gradient(135deg, var(--color-gold-dark), var(--color-gold));
color: #0a0a0a;
}
/* 动画 */
@keyframes fade-in-up {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in-up {
animation: fade-in-up 0.6s ease forwards;
}
6. 项目核心模式解析
6.1 页面数据流
用户输入 → useState 状态管理 → API 调用 → 路由跳转 → 展示结果
以激活码验证为例(app/page.tsx):
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function GatePage() {
const router = useRouter();
const [code, setCode] = useState(""); // 状态管理
const [error, setError] = useState(""); // 错误状态
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
// 调用 API
const res = await fetch("/api/activate", {
method: "POST",
body: JSON.stringify({ code }),
});
const data = await res.json();
if (data.valid) {
// 保存登录态
sessionStorage.setItem("session_token", data.session_token);
// 跳转到下一步
router.push("/studio");
} else {
setError(data.error);
}
setLoading(false);
};
return (
<form onSubmit={handleSubmit}>
<input value={code} onChange={e => setCode(e.target.value)} />
<button type="submit" disabled={loading}>
{loading ? "验证中..." : "开始鉴定"}
</button>
</form>
);
}
6.2 状态管理方案
本项目使用 React 内置 Hooks 和 sessionStorage:
// 状态管理(组件内)
const [state, setState] = useState(initialValue);
// 持久化存储(跨页面共享)
sessionStorage.setItem("key", "value");
sessionStorage.getItem("key");
// 复杂状态
const [preview, setPreview] = useState<string | null>(null);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
6.3 图片处理
使用 Next.js 的 Image 组件:
import Image from "next/image";
// 基础用法
<Image
src="/my-image.jpg" // 图片路径
alt="描述文字"
width={200} // 宽度
height={200} // 高度
/>
// 响应式填充容器(项目常用)
<Image
src={imageUrl}
alt="爱犬照片"
fill // 自动填满父容器
className="object-cover" // 保持比例裁剪
unoptimized // 禁用 Next.js 优化(外部图片需要)
/>
6.4 数据获取与轮询
"use client";
import { useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
export default function ProcessingPage() {
const router = useRouter();
const pollRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
// 触发分析
fetch("/api/analyze", { ... })
.then(r => r.json())
.then(data => {
if (data.report_id) {
startPolling(data.report_id); // 开始轮询
}
});
// 轮询函数
function startPolling(reportId: string) {
pollRef.current = setInterval(async () => {
const res = await fetch(`/api/report/${reportId}`);
const data = await res.json();
if (data.status === "done") {
clearInterval(pollRef.current!);
router.push(`/report/${reportId}`);
}
}, 2000); // 每 2 秒轮询一次
}
// 清理
return () => {
if (pollRef.current) clearInterval(pollRef.current);
};
}, []);
}
7. 实战技巧与最佳实践
7.1 常见错误与解决
| 错误 | 原因 | 解决 |
|---|---|---|
useRouter() 报错 | 在服务端组件使用 | 添加 "use client" |
Image 加载失败 | 外部图片未配置域名 | 添加 unoptimized |
params.id 是 Promise | Next.js 16 新变化 | 使用 await params |
| CSS 变量不生效 | 未在 globals.css 定义 | 确保 CSS 变量在 :root 中 |
7.2 Next.js 16 新变化
动态参数获取方式改变:
// ❌ 旧写法(Next.js 14)
export default function Page({ params }: { params: { id: string } }) {
const id = params.id;
}
// ✅ 新写法(Next.js 16)
export default async function Page(
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
}
7.3 实用代码片段
表单处理:
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// 限制输入:只允许字母数字
const value = e.target.value.replace(/[^a-zA-Z0-9]/g, "");
// 限制长度
setCode(value.slice(0, 12));
};
图片预览:
const handleFile = (file: File) => {
if (!file.type.startsWith("image/")) return;
const url = URL.createObjectURL(file); // 本地预览
setPreview(url);
setSelectedFile(file);
};
文件上传 FormData:
const form = new FormData();
form.append("image", selectedFile);
const res = await fetch("/api/upload", { method: "POST", body: form });
动态导入(减少首屏 JS):
const html2canvas = (await import("html2canvas")).default;
const canvas = await html2canvas(domRef);
7.4 项目结构速查
项目根目录/
├── app/ # 所有页面和 API
│ ├── page.tsx # 激活码页 (/)
│ ├── globals.css # 全局样式
│ ├── layout.tsx # 根布局
│ ├── studio/page.tsx # 上传页 (/studio)
│ ├── processing/page.tsx # 处理页 (/processing)
│ ├── report/[id]/page.tsx # 报告页 (/report/:id)
│ ├── share/[id]/page.tsx # 海报页 (/share/:id)
│ └── api/ # API 路由
│ ├── activate/route.ts
│ ├── upload/route.ts
│ ├── analyze/route.ts
│ └── report/[id]/route.ts
├── lib/ # 工具库
│ └── supabase.ts # Supabase 客户端
├── aiInfo/ # 项目文档
│ ├── prd.md # 产品需求
│ ├── tech.md # 技术架构
│ └── todo.md # 任务列表
└── package.json # 项目依赖
8. 快速查阅 Cheat Sheet
页面组件模板
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
// import Image from "next/image";
export default function PageName() {
const router = useRouter();
const [state, setState] = useState(initialValue);
useEffect(() => {
// 组件挂载时的逻辑
}, []);
return (
<main className="min-h-screen flex flex-col items-center justify-center px-6">
{/* 页面内容 */}
</main>
);
}
API 路由模板
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const body = await req.json();
// 业务逻辑
return NextResponse.json({ success: true }, { status: 200 });
}
常用 Tailwind 类
布局: flex, grid, block, hidden
方向: flex-col, flex-row
对齐: items-center, justify-center, justify-between
间距: p-4, m-2, mt-4, mb-2, gap-4
尺寸: w-full, h-full, max-w-sm, aspect-square
圆角: rounded, rounded-lg, rounded-full
文字: text-sm, text-lg, text-2xl, font-bold
颜色: bg-black, text-white, border-gray
动画: animate-pulse, transition-all
路由导航速查
import { useRouter } from "next/navigation";
const router = useRouter();
router.push("/path"); // 跳转
router.replace("/path"); // 替换当前页
router.back(); // 返回
router.refresh(); // 刷新数据
总结
这篇文章基于我正在开发的「名犬鉴赏局」项目,系统整理了 Next.js 16 的核心知识点。如果你也是从传统 React 开发转向 Next.js,希望这篇文章能帮你快速上手。
学习新技术最好的方式就是动手实践。建议 clone 这个项目,自己改一改、跑一跑,遇到问题再深入研究官方文档。
如果你喜欢这篇文章,欢迎分享给有需要的朋友。后续我会继续更新项目的后端实现、数据库设计等内容,敬请期待。
参考资源
本文首发于 Guoquanmai’s Blog,如需转载,请注明出处。