一篇文章搞定 Next Router

前言

你想做一名酷酷的黑客吗?你想成为一名独立产品的作者吗?那么它们用什么技术来构建它们的产品的呢?IndieHackerStacks 那么我推荐你看该网站。它详细的介绍了黑客门在构建它们的独立产品所用的技术。

当你打开了这个网站后,你会发现很多产品都用到了前端的主流技术如 Next.js、React、TailwindCSS等。这就说明了优秀的人会使用优秀的技术,构建它那优秀的产品。

正文

Next.js 有两套路由决绝方案:一个是 “Pages Router” ,另一个是 “App Router” 是现在 Next.js 路由解决方案。

本文将学习 App Router 下路由的顶用方式和常见的文件约定。

文件系统

Next.js 的路由基于文件系统。一个文件就是一个路由。当我们在 app 文件夹下创建一个 index.tsx 文件,那么 Next.js 会直接把他映射到路由的根地址。

使用 App Router

定义路由

定义路由,是使用文件夹来定义的。每一个文件夹名都代表着它的 URL 路径的片段名。

创建嵌套路由,只需要创建嵌套的文件夹。

举个例子,下图的 app/dashboard/settings目录对应的路由地址就是 /dashboard/settings

image.png

定义页面(Page)

在 Next.js 中只要你创建一个名为 page.tsx 文件那么它就是这个路由的页面。

image.png

定义布局(Layouts)

布局是指多个页面共享的 UI。在导航的时候,布局会保留状态、保持可交互性并且不会重新渲染,比如用来实现后台管理系统的侧边导航栏。

首先要使用布局文件,得先创建一个名为 layout.js 的文件,该文件默认导出一个 React 组件,并且它要接受一个 children prop, children 表示可能是子布局或子页面(没有子布局就是子页面)。

举例

1
2
3
4
5
6
7
8
// about/page.jsx
export default function Page() {
return (
<div id="content">
<p>你好,欢迎来到关于我的页面</p>
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// about/layout.jsx
export default function aboutLayout({ children }) {
return (
<>
<div id="nav">
<ul>
<li>
<a href="#">首页</a>
</li>
<li>
<a href="#">文章</a>
</li>
</ul>
</div>
{children}
</>
);
}

image-20240830185827219

当面访问 /about 页面后效果如上。

你可以发现:同一文件夹下如果有 layout.js 和 page.js,page 会作为 children 参数传入 layout。换句话说,layout 会包裹同层级的 page。

根布局(Root Layout)

根布局就是顶层的页面布局,也就是在 app/layout.jsx 的布局就是根布局。它会应用在所有路由上。

使用 create-next-app 默认创建的 layout.js 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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. 根布局必须包含 htmlbody标签,其他布局不能包含这些标签。如果你要更改这些标签,不推荐直接修改,参考《Metadata 篇》
  3. 你可以使用路由组创建多个根布局。
  4. 默认根布局是服务端组件,且不能设置为客户端组件。

定义模板(Templates)

它和布局差不多,但是它不会保持状态。

template.js 代码如下:

1
2
3
4
// app/template.js
export default function Template({ children }) {
return <div>{children}</div>
}

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

1
2
3
4
<Layout>
{/* 模板需要给一个唯一的 key */}
<Template key={routeParam}>{children}</Template>
</Layout>

也就是说 layout 会包裹 templatetemplate 又会包裹 page

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

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

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

定义加载界面(Loading UI)

在组件的切换过程中,经常会有另一个组件还没来得及渲染到页面,导致留白的样子。这是绝对不利于用户体验的所以我们要用到 App Router 提供用于展示加载的 loading.tsx

那么该如何写这个 loading.js吧。dashboard 目录下我们新建一个 loading.js。值得注意的事情是如果要定义全局通用的 loading 应该放在 app 根目录下,定义局部的则放在相当应的文件夹下即可。

1
2
3
4
// app/dashboard/loading.js
export default function DashboardLoading() {
return <>Loading dashboard...</>
}
1
2
3
4
5
6
7
8
9
10
11
// app/dashboard/page.js
async function getData() {
await new Promise((resolve) => setTimeout(resolve, 3000))
return {
message: 'Hello, Dashboard!',
}
}
export default async function DashboardPage(props) {
const { message } = await getData()
return <h1>{message}</h1>
}

11.gif

一个简单的 loading 组件就这么轻易的实现了。

对于这些特殊文件的层级问题,直接一张图搞定:

image.png

定义错误处理(Error Handling)

error.tsx 适用于在创建发生错误时展示的 UI。我也是没有写过这方面的经验,我把原文作所写的粘贴过来。

定义错误处理(Error Handling)

再讲讲特殊文件 error.js。顾名思义,用来创建发生错误时的展示 UI。

其实现借助了 React 的 Error Boundary 功能。简单来说,就是给 page.js 和 children 包了一层 ErrorBoundary

image.png

我们写一个 demo 演示一下 error.js 的效果。dashboard 目录下新建一个 error.js,目录效果如下:

image.png

dashboard/error.js代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'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
</button>
</div>
)
}

为触发 Error 错误,同级 page.js 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"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>}</>
);
}

效果如下:

13.gif

有时错误是暂时的,只需要重试就可以解决问题。所以 Next.js 会在 error.js 导出的组件中,传入 reset 函数,帮助尝试从错误中恢复。该函数会触发重新渲染错误边界里的内容。如果成功,会替换展示重新渲染的内容。

线上查看代码和效果:CodeSandbox Error

还记得上节讲过的层级问题吗?让我们回顾一下:

image.png

从这张图里你会发现一个问题:因为 LayoutTemplateErrorBoundary 外面,这说明错误边界不能捕获同级的 layout.js 或者 template.js 中的错误。如果你想捕获特定布局或者模板中的错误,那就需要在父级的 error.js 里进行捕获。

那问题来了,如果已经到了顶层,就比如根布局中的错误如何捕获呢?为了解决这个问题,Next.js 提供了 global-error.js文件,使用它时,需要将其放在 app 目录下。

global-error.js会包裹整个应用,而且当它触发的时候,它会替换掉根布局的内容。所以,global-error.js 中也要定义 <html><body> 标签。

global-error.js示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
'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>
)
}

注:global-error.js 用来处理根布局和根模板中的错误,app/error.js 建议还是要写的

定义 404 页面

not-found.js 文件用于定义当路由不匹配用户访问的路径的时候展示的内容。

总结

我们不得不惊叹于 Next.js 的强大,一个文件或文件就是路由,而且书写起来竟然那么的方便。像 React、Vue 我们还得专门的去 npm 下相关的 Router 库,然后再进行配置。

参考