后端转全栈之Next.js 路由系统App Router

Next.js 路由系统App Router

本文概括:

  1. 文件即路由规则 :文件夹结构决定 URL,对应的特殊文件有 layout.tsx、loading.tsx、error.tsx、not-found.tsx、template.tsx。

  2. 路由段类型 :支持动态路由、多级动态路由、Catch-all([...slug]、[[...slug]])等。

  3. 参数获取方式:

    • 动态段参数:服务端用 params,客户端用 useParams。
    • 查询参数:服务端用 searchParams,客户端用 useSearchParams。
  4. 路由导航:

    • 声明式:<Link />。
    • 服务端:redirect()。
    • 客户端:useRouter.push()。
  5. 路由控制:

    • 404 页面:not-found.tsx 或 notFound()。
    • 路由组 (group) 和私有 _ 路由。
  6. 布局文件 :layout.tsx 可嵌套,用于 UI 框架复用。

  7. Metadata(SEO) :

    • 静态:export const metadata。
    • 动态:generateMetadata()。
    • 继承 & 覆盖规则,SEO 最佳实践。
  8. 其他 API :usePathname、useParams 对比,适用场景。

服务端&客户端 API对照表:

功能 服务端可用 客户端可用
获取动态参数 params useParams
获取查询参数 searchParams useSearchParams
路由跳转 redirect() useRouter.push() / router.replace()
获取 pathname ❌ usePathname()
404 控制 notFound() ❌
Metadata metadata / generateMetadata() ❌
错误边界 error.tsx(需 "use client") error.tsx 内逻辑
Loading 状态 loading.tsx ❌
布局 layout.tsx ❌
声明式导航 Link(通用) Link(通用)

文件即路由规则

  • 规则 :app/segment/page.tsx → /segment

  • 特殊文件:

    • layout.tsx:布局,可嵌套
    • loading.tsx:加载态骨架
    • error.tsx:错误边界(必须 "use client")
    • not-found.tsx:404 页面
    • template.tsx:强制每次重新渲染

路由段类型

动态路由/blog/[slog]

对于: /app/blog/[slug]/page.tsx 的文件路由,想要获取 slug 参数的方式如下:

服务端中:

javascript 复制代码
export default function Page({ params }: { params: { slug: string } }) {
  return <div>{params.slug}</div>;
}

客户端中:

typescript 复制代码
"use client";
import { useParams } from "next/navigation";
const { slug } = useParams<{ slug: string }>();

多级动态路由 /blog/[blogId]/reviews/[reviewId]

对于: /app/blog/[blogId]/reviews/[reviewId]/page.tsx 的文件路由,想要获取 blog 和 reviewId 的方式为:

服务端中:

typescript 复制代码
export default function ReviewPage({ params }: {
  params: { blogId: string; reviewId: string }
}) {
  return (
    <div>
      Review {params.reviewId} for blog {params.blogId}
    </div>
  );
}

客户端中:

typescript 复制代码
"use client";
import { useParams } from "next/navigation";

export default function ReviewClient() {
  const { blogId, reviewId } = useParams<{ blogId: string; reviewId: string }>();
  return (
    <div>
      Review {reviewId} for blog {blogId}
    </div>
  );
}

Catch-all路由

这种路由的使用场景是,比如我要访问 /docs/feature1/concept1/example1,这样后面会有很多路由,如何在一个文件中获取?

可以使用:/src/app/docs/[...slug]/page.tsx

这种路由有两种形式:

lua 复制代码
/app/docs/[...slug]/page.tsx
/app/docs/[[...slug]]/page.tsx
文件 匹配示例 说明
[...slug] /docs/a、/docs/a/b、/docs/a/b/c 必须 ≥ 1 个段
[[...slug]] /docs、/docs/a、/docs/a/b 可选,0 个或多个段

比如想要访问 /docs/ 的时候也有页面,那么可以使用:/src/app/docs/[[...slug]]/page.tsx

服务端中获取参数:

csharp 复制代码
export default function DocPage({ params }: { params: { slug?: string[] } }) {
  return (
    <div>
      Doc Page {params.slug ? params.slug.join("/") : "index"}
    </div>
  );
}

客户端中获取参数:

csharp 复制代码
"use client";
import { useParams } from "next/navigation";

export default function DocClient() {
  const { slug } = useParams<{ slug?: string[] }>();
  return (
    <div>
      Doc Page {slug ? slug.join("/") : "index"}
    </div>
  );
}

查询参数获取

这里需要获取的内容是: xxx?q=xxx 这种场景,需要获取 q 是什么

服务端获取:

javascript 复制代码
export default function Page({ searchParams }: { searchParams: { q?: string } }) {
  return <div>{searchParams.q}</div>;
}

客户端获取:

ini 复制代码
"use client";
import { useSearchParams } from "next/navigation";
const sp = useSearchParams();
const q = sp.get("q");

路由导航

声明式导航,使用Next.js提供的 Link组件:

javascript 复制代码
import Link from "next/link";
<Link href="/about">About</Link>

服务端重定向:

javascript 复制代码
import { redirect } from "next/navigation";
redirect("/login");

客户端编程式,使用 useRouter进行跳转:

ini 复制代码
"use client";
import { useRouter } from "next/navigation";
const router = useRouter();
router.push("/dashboard");

路由控制

404控制:

全局的可以直接写 not-fount.tsx 文件里

代码里触发可以使用:

javascript 复制代码
import { notFound } from "next/navigation";
if (!data) notFound();

路由组(group)

常用于:代码组织 和 独立布局。

例如我现在有几个页面:

