主题
Next.js 路由
基础篇
1. 静态路由
Next.js与React不同,Next,js提供了一套内置的文件路由系统,文件的命名与路由的生成有着直接的对应关系
具体来说,app目录下的page.tsx文件会自动映射为页面的路由,例如,app/page.tsx会被解析为根路由“/”。同样地,app/login/page.tsx则会代表着“/login”这一路由。
目录结构对应路由
| 目录结构 | 对应路由 |
|---|---|
| app/page.js | / |
| app/dashboard/page.js | /dashboard |
| app/dashboard/settings/page.js | /dashboard/settings |
2. 嵌套路由
layout.tsx会自动映射为多个路由之间共享的UI组件
1. 根布局
在app目录下放置的layout.tsx文件将作为根布局,它是所有路由共用的UI基础。在这个文件中,可以引入全局样式、公共组件等,确保它们能够应用到整个应用程序的每一个页面。
根布局必须包含html和body标签
tsx
//app/layout.tsx
const RootLayout = ({ children }: React.PropsWithChildren) => (
<html lang="en">
<body>{children}</body>
</html>
);
export default RootLayout;2. 嵌套布局
在app目录的各个子文件夹下创建layout.tsx文件。这些子文件夹下的layout.tsx文件将作为该路由下的共享UI组件,仅在该子文件夹下的页面之间共享。
新建src/app/home/layout.tsx
tsx
export default function HomeLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section>
布局页面
{children}
</section>
);
}新建src/app/home/user/page.tsx
tsx
const User = () => <div>用户页面</div>;
export default User;此时的目录结构为:
上面的路由形式可以理解为react-router-dom V6的嵌套路由配置
js
const routes = [
{
element: <HomeLayout />,
children: [
{
path: "/home/user",
element: <Manage />,
},
],
},
];访问http://localhost:3001/home/user查看效果(默认端口号为3000,我这里启动端口修改成了3001)
3. 动态路由
在Next.js中,要实现动态路由,你需要在pages目录的相应位置使用方括号来创建特殊的文件夹名称,例如[id]或[slug]。动态字段将作为参数传递给layout、page、route和generateMetadata函数
1. [folderName]
动态路由的基本用法,新建src/app/blog/[id]/page.tsx
tsx
const Blog = ({ params }: { params: { id: string } }) => {
return <div>{params.id}博客页面</div>;
};
export default Blog;访问路由“/blog/2024”,页面将呈现“2024博客页面”
2. [...folderName]
如果访问路由“/blog/2024/1”,则页面会显示404。可在方括号内添加省略号[...folderName],捕获后面所有的路由片段
新建src/app/blog/[...year]/page.tsx
tsx
const Blog = ({ params }: { params: { year: string[] } }) => {
return (
<div>
{params.year.map((o, index) => (
<div key={index}>{o}</div>
))}
</div>
);
};
export default Blog;访问路由“/blog/2024/1”,页面将会显示20241
目录结构,对应路由与-参数的关系如下所示:
| 目录结构 | 对应路由 | 参数 |
|---|---|---|
| app/blog/[...year]/page.tsx | /blog/a | |
| app/blog/[...year]/page.tsx | /blog/a/b | |
| app/blog/[...year]/page.tsx | /blog/a/b/c |
3. [[...folderName]]
blog/[...year]能捕获/blog/a,也能捕获/blog/a/b,但不能捕获/blog,访问/blog将会得到404页面。可通过在双方括号内添加省略号的形式,捕获包括自身的路由段。
将上述[...year]修改为[[...year]],访问/blog得到的对应参数为空对象 {},因此再修改下src/app/blog/[[...year]]/page.tsx页面
tsx
const Blog = ({ params }: { params: { year: string[] } }) => {
return (
<div>
{params.year ? (
params.year.map((o, index) => <span key={index}>{o}</span>)
) : (
<span>blog页面</span>
)}
</div>
);
};
export default Blog;访问/blog将看到页面显示“blog页面”
4. 路由组
路由组是为了在项目比较大时,更好的管理路由文件的一种方式。使用中括号来创建特殊的文件夹名称,例如(marketing)或(shop)。
路由组不会影响url结构,例如app/(marketing)/about/page.js对应的路由为/about
5. 平行路由
1. 基本使用
平行路由可以使你在同一个布局中同时或者有条件的渲染一个或者多个页面。文件夹以@作为开头进行命名,这个文件夹下面的 page.js 将会自动注入文件夹同级 layout 的 props 中
新建平行路由analytics
新建app/home/@analytics/page.tsx
tsx
export default function Page() {
return <h1>分析页</h1>;
}新建平行路由team
新建app/home/@team/page.tsx
tsx
export default function Page() {
return <h1>团队页</h1>;
}新建/home的布局文件
新建app/home/layout.tsx
tsx
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<section>
{children}
{team}
{analytics}
</section>
);
}2. 约定文件 default
default.tsx是平行路由的回退页面,也就是说当匹配不到平行路由时,会显示平行路由下default.tsx的内容
新建app/home/user/page.tsx
tsx
export default function Page() {
return <h1>用户页</h1>;
}此时访问路由“/home/user”,页面会显示404,明明创立了该文件,为什么访问不到?
因为app/home/layout.tsx文件中定义了三个插槽,分别为children,team,analytics。user下的page.tsx默认匹配了children,但team和analytics两个插槽没匹配上,因此会报404页面,可在平行路由下定义default.tsx
新建app/home/@analytics/default.tsx
tsx
export default function Page() {
return <h1>分析页的默认页</h1>;
}新建app/home/@team/default.tsx
tsx
export default function Page() {
return <h1>team的默认页</h1>;
}此时再访问路由“/home/user”,页面将显示
text
用户页
team的默认页
分析页的默认页如果不想访问home的子路由被平行路由插槽影响时,default.tsx可返回一个null,将@analytics/default.tsx,@team/default.tsx分别改为
tsx
export default function Page() {
return null;
}再访问路由“/home/user”,页面将显示
text
用户页3. 平行路由的子路由
在平行路由的基本使用中,实现了将analytics和team插入到home的layout中。当然使用组件也能实现同样的效果,接下来看平行路由与组件的不同点:平行路由可以创建自己的子路由
新建app/home/@analytics/behavior/page.tsx
tsx
export default function Page() {
return <h1>行为分析</h1>;
}新建app/home/@analytics/performance/page.tsx
tsx
export default function Page() {
return <h1>性能分析</h1>;
}新建app/home/@analytics/layout.tsx,进行布局
tsx
import Link from "next/link";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Link href="/home/behavior">behavior Views</Link>
----------
<Link href="/home/performance">performance Views</Link>
</nav>
<div>{children}</div>
</>
);
}在访问路由时,平行路由@开头的文件名可省略,例如这里app/home/@analytics/behavior/page.tsx对应着路由“/home/behavior”,访问/home/behavior页面显示404,这是为什么?
因为在app/home/layout.tsx中,我们定义了三个插槽children,team,analytics,此时访问平行路由“/home/behavior”,意味着只匹配上了插槽analytics,而children,team没有匹配上,在之前我们定义了@team/default.tsx,但没有定义默认插槽children的回退内容
新建app/home/default.tsx
tsx
export default function Page() {
return <div>home的默认页</div>;
}再次访问路由“/home/behavior”,页面将会正常显示
6. 拦截路由
拦截路由允许你在当前布局内加载应用其他部分的路由,通常与平行路由一同使用,以实现Modal对话框效果
例如图片中左边的路由为“/feed”,当点击图片进行路由跳转到“/photo/123”时,浏览器上的路径被更改为“/photo/123”,但/photo/123的内容以对话框Modal的形式展示在路由“/feed”上
而当在浏览器中直接输入路由“/photo/123”或刷新浏览器时,页面将直接呈现/photo/123的内容,而非以对话框Modal的形式呈现
在 Next.js 中,实现拦截路由需要你在命名文件夹的时候以 (..) 开头,规则为:
| 文件命名 | 匹配层级 |
|---|---|
| (.) | 表示匹配同一层级 |
| (..) | 表示匹配上一层级 |
| (..)(..) | 表示匹配上上层级 |
| (...) | 表示匹配根目录 |
但是要注意的是,这个匹配的是路由的层级而不是文件夹的层级,就比如路由组、平行路由这些不会影响 URL 的文件夹就不会被计算层级。
新建app/food/photo/page.tsx
该页面为输入路由“/food/photo”或刷新浏览器时,页面呈现的内容
tsx
export default function Page() {
return <article>路由photo</article>;
}新建app/food/@modal/(.)photo/page.tsx
平行路由@modal不算url的层级,因此文件夹photo与文件夹(.)photo都在文件夹food下,属于同一层级,因此括号中使用一个点
tsx
export default function Page() {
return (
<div
style={{
width: 500,
height: 500,
position: "absolute",
background: "blue",
}}
>
拦截路由photo
</div>
);
}新建app/food/layout.tsx
在layout中定义平行路由的插槽modal
tsx
export default function Layout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<section>
{children}
{modal}
</section>
);
}新建app/food/page.tsx
定义一个跳转到/photo的链接
tsx
import Link from "next/link";
export default function Page() {
return (
<article>
<Link href="/food/photo">图片</Link>
</article>
);
}新建app/food/default.tsx
定义平行路由children插槽的默认文件
tsx
export default function Default() {
return null;
}新建app/food/@modal/default.tsx
定义平行路由modal插槽的默认文件
tsx
export default function Default() {
return null;
}重启项目,访问路由/food,点击图片跳转到/food/photo,但页面显示的内容是@modal/(.)photo中的内容;刷新浏览器,此时路由为/food/photo,页面直接显示为“路由photo”
7. API 路由
API 路由在Next.js 14中又被称为Route Handlers(路由拦截器)。作为node中间层,可用来封装业务逻辑,减少客户端bundle体积;也可作为网关代理,隐藏真实后端接口
1. 基本使用
与page.tsx文件会自动映射为页面的路由一样,app目录下的route.ts文件会映射为一个api接口
为了避免route.ts与page.tsx冲突,可在app下新建api文件夹,专门用于存放API路由
新建app/api/route.ts
ts
export async function GET() {
return Response.json({ name: "张三" });
}启动项目,例如我的端口号为3001,在浏览器中访问http://localhost:3001/api可以看到接口返回
修改app/page.tsx
在路由页面中使用API 路由
tsx
async function getData() {
const data = await fetch("http://localhost:3001/api");
return data.json();
}
export default async function Home() {
const data = await getData();
return <article>{data.name}</article>;
}访问http://localhost:3001,页面显示为张三
在项目中,可封装后台的业务逻辑
ts
export async function GET() {
const res = await fetch("https://www.test.com/...");
const data = await res.json();
//处理数据...
return Response.json({ data });
}2. 动态API路由
动态API路由文件命名规则与路由文件一致
1. [folderName]
获取单个参数
新建app/api/[id]/route.ts
ts
//参数使用params获取
export async function GET(
request: Request,
{ params }: { params: { id: string } },
) {
return Response.json({ name: "张三", id: params.id });
}访问http://localhost:3001/api/1,页面返回
2. [...folderName]
获取多个参数(至少得有一个参数)
新建app/api/[...key]/route.ts
ts
//字段key返回一个数组
export async function GET(
request: Request,
{ params }: { params: { key: string[] } },
) {
const key = params.key;
return Response.json({ key });
}访问http://localhost:3001/api/1/2,页面返回
3. [[...folderName]]
获取多个参数(可一个参数都没有)
新建app/blog/[[...key]]/route.ts
ts
export async function GET(
request: Request,
{ params }: { params: { key: string[] } },
) {
const key = params.key;
return Response.json({ key });
}访问http://localhost:3001/api/blog,页面返回一个空对象{}
8. 私有路由
在app中定义的components文件夹,存放的都是组件,肯定不希望渲染为页面。一种方法是文件夹下不定义名为page.tsx的文件;另一种方法是使用下划线定义文件夹名,这样该文件夹及其子文件夹都不会被渲染为路由页面
9. 声明式、编程式导航
1. 声明式导航
使用Link组件进行路由跳转
tsx
import Link from "next/link";
export default function Page() {
return <Link href="/dashboard">Dashboard</Link>;
}如果想要增加激活状态的样式,需使用usePathname
tsx
"use client";
import { usePathname } from "next/navigation";
import Link from "next/link";
export function Links() {
const pathname = usePathname();
return (
<nav>
<ul>
<li>
<Link className={`link ${pathname === "/" ? "active" : ""}`} href="/">
Home
</Link>
</li>
<li>
<Link
className={`link ${pathname === "/about" ? "active" : ""}`}
href="/about"
>
About
</Link>
</li>
</ul>
</nav>
);
}2. 编程式导航
使用hook进行路由跳转
tsx
"use client";
import { useRouter } from "next/navigation";
export default function Page() {
const router = useRouter();
return (
<button type="button" onClick={() => router.push("/dashboard")}>
Dashboard
</button>
);
}因为使用了hook,因此在这里需使用'use client',显式声明是客户端组件
10. 路由传参
1. 动态路由匹配
1. 定义路由跳转
修改src/app/page.tsx
tsx
import Link from "next/link";
const Home = () => (
<div className="App">
<Link href={"/blog/1"}>博客</Link>
</div>
);
export default Home;2. 获取参数
(1)在服务端组件中,使用params获取
使用方括号来创建动态路由文件,上文已创建过src/app/blog/[id]/page.tsx
tsx
const Blog = ({ params }: { params: { id: string } }) => {
return <div>{params.id}博客页面</div>;
};
export default Blog;(2)在客户端组件中,使用useParams获取
修改src/app/blog/[id]/page.tsx
tsx
"use client";
import { useParams } from "next/navigation";
export default function Blog() {
const params = useParams();
return <div>{params.id}博客页面</div>;
}2. query 传参
1. 编程式传参
修改src/app/page.tsx
tsx
import Link from "next/link";
const Home = () => (
<div className="App">
<Link
href={{
pathname: "/new",
query: {
sort: "name",
id: "2",
},
}}
>
新闻页
</Link>
</div>
);
export default Home;2. 命令式传参
修改src/app/page.tsx
tsx
"use client";
import { useRouter } from "next/navigation";
const Home = () => {
const router = useRouter();
return (
<div className="App">
<button onClick={() => router.push("/new?sort=name&id=2")}>新闻页</button>
</div>
);
};
export default Home;3. 获取参数
新建src/app/new/page.tsx
tsx
"use client";
import { Suspense } from "react";
import { useSearchParams } from "next/navigation";
function New() {
const searchParams = useSearchParams();
return <Suspense>Search: {searchParams.get("id")}</Suspense>;
}
export default function Page() {
return (
<Suspense>
<New />
</Suspense>
);
}使用Suspense包裹,避免useSearchParams获取客户端路由时页面空白
进阶篇
1. 配置404页面
not-found.tsx文件用于处理页面未找到的情况
新建src/app/not-found.tsx
tsx
export default function NotFound() {
return <div>我是自定义404页面</div>;
}2. 错误处理
在路由组件加载过程中发生错误时展示的元素
error.tsx文件用于处理页面报错的情况
新建src/app/error.tsx
tsx
"use client";
import { useEffect } from "react";
export default function Error({
error,
}: {
error: Error & { digest?: string };
}) {
useEffect(() => {
// 收集错误日志,发送给服务端
console.error("11", error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
</div>
);
}提示:error.tsx只能为客户端组件
新建src/app/user/page.tsx
tsx
const obj: any = {
a: "2",
};
export default function New() {
return (
<div>
{/* 报错:因为取不到c属性 */}
{obj.b.c}
</div>
);
}访问/user路由,可以看到页面显示为“Something went wrong!”
3. 动态路由的静态参数
例如博客页面,博客1的路由为/blogs/1,博客2的路由为/blogs/2,博客的id都是固定的,不会随着用户的每次请求而变化。
对于这些动态路由,可使用 generateStaticParams 定义路由中的静态参数,这些参数在构建时会被预先计算并用于生成静态页面。从而提高页面的性能和效率。
generateStaticParams返回一个数组,数组中的每一项为页面的静态参数,页面通过params去接收。
例如返回[{ id: '1' }, { id: '2' }]。{ id: '1' }为一个页面的静态参数,在props中通过params获取到
新建src/app/blog/[id]/page.tsx
tsx
export async function generateStaticParams() {
//const products = await fetch('https://.../products').then((res) => res.json())
//模拟通过接口返回所有id信息
return [{ id: "1" }, { id: "2" }];
}
export default async function Blog({ params }: { params: { id: string } }) {
//可通过id去请求接口,获取博客详情页
return <div>{params.id}博客页面</div>;
}运行npm run builld进行打包,在.next/server/app/blog文件夹下,可以看到生成了2个html文件,分别为1.html,2.html。都各自预渲染了相应内容
相比于用户请求时再创建相应页面,在构建时预渲染页面,显然能显著的提高页面的打开速度
4. 中间件
中间件能够在请求处理完成之前执行特定的代码。例如进行重定向、修改请求或响应头等
在src目录下新建middleware.ts
ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
console.log("===request", request.method, request.url);
return NextResponse.next();
}
// 中间件生效的路由规则
export const config = {
matcher: "/blog/:id*",
};访问http://localhost:3001/blog/2,终端将打印request请求信息
5. 重定向
1. 客户端组件重定向
使用useRouter进行重定向
点击按钮,使用router.push进行路由跳转
tsx
"use client";
import { useRouter } from "next/navigation";
export default function Page() {
const router = useRouter();
return (
<button type="button" onClick={() => router.push("/login")}>
Dashboard
</button>
);
}2. 服务端组件重定向
当接口请求失败时,使用redirect方法进行重定向
tsx
import { redirect } from "next/navigation";
async function fetchTeam(id) {
const res = await fetch("https://...");
if (!res.ok) return undefined;
return res.json();
}
export default async function Profile({ params }) {
const team = await fetchTeam(params.id);
if (!team) {
redirect("/login");
}
// ...
}3. 中间件重定向
当cookie中没有验证信息时,使用NextResponse.redirect进行重定向
ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("token");
if (token) {
return NextResponse.next();
} else {
return NextResponse.redirect(new URL("/login", request.url));
}
}
export const config = {
matcher: "/blog/:path*",
};