Next.js 初学者核心知识点

基于本项目(海克斯大乱斗攻略站)讲解,结合实际代码理解概念。


一、服务端组件 vs 客户端组件

这是 Next.js App Router 最核心的概念,没搞清楚这个,后面全是坑。

服务端组件(默认)

app/ 目录下的组件默认都是服务端组件,特点:

  • 可以直接 async/await,在服务器上跑
  • 可以直接访问数据库、调接口,不暴露给浏览器
  • 不能用 useStateuseEffectonClick 等浏览器 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>
  );
}
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.jsimages.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

表示静态,ƒ 表示动态(每次请求都渲染)。

相关推荐
张一凡932 小时前
easy-model 在数据可视化仪表板中的应用
前端·react.js
学以智用2 小时前
# Vue3 AJAX 请求数据
前端·vue.js
miss2 小时前
JavaScript 异步循环完全指南:从踩坑到最佳实践
前端
家里有蜘蛛2 小时前
从 Webpack 迁移到 Rspack 后,循环依赖为什么炸了?一个 const vs var 引发的血案
前端
山_雨2 小时前
前端重连机制
前端
Cache技术分享2 小时前
355. Java IO API -去除路径中的冗余信息
前端·后端
牛马1112 小时前
Flutter CustomPaint
开发语言·前端·javascript
炽烈小老头2 小时前
函数式编程范式(三)
前端·typescript
ruoyusixian3 小时前
chrome二维码识别查插件
前端·chrome