bash 复制代码
app/
 ├─ (auth)/
 │   ├─ login/page.tsx
 │   ├─ register/page.tsx
 │   └─ layout.tsx
 ├─ (marketing)/
 │   ├─ about/page.tsx
 │   └─ layout.tsx
 └─ layout.tsx   // 全局布局

他们的URL 会映射为

  • app/(auth)/login/page.tsx → /login
  • app/(marketing)/about/page.tsx → /about

好处如下:

  • 代码归类 :把登录/注册页面放在 (auth) 文件夹里,看起来整洁,但 URL 没有 /auth 前缀。
  • 局部布局 :在 (auth)/layout.tsx 定义一个不带导航栏/页脚的布局,而全局其他页面继续用全局 layout.tsx。

私有路由:

  • 在目录名前面加下划线 _,该段不会出现在 URL。
  • 用于:代码组织/内部实现,不暴露为路由。

如果想要一个路由私有,不被访问,可以使用_开头,例如app/_lib/page.tsx


布局文件:

layout.tsx文件可以定义布局,可以嵌套使用,

javascript 复制代码
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

Metadata(SEO)

Metadata 是 页面/路由的元信息 ,用于 SEO、社交分享、浏览器行为控制 。Next.js 在 App Router 中内置了 Metadata API ,帮你自动生成 <head> 标签内容,而不用手动写 <head>。可以自动注入 到 HTML 的 <head>,如 <title>、<meta>、<link> 等。

使用:

  • 静态定义 :直接在页面或布局文件中 export const metadata。
  • 动态生成 :通过 export async function generateMetadata() 返回。

📌 作用范围:

  • 可以定义在 page.tsx → 仅作用于该页面。
  • 可以定义在 layout.tsx → 作用于该布局下的所有子页面。
  • 支持嵌套,子页面的 metadata 会覆盖/合并父布局的 metadata。

静态Metadata:

在页面/布局文件里直接导出一个 metadata 常量:

typescript 复制代码
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "My Blog",
  description: "Welcome to my blog",
};

适合的场景为:固定的 SEO 信息,如静态 About 页面。这种方式编译时就确定,不依赖运行时数据。


动态Metadata:

如果 metadata 需要依赖 路由参数 / 数据库 / API ,就用 generateMetadata:

typescript 复制代码
import type { Metadata } from "next";

type Props = {
  params: { id: string };
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const product = await fetch(`https://api.example.com/products/${params.id}`).then(res => res.json());

  return {
    title: product.name,
    description: product.description,
    openGraph: {
      images: [product.image],
    },
  };
}

常用的字段有:

字段 作用
title 网页标题
description 描述,搜索引擎摘要
keywords 关键词(SEO 较弱化,但仍可加)
robots 搜索引擎爬虫规则(如 noindex, nofollow)

metadata覆盖问题:

  • 在 layout.tsx 定义的 metadata 会作用于整个子树。
  • 在 page.tsx 定义的 metadata 会覆盖布局里的同名字段。

例如:

less 复制代码
app/
 ├─ layout.tsx          (title: "My App")
 ├─ page.tsx            (title: "Home")
 └─ blog/
     ├─ layout.tsx      (title: "Blog")
     └─ [id]/page.tsx   (title: 动态文章标题)

访问:

  • / → title = "Home"
  • /blog → title = "Blog"
  • /blog/123 → title = 动态文章标题

比较好的SEO优化实践为:

  • 静态优先,动态按需 :静态页面用 metadata,详情页用 generateMetadata。
  • 统一 Open Graph:在全局 layout 设置默认 OG 图,文章页再覆盖。
  • 社交卡片优化 :为每个文章/产品单独设置 og:image,提升分享效果。

注意:Metadata(不管是 metadata 还是 generateMetadata)只能在服务端用,不能在客户端用。

其他API

usePathname 当前路径的字符串,使用例子如下:

javascript 复制代码
"use client";
import { usePathname, useSearchParams } from "next/navigation";

export default function Example() {
  const pathname = usePathname();          // "/products/shoes"
  const sp = useSearchParams();
  const q = sp.get("q");                   // "nike"
  const page = sp.get("page");             // "2"

  return (
    <div>
      <p>Path: {pathname}</p>
      <p>q: {q}</p>
      <p>page: {page}</p>
    </div>
  );
}
相关推荐
计算机魔术师39 分钟前
DeepSeek 据报道接近完成至少 800 亿元融资,腾讯与宁德时代参与
前端
可乐ea1 小时前
多模型编排的三层:框架、模型路由与提供商路由
前端·网络·人工智能·ai智能体·多智能体协作·多模型编排·大模型路由
青柠之夏cc3 小时前
前端拖拽功能原生实现,不引入拖拽库完成业务
开发语言·前端·python
liangshanbo12153 小时前
前端大文件切片上传怎么做?
前端
Sand(ContextGate)4 小时前
Python Agent 测试实战:测试与评估,让 Agent 像传统软件一样可交付
前端·javascript·python·microsoft·ai
广州华水科技4 小时前
单北斗GNSS变形监测系统在城市安全与地质灾害中的应用前景
前端
颜进强4 小时前
20 · NestJs循环依赖与 forwardRef:容器为什么在环面前会死,"占位再补齐"怎么救
前端·后端·ai编程
颜进强4 小时前
19 · NestJs @Global 落地账:装饰器与 `isGlobal` 参数,各在什么场景上岗
前端·后端·ai编程
码艺-Alimjan4 小时前
Tauri 网站To桌面应用实战总结
前端·javascript·vue
颜进强4 小时前
18 · NestJS动态模块与 forRoot:`imports: [ConfigModule.forRoot({...})]` 到底在 import 什么
前端·后端·ai编程