基于实战项目的 Next.js 16 学习指南

本文基于真实的「名犬鉴赏局」AI 品相鉴定项目编写,所有代码示例都来自实际项目代码。

作为一个长期写业务代码的开发者,我最近开始学习 Next.js 16,但发现市面上的教程要么太基础(只讲 Hello World),要么太深入(直接讲源码原理)。于是我决定基于自己正在开发的项目,写一篇实战导向的学习指南。

本文不追求面面俱到,而是围绕项目实际用到的功能展开讲解。读完这篇,你能够:

  • 理解 Next.js 的核心概念和架构
  • 掌握 App Router 的路由设计
  • 学会创建 API 接口
  • 能够独立开发一个完整的页面

目录

  1. Next.js 基础概念
  2. App Router 路由系统
  3. 客户端与服务端组件
  4. API 路由 (Route Handlers)
  5. 样式方案 (Tailwind CSS)
  6. 项目核心模式解析
  7. 实战技巧与最佳实践
  8. 快速查阅 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
渲染性能需下载 JSHTML 已渲染好

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"),原因:

  1. 页面需要用户交互(表单、按钮、文件上传)
  2. 需要管理状态(useState、useEffect)
  3. 需要路由跳转(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 内置 HookssessionStorage

// 状态管理(组件内)
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 是 PromiseNext.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,如需转载,请注明出处。