Skip to content

App Router

前言

路由(Router)是 Next.js 应用的重要组成部分。在 Next.js 中,路由决定了一个页面如何渲染或者一个请求该如何返回。

Next.js 有两套路由解决方案,之前的方案称之为Pages Router,目前的方案称之为App Router,两套方案目前是兼容的,都可以在 Next.js 中使用。

但是从13.4App 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 的区别:

以前我们的目录结构类似

ts
└── pages
    ├── index.js
    ├── about.js
    └── more.js

这种方式有一个弊端,那就是 pages 目录的所有 js 文件都会被当成路由文件,这就导致比如组件不能写在 pages 目录下,这就不符合开发者的使用习惯。(当然 Pages Router 还有很多其他的问题,只不过目前我们介绍的内容还太少,为了不增加大家的理解成本,就不多说了)

升级为新的 App Router 后,目录结构变成了这样:

ts
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定义一个最简单的页面吧:

ts
// 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 propchidren 表示子布局(如果有的话)或者子页面。

举个例子:

我们新建目录和文件如下图所示

相关代码如下:

ts
// app/dashboard/layout.js
export default function DashboardLayout({ children }) {
  return (
    <section>
      <nav>nav</nav>
      {children}
    </section>
  );
}
  • page.js
ts
// 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.jspage.js page 会作为 children 参数传入 layout.

换句话说 layouot 会包裹同层级的 page

app/dashboard/settings/page.js 代码如下:

其中,nav 来自于 app/dashboard/layout.jsHello, Settings! 来自于 app/dashboard/settings/page.js

注意

你可以发现:布局是支持嵌套的,app/dashboard/settings/page.js 会使用 app/layout.jsapp/dashboard/layout.js 两个布局中的内容,不过因为我们没有在 app/layout.js 写入可以展示的内容,所以图中没有体现出来。

根布局(Root Layout)

布局支持嵌套,最顶层的布局我们称之为根布局(Root Layout),也就是 app/layout.js。它会应用于所有的路由。除此之外,这个布局还有点特殊。

  • 默认布局如下:
ts
// 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>
  );
}
  • 注意

注意

  1. app 目录必须包含根布局,也就是 app/layout.js 这个文件是必需的。

  2. 根布局必须包含 html 和 body 标签

  3. 你可以使用路由组创建多个根布局

  4. 默认根布局是服务端组件,且不能设置为客户端组件。

定义模板(Templates)

模板类似于布局,它也会传入每个子布局或者页面。但不会像布局那样维持状态

模板在路由切换时会为每一个 children 创建一个实例。这就意味着当用户在共享一个模板的路由间跳转的时候,将会重新挂载组件实例,重新创建 DOM 元素,不保留状态。这听起来有点抽象,没有关系,我们先看看模板的写法,再写个 demo 你就明白了。

定义一个模板,你需要新建一个名为 template.js 的文件,该文件默认导出一个 React 组件,该组件接收一个 children prop。我们写个示例代码。

在 app 目录下新建一个 template.js 文件:

  • template.js 代码如下:
ts
// app/template.js
export default function Template({ children }) {
  return <div>{children}</div>;
}

你会发现,这用法跟布局一模一样。它们最大的区别就是状态的保持。如果同一目录下既有 template.js 也有 layout.js,最后的输出效果如下:

html
<Layout>
  {/* 模板需要给一个唯一的 key */}
  <template key="{routeParam}">{children}</template>
</Layout>

也就是说layout会包裹template.js,然后template.js会包裹page.js

某些情况下,模板会比布局更适合:

注意

  1. 依赖于 useEffect 和 useState 的功能,比如记录页面访问数(维持状态就不会在路由切换时记录访问数了)、用户反馈表单(每次重新填写)等

  2. 更改框架的默认行为,举个例子,布局内的 Suspense 只会在布局加载的时候展示一次 fallback UI,当切换页面的时候不会展示。但是使用模板,fallback 会在每次路由切换的时候展示

布局 VS 模板

简单的来说,如果你需要在导航(路由切换)的时候做一些事情如发送统计代码、重新加载、添加动画效果等等,那就可以考虑使用 template.js。

但要是后台系统左侧导航栏这种,需要保持状态,那就使用 layout.js。

定义加载界面(Loading UI)

现在我们已经了解了 page.jslayout.jstemplate.js 的功能,然而特殊文件还不止这些。App Router 提供了用于展示加载界面的 loading.js

  • 首先在根目录创建一个loading.js 文件,然后里面写代码
ts
function LoadingPage() {
  return (
    <>
      <div>页面加载中</div>
    </>
  );
}

export default LoadingPage;
  • page.js 页面模拟异步加载等待
ts
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

  • 注意

注意

  1. global-error.js 是全局错误页面,如果需要针对不同的路由使用不同的错误页面,那就在各自的文件夹下面新建 error.js

  2. global-error.js 和 error.js 都必须使用use client

  3. global-error.js 中要定义htmlbody标签,error.js 中不需要定义

简单来说 就是给 page.js 和 children 包裹了一层ErrorBoundary

我们写一个 demo 演示下error.js.dashboard目录下新建一个error.js,目录如下

  • dashboard/error.js代码如下:
ts
"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 的代码如下:

ts
"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

ts
"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,代码示例如下:

ts
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 一定要说明一点的是,它只能由两种情况触发:

注意

  1. 当组件抛出了 notFound 函数的时候 notFound()

  2. 当路由地址不匹配的时候

  • 实际运用中

对应到实际开发,当我们请求一个用户的数据时或是请求一篇文章的数据时,如果该数据不存在,就可以直接丢出 notFound 函数,渲染自定义的 not-found.js 界面。示例代码如下:

ts
// 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 的这个目录结构:

bash
src/
└── app
    ├── page.js
    ├── layout.js
    ├── template.js
    ├── loading.js
    ├── error.js(测试环境)
    ├── global-error.js(生产环境)
    └── not-found.js
    ├── about
   └── page.js
    └── more
        └── page.js