App Router
前言
路由(Router)是 Next.js 应用的重要组成部分。在 Next.js 中,路由决定了一个页面如何渲染或者一个请求该如何返回。
Next.js 有两套路由解决方案,之前的方案称之为Pages Router
,目前的方案称之为App Router
,两套方案目前是兼容的,都可以在 Next.js 中使用。
但是从13.4
起 App Router
已成为默认的路由方案,新的 Next.js 项目建议使用 App Router
。
我们介绍的就是App Router
。
从 Pages Router 到 App Router
现在你打开使用 create-next-app
创建的项目,你会发现默认并没有 pages
这个目录。查看 packages.json
中的 Next.js
版本,如果版本号大于 13.4
,那就对了!
Next.js 从 v13 起就使用了新的路由模式 —— App Router
。之前的路由模式我们称之为 Pages Router
,为保持渐进式更新,依然存在。从 v13.4 起,App Router
正式进入稳定化阶段,App Router
功能更强、性能更好、代码组织更灵活,以后就让我们使用新的路由模式吧!
Pages Router 和 App Router 的区别:
以前我们的目录结构类似
└── pages
├── index.js
├── about.js
└── more.js
这种方式有一个弊端,那就是 pages
目录的所有 js
文件都会被当成路由文件,这就导致比如组件不能写在 pages
目录下,这就不符合开发者的使用习惯。(当然 Pages Router 还有很多其他的问题,只不过目前我们介绍的内容还太少,为了不增加大家的理解成本,就不多说了)
升级为新的 App Router 后,目录结构变成了这样:
src/
└── app
├── page.js
├── layout.js
├── template.js
├── loading.js
├── error.js
└── not-found.js
├── about
│ └── page.js
└── more
└── page.js
使用了新的模式后,你就会发现app
多了很多文件,这些文件的名字都不能胡乱起,而是 Nest.js 约定的一些特殊文件
从这些文件中你也可以了解文件的实现功能:
page.js
:页面文件,用于渲染页面layout.js
:布局文件,用于定义页面的布局template.js
:模板文件,用于定义页面的模板loading.js
:加载文件,用于定义页面加载时的状态error.js
:错误文件,用于定义页面出错时的状态not-found.js
:404 文件,用于定义页面找不到时的状态
这些文件都是可选的,你可以根据自己的需求来选择使用哪些文件。
使用 App Router
定义路由(Routes)
现在让我们正式开始学习 App Router 吧!
首先是定义路由,文件夹是用来定义路由的.每个文件夹都代表一个对应到 URL 片段的路由片段.
创建嵌套的路由,仅仅需要创建嵌套的文件夹.举个例子如下图:
app/dashboard/settings
目录对应的路由地址就是 /dashboard/settings
定义页面(Pages)
那如何保证这个路由可以被访问呢?你需要创建一个特殊的名为 page.js
的文件。至于为什么叫 page.js
呢?除了 page
有“页面”这个含义之外,你可以理解为这是一种约定或者规范。
在上图的例子中:
app/index.js
对应的路由地址就是/
app/dashboard/page.js
对应的路由地址就是/dashboard
app/dashboard/settings/page.js
对应的路由地址就是/dashboard/settings
analytics
目录下因为没有page.js
文件,所以没有对应的路由。这个文件可以被用于存放组件、样式表、图片或者其他文件。
注意
不仅仅是.js
文件 Next.js 默认是支持 React,TS 所以.js
,.jsx
,.ts
,.tsx
都是支持的
让我们在page.js
定义一个最简单的页面吧:
// app/page.js
export default function Page() {
return (
<div>
<h1>渲染结果--{process.env.NEXT_PUBLIC_API}</h1>
</div>
);
}
那么我们访问 http://localhost:3000
效果如下
定义布局(Layouts)
布局是指多个页面共享的 UI。在导航的时候,布局会保留状态
、保持可交互性并且不会重新渲染,比如用来实现后台管理系统的侧边导航栏
。
定义一个布局,你需要新建一个名为 layout.js
的文件,该文件默认导出一个 React
组件,该组件应接收一个 children prop
,chidren
表示子布局(如果有的话)或者子页面。
举个例子:
我们新建目录和文件如下图所示
相关代码如下:
// app/dashboard/layout.js
export default function DashboardLayout({ children }) {
return (
<section>
<nav>nav</nav>
{children}
</section>
);
}
- page.js
// app/dashboard/page.js
export default function Page() {
return <h1>Hello, Dashboard!</h1>;
}
当访问 /dashboard
的时候,效果如下:
其中nav
来自 app/dashboard/layout.js
其中Hello, Dashboard!
来自 app/dashboard/page.js
- 注意
注意
同一个文件夹下 如果有layout.js
和page.js
page 会作为 children 参数传入 layout.
换句话说 layouot 会包裹同层级的 page
app/dashboard/settings/page.js
代码如下:
其中,nav
来自于 app/dashboard/layout.js
,Hello, Settings!
来自于 app/dashboard/settings/page.js
注意
你可以发现:布局是支持嵌套的,app/dashboard/settings/page.js
会使用 app/layout.js
和 app/dashboard/layout.js
两个布局中的内容,不过因为我们没有在 app/layout.js
写入可以展示的内容,所以图中没有体现出来。
根布局(Root Layout)
布局支持嵌套,最顶层的布局我们称之为根布局(Root Layout),也就是 app/layout.js
。它会应用于所有的路由。除此之外,这个布局还有点特殊。
- 默认布局如下:
// app/layout.js
import "./globals.css";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
- 注意
注意
app
目录必须包含根布局,也就是app/layout.js
这个文件是必需的。根布局必须包含 html 和 body 标签
你可以使用路由组创建多个根布局
默认根布局是服务端组件,且不能设置为客户端组件。
定义模板(Templates)
模板类似于布局,它也会传入每个子布局或者页面。但不会像布局那样维持状态
。
模板在路由切换时会为每一个 children 创建一个实例。这就意味着当用户在共享一个模板的路由间跳转的时候,将会重新挂载组件实例,重新创建 DOM 元素,不保留状态。这听起来有点抽象,没有关系,我们先看看模板的写法,再写个 demo 你就明白了。
定义一个模板,你需要新建一个名为 template.js
的文件,该文件默认导出一个 React 组件,该组件接收一个 children prop
。我们写个示例代码。
在 app 目录下新建一个 template.js
文件:
template.js
代码如下:
// app/template.js
export default function Template({ children }) {
return <div>{children}</div>;
}
你会发现,这用法跟布局一模一样。它们最大的区别就是状态的保持。如果同一目录下既有 template.js
也有 layout.js
,最后的输出效果如下:
<Layout>
{/* 模板需要给一个唯一的 key */}
<template key="{routeParam}">{children}</template>
</Layout>
也就是说layout
会包裹template.js
,然后template.js
会包裹page.js
某些情况下,模板会比布局更适合:
注意
依赖于 useEffect 和 useState 的功能,比如记录页面访问数(维持状态就不会在路由切换时记录访问数了)、用户反馈表单(每次重新填写)等
更改框架的默认行为,举个例子,布局内的
Suspense
只会在布局加载的时候展示一次fallback UI
,当切换页面的时候不会展示。但是使用模板,fallback
会在每次路由切换的时候展示
布局 VS 模板
简单的来说,如果你需要在导航(路由切换)的时候做一些事情如发送统计代码、重新加载、添加动画效果等等,那就可以考虑使用 template.js。
但要是后台系统左侧导航栏这种,需要保持状态,那就使用 layout.js。
定义加载界面(Loading UI)
现在我们已经了解了 page.js
、layout.js
、template.js
的功能,然而特殊文件还不止这些。App Router 提供了用于展示加载界面的 loading.js
。
- 首先在根目录创建一个
loading.js
文件,然后里面写代码
function LoadingPage() {
return (
<>
<div>页面加载中</div>
</>
);
}
export default LoadingPage;
page.js
页面模拟异步加载等待
export async function getData() {
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve("hello");
}, 1000);
});
return {
message: "Hello,Nest",
};
}
export default async function Home() {
const { message } = await getData();
return (
<div>
<h1>渲染结果--{process.env.NEXT_PUBLIC_API}</h1>
<div>{message}</div>
</div>
);
}
- 最后结果如下图所示:
- 注意
一般情况网站就一个 loading 页面 如果要是想针对不同的路由 使用不同的 loading 效果,那在各自的文件夹下面写 loading.js 即可
- 同一文件夹下面
page.js
,loading.js
,error.js
,layout.js
,template.js
的结构如下:
定义错误
文件名称:error.js
或者 global-error.js
顾名思义,用来创建发生错误时候展示的 UI
- 注意
注意
global-error.js 是全局错误页面,如果需要针对不同的路由使用不同的错误页面,那就在各自的文件夹下面新建 error.js
global-error.js 和 error.js 都必须使用
use client
global-error.js 中要定义
html
和body
标签,error.js 中不需要定义
简单来说 就是给 page.js 和 children 包裹了一层ErrorBoundary
我们写一个 demo 演示下error.js
.dashboard
目录下新建一个error.js
,目录如下
dashboard/error.js
代码如下:
"use client"; // 错误组件必须是客户端组件
// dashboard/error.js
import { useEffect } from "react";
export default function Error({ error, reset }) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// 尝试恢复
() => reset()
}
>
Try again - {error.toString()}
</button>
</div>
);
}
为触发 Error 错误,同级 page.js 的代码如下:
"use client";
// dashboard/page.js
import React from "react";
export default function Page() {
const [error, setError] = React.useState(false);
const handleGetError = () => {
setError(true);
};
return (
<>{error ? Error() : <button onClick={handleGetError}>Get Error</button>}</>
);
}
效果如下:
有时错误是暂时的,只需要重试就可以解决问题。所以 Next.js 会在 error.js 导出的组件中,传入 reset 函数,帮助尝试从错误中恢复。该函数会触发重新渲染错误边界里的内容。如果成功,会替换展示重新渲染的内容。
- 层级问题如下:
从这张图里你会发现一个问题:因为 Layout 和 Template 在 ErrorBoundary 外面,这说明错误边界不能捕获同级的 layout.js 或者 template.js 中的错误。如果你想捕获特定布局或者模板中的错误,那就需要在父级的 error.js 里进行捕获。
根目录错误页面
global-error.js
中定义html
,body
标签
注意
global-error.js 要是想生效 只有在生产环境(production)下生效,在开发环境(development)下要使用error.js
先 build 在 start
"use client";
// app/global-error.js
export default function GlobalError({ error, reset }) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
);
}
定义 404 页面(只能全局)
- 最后再讲一个特殊文件 ——
not-found.js
。顾名思义,当该路由不存在的时候展示的内容。
新建 app/not-found.js
,代码示例如下:
import Link from "next/link";
export default function NotFound() {
return (
<div>
<h2>Not Found</h2>
<p>Could not find requested resource</p>
<Link href="/">Return Home</Link>
</div>
);
}
- 效果
- 注意:
关于 app/not-found.js
一定要说明一点的是,它只能由两种情况触发:
注意
当组件抛出了 notFound 函数的时候
notFound()
当路由地址不匹配的时候
- 实际运用中
对应到实际开发,当我们请求一个用户的数据时或是请求一篇文章的数据时,如果该数据不存在,就可以直接丢出 notFound 函数,渲染自定义的 not-found.js 界面。示例代码如下:
// app/dashboard/blog/[id]/page.js
import { notFound } from "next/navigation";
async function fetchUser(id) {
const res = await fetch("https://...");
if (!res.ok) return undefined;
return res.json();
}
export default async function Profile({ params }) {
const user = await fetchUser(params.id);
if (!user) {
notFound();
}
// ...
}
总结
这一节我们重点讲解了 Next.js 基于文件系统的路由解决方案 App Router,介绍了用于定义页面的 page.js
、定义布局的 layout.js
、定义模板的 template.js
、定义加载界面的 loading.js
、定义错误处理的 error.js
、定义 404 页面的 not-found.js
。现在你再看 App Router 的这个目录结构:
src/
└── app
├── page.js
├── layout.js
├── template.js
├── loading.js
├── error.js(测试环境)
├── global-error.js(生产环境)
└── not-found.js
├── about
│ └── page.js
└── more
└── page.js