基于本项目(海克斯大乱斗攻略站)讲解,结合实际代码理解概念。
一、服务端组件 vs 客户端组件
这是 Next.js App Router 最核心的概念,没搞清楚这个,后面全是坑。
服务端组件(默认)
app/ 目录下的组件默认都是服务端组件,特点:
- 可以直接
async/await,在服务器上跑 - 可以直接访问数据库、调接口,不暴露给浏览器
- 不能用
useState、useEffect、onClick等浏览器 API
tsx
// app/page.tsx ------ 服务端组件,直接 async
export default async function Home() {
const data = await fetch("https://api.example.com/data");
const json = await data.json();
return <div>{json.title}</div>;
}
客户端组件
顶部加 "use client" 声明,特点:
- 可以用 React hooks(useState、useEffect 等)
- 可以绑定事件(onClick、onChange)
- 不能直接 async/await 请求(需要用 useEffect 或 SWR)
tsx
// components/SearchBox.tsx ------ 客户端组件
"use client";
import { useState } from "react";
export default function SearchBox() {
const [value, setValue] = useState("");
return (
<input value={value} onChange={(e) => setValue(e.target.value)} />
);
}
本项目的例子
bash
服务端组件:app/page.tsx、app/builds/page.tsx、app/champions/[id]/page.tsx
客户端组件:components/ChampionGrid.tsx、app/augments/page.tsx
最佳实践:尽量让父组件(页面)是服务端组件负责取数据,把数据作为 props 传给客户端组件负责交互。
二、fetch 还是 axios?
直接结论:Next.js 服务端推荐用原生 fetch,不推荐 axios
原因是 Next.js 对原生 fetch 做了扩展增强,加了缓存和重新验证功能,这是 axios 没有的:
ts
// Next.js 扩展的 fetch,支持缓存控制
fetch(url, {
next: {
revalidate: 86400, // ISR:缓存 24 小时后重新请求
},
});
fetch(url, {
cache: "no-store", // 每次请求都不缓存(相当于 SSR)
});
fetch(url, {
cache: "force-cache", // 永久缓存(相当于 SSG)
});
本项目里就用了 revalidate: 86400,英雄数据每天只请求一次 Riot 服务器,其余时间直接用缓存,性能很好。
axios 能用吗?
能用,但有限制:
- 客户端组件里完全可以用 axios(和普通 React 一样)
- 服务端组件里用 axios 可以发请求,但失去了 Next.js 的缓存能力
tsx
// 客户端组件里用 axios ------ 完全没问题
"use client";
import axios from "axios";
import { useEffect, useState } from "react";
export default function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
axios.get("/api/data").then((res) => setData(res.data));
}, []);
return <div>{data}</div>;
}
总结:服务端用原生 fetch + Next.js 缓存,客户端交互请求用 axios 或 fetch 都行。
三、路由系统
文件即路由
bash
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── champions/
│ ├── page.tsx → /champions
│ └── [id]/
│ └── page.tsx → /champions/任意值(动态路由)
└── blog/
└── [...slug]/
└── page.tsx → /blog/任意/层级/路径(捕获所有路由)
动态路由取参数
tsx
// app/champions/[id]/page.tsx
interface Props {
params: Promise<{ id: string }>;
}
export default async function ChampionPage({ params }: Props) {
const { id } = await params; // Next.js 15+ params 是 Promise
// id 就是 URL 里的值,比如访问 /champions/Jinx,id = "Jinx"
}
编程式跳转(客户端)
tsx
"use client";
import { useRouter } from "next/navigation";
export default function MyButton() {
const router = useRouter();
return (
<button onClick={() => router.push("/champions")}>
去英雄列表
</button>
);
}
Link 组件(推荐用于导航)
tsx
import Link from "next/link";
// 比 <a> 标签好,支持预加载
<Link href="/champions/Jinx">查看金克斯</Link>
四、路由守卫(权限控制)
Next.js 没有 Vue Router 那种内置的 beforeEach 守卫,但有几种方式实现。
方式一:middleware.ts(推荐,最强大)
在项目根目录创建 middleware.ts,它会在请求到达页面之前执行,适合做登录验证:
ts
// middleware.ts(放在项目根目录,和 src/ 同级)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("token")?.value;
const isLoginPage = request.nextUrl.pathname === "/login";
// 没有 token 且不是登录页,跳转到登录页
if (!token && !isLoginPage) {
return NextResponse.redirect(new URL("/login", request.url));
}
// 已登录还访问登录页,跳转首页
if (token && isLoginPage) {
return NextResponse.redirect(new URL("/", request.url));
}
return NextResponse.next(); // 放行
}
// 配置哪些路由需要走 middleware(不写默认全部)
export const config = {
matcher: ["/dashboard/:path*", "/profile/:path*", "/login"],
};
方式二:服务端组件里直接判断
tsx
// app/dashboard/page.tsx
import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth"; // 你自己的获取 session 函数
export default async function DashboardPage() {
const session = await getSession();
if (!session) {
redirect("/login"); // 服务端直接重定向,用户看不到页面内容
}
return <div>欢迎,{session.user.name}</div>;
}
方式三:客户端组件里判断
tsx
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
export default function ProtectedPage() {
const router = useRouter();
useEffect(() => {
const token = localStorage.getItem("token");
if (!token) {
router.push("/login");
}
}, []);
return <div>受保护的内容</div>;
}
三种方式对比:
| 方式 | 执行时机 | 适合场景 |
|---|---|---|
| middleware | 请求到达前 | 全局权限控制,性能最好 |
| 服务端组件 redirect | 服务器渲染时 | 单个页面的权限判断 |
| 客户端 useEffect | 浏览器渲染后 | 简单场景,会有短暂闪烁 |
推荐用 middleware,一次配置,全局生效,用户甚至看不到页面内容就被重定向了。
五、API 路由(后端接口)
Next.js 可以在同一个项目里写后端接口,不需要单独起一个服务。
bash
app/
└── api/
└── champions/
└── route.ts → GET/POST /api/champions
ts
// app/api/champions/route.ts
import { NextResponse } from "next/server";
// 处理 GET 请求
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const name = searchParams.get("name");
const data = { champions: ["Jinx", "Lux", "Darius"] };
return NextResponse.json(data);
}
// 处理 POST 请求
export async function POST(request: Request) {
const body = await request.json();
// 处理 body...
return NextResponse.json({ success: true }, { status: 201 });
}
前端调用:
ts
const res = await fetch("/api/champions?name=Jinx");
const data = await res.json();
六、数据获取的三种模式
理解这三种模式,就理解了 Next.js 的核心价值。
SSG(静态生成)------ 构建时生成
ts
fetch(url, { cache: "force-cache" }); // 或者不传 cache 参数
- 构建时请求一次,生成静态 HTML
- 适合:不常变化的内容(博客文章、文档)
- 优点:最快,CDN 可以缓存
ISR(增量静态再生)------ 定时刷新
ts
fetch(url, { next: { revalidate: 3600 } }); // 每小时刷新
- 先返回缓存内容,后台定时重新生成
- 适合:数据偶尔更新(本项目的英雄数据)
- 优点:兼顾性能和数据新鲜度
SSR(服务端渲染)------ 每次请求都重新获取
ts
fetch(url, { cache: "no-store" });
- 每次用户访问都重新请求数据
- 适合:实时数据(用户个人信息、股票价格)
- 优点:数据永远最新
七、Image 组件
Next.js 内置的 <Image> 比普通 <img> 强很多:
tsx
import Image from "next/image";
<Image
src="https://ddragon.leagueoflegends.com/cdn/15.8.1/img/champion/Jinx.png"
alt="金克斯"
width={64}
height={64}
className="rounded-md"
/>
自动做的事情:
- 懒加载(滚动到才加载)
- 自动转换为 WebP 格式(更小)
- 防止布局偏移(需要指定 width/height)
注意 :加载外部域名的图片需要在 next.config.js 里配置白名单:
js
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "ddragon.leagueoflegends.com",
},
],
},
};
八、环境变量
bash
# .env.local(本地开发,不提交 git)
DATABASE_URL=mysql://localhost:3306/mydb
NEXT_PUBLIC_API_URL=https://api.example.com # NEXT_PUBLIC_ 前缀才能在浏览器用
SECRET_KEY=abc123 # 没有前缀,只能在服务端用
使用:
ts
// 服务端(任何地方都能用)
const secret = process.env.SECRET_KEY;
// 客户端(只能用 NEXT_PUBLIC_ 开头的)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
九、常见错误和解决方法
错误1:在服务端组件里用了 useState
vbnet
Error: useState is not a function
解决:组件顶部加 "use client",或者把有状态的部分拆成单独的客户端组件。
错误2:外部图片不显示
javascript
Error: Invalid src prop, hostname not configured
解决:在 next.config.js 的 images.remotePatterns 里加上对应域名。
错误3:hydration 错误
sql
Error: Hydration failed because the initial UI does not match
原因:服务端渲染的 HTML 和客户端渲染的结果不一致(比如用了 Math.random() 或 Date.now())。
解决:把这类逻辑放到 useEffect 里,确保只在客户端执行。
错误4:params 需要 await(Next.js 15+)
csharp
Error: params should be awaited before using its properties
解决:
tsx
// 错误写法
const { id } = params;
// 正确写法
const { id } = await params;
十、项目目录约定速查
| 文件/目录 | 作用 |
|---|---|
app/layout.tsx |
全局布局,所有页面的外壳 |
app/page.tsx |
首页(/) |
app/loading.tsx |
页面加载中的 UI |
app/error.tsx |
页面报错时的 UI |
app/not-found.tsx |
404 页面 |
app/api/*/route.ts |
API 接口 |
middleware.ts |
请求中间件(路由守卫) |
public/ |
静态资源,直接通过 /文件名 访问 |
.env.local |
本地环境变量 |
next.config.js |
Next.js 配置文件 |
十一、开发常用命令
bash
pnpm dev # 启动开发服务器(热更新)
pnpm build # 构建生产版本
pnpm start # 启动生产服务器(需要先 build)
pnpm lint # 检查代码规范
构建时 Next.js 会告诉你每个页面是 SSG、SSR 还是 ISR,输出类似:
scss
Route (app) Size First Load JS
┌ ○ / 2.5 kB 88.3 kB
├ ○ /augments 1.2 kB 87.0 kB
├ ○ /builds 3.1 kB 88.9 kB
└ ƒ /champions/[id] 1.8 kB 87.6 kB
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
○ 表示静态,ƒ 表示动态(每次请求都渲染)。