龙母礼铺 發表於 2024-1-13 20:36:00

Next.js 开发指南 路由篇 | App Router

<h1 class="postTitle"><span>Next.js 开发指南 路由篇 | App Router</span></h1>
<div class="clear">&nbsp;</div>
<div class="postBody">
<div id="cnblogs_post_body" class="blogpost-body blogpost-body-html">
<h2 data-id="heading-0">前言</h2>
<p>路由(routers)是应用的重要组成部分。所谓路由,有多种定义,对于应用层的单页应用程序而言,路由是一个决定 URL 如何呈现的库,在服务层实现 API 时,路由是解析请求并将请求定向到处理程序的组件。简单的来说,在 Next.js 中,路由决定了一个页面如何渲染或者一个请求该如何返回。</p>
<p>Next.js 目前有两套路由解决方案,之前的方案称之为“Pages Router”,目前的方案称之为“App Router”,两套方案是兼容的,都可以在 Next.js 中使用。本篇我们会重点讲解 App Router,并学习 App Router 下路由的定义方式和常见的文件约定,学习完本篇,你将学会如何创建一个页面。</p>
<h2 data-id="heading-1">1. 文件系统(file-system)</h2>
<p>Next.js 的路由基于的是文件系统,也就是说,一个文件就可以是一个路由。举个例子,你在&nbsp;<code>app/pages</code>&nbsp;目录下创建一个&nbsp;<code>index.js</code>&nbsp;文件,它会直接映射到&nbsp;<code>/</code>&nbsp;路由地址:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-comment">// app/pages/index.js
<span class="hljs-keyword">import <span class="hljs-title class_">React <span class="hljs-keyword">from <span class="hljs-string">'react'
<span class="hljs-keyword">export <span class="hljs-keyword">default () =&gt; <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">h1&gt;Hello world<span class="hljs-tag">&lt;/<span class="hljs-name">h1&gt;
</span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>在&nbsp;<code>app/pages</code>&nbsp;目录下创建一个&nbsp;<code>about.js</code>&nbsp;文件,它会直接映射到&nbsp;<code>/about</code>&nbsp;路由地址:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-comment">// app/pages/about.js
<span class="hljs-keyword">import <span class="hljs-title class_">React <span class="hljs-keyword">from <span class="hljs-string">'react'
<span class="hljs-keyword">export <span class="hljs-keyword">default () =&gt; <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">h1&gt;About us<span class="hljs-tag">&lt;/<span class="hljs-name">h1&gt;
</span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<h2 data-id="heading-2">2. 从 Pages Router 到 App Router</h2>
<p>现在你打开使用&nbsp;<code>create-next-app</code>&nbsp;创建的项目,你会发现默认并没有&nbsp;<code>pages</code>&nbsp;这个目录。查看&nbsp;<code>packages.json</code>中的 Next.js 版本,如果版本号大于&nbsp;<code>13.4</code>,那就对了!</p>
<p>Next.js 从 v13 起就使用了新的路由模式 —— App Router。之前的路由模式我们称之为“Pages Router”,为保持渐进式更新,依然存在。从 v13.4 起,App Router 正式进入稳定化阶段,App Router 功能更强、性能更好、代码组织更灵活,以后就让我们使用新的路由模式吧!</p>
<p>可是这俩到底有啥区别呢?Next.js 又为什么升级到 App Router 呢?知其然知其所以然,让我们简单追溯一下。以前我们声明一个路由,只用在&nbsp;<code>pages</code>&nbsp;目录下创建一个文件就可以了,以前的目录结构类似于:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs">└── pages
    ├── index.<span class="hljs-property">js
    ├── about.<span class="hljs-property">js
    └── more.<span class="hljs-property">js
</span></span></span></code></pre>
<p>这种方式有一个弊端,那就是&nbsp;<code>pages</code>&nbsp;目录的所有 js 文件都会被当成路由文件,这就导致比如组件不能写在&nbsp;<code>pages</code>&nbsp;目录下,这就不符合开发者的使用习惯。(当然 Pages Router 还有很多其他的问题,只不过目前我们介绍的内容还太少,为了不增加大家的理解成本,就不多说了)</p>
<p>升级为新的 App Router 后,现在的目录结构类似于:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs">src/
└── app
    ├── page.<span class="hljs-property">js
    ├── layout.<span class="hljs-property">js
    ├── template.<span class="hljs-property">js
    ├── loading.<span class="hljs-property">js
    ├── error.<span class="hljs-property">js
    └── not-found.<span class="hljs-property">js
    ├── about
    │   └── page.<span class="hljs-property">js
    └── more
      └── page.<span class="hljs-property">js
</span></span></span></span></span></span></span></span></code></pre>
<p>使用新的模式后,你会发现&nbsp;<code>app</code>&nbsp;下多了很多文件。这些文件的名字并不是我乱起的,而是 Next.js 约定的一些特殊文件。从这些文件的名称中你也可以了解文件实现的功能,比如布局(layout.js)、模板(template.js)、加载状态(loading.js)、错误处理(error.js)、404(not-found.js)等。</p>
<p>简单的来说,App Router 制定了更加完善的规范,使代码更好被组织和管理。至于这些文件具体的功能和介绍,不要着急,本篇我们会慢慢展开。</p>
<h2 data-id="heading-3">3. 使用 Pages Router</h2>
<p>当然你也可以继续使用 Pages Router,如果你想使用 Pages Router,只需要在&nbsp;<code>src</code>&nbsp;目录下创建一个&nbsp;<code>pages</code>&nbsp;文件夹或者在根目录下创建一个&nbsp;<code>pages</code>文件夹。其中的 JS 文件会被视为 Pages Router 进行处理。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6e3628b5a76b4bdc87b423b377f80946~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=1600&amp;h=444&amp;s=212184&amp;e=png&amp;b=141414" alt="image.png" class="medium-zoom-image"></p>
<p>但是要注意,虽然两者可以共存,但 App Router 的优先级要高于 Pages Router。而且如果两者解析为同一个 URL,会导致构建错误。</p>
<p>你在 Next.js 官方文档进行搜索的时候,左上角会有 App 和 Pages 选项,这对应的就是 App Router 和 Pages Router:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ab940655f6c14e428a72c91b1f727681~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=1382&amp;h=586&amp;s=71771&amp;e=png&amp;b=040404" alt="image.png" class="medium-zoom-image"></p>
<p>因为两种路由模式的使用方式有很大不同,所以搜索的时候注意选择正确的的路由模式。</p>
<h2 data-id="heading-4">4. 使用 App Router</h2>
<h3 data-id="heading-5">4.1. 定义路由</h3>
<p>现在让我们开始正式的学习 App Router 吧。</p>
<p>首先是定义路由,文件夹被用来定义路由。每个文件夹都代表一个对应到 URL 片段的路由片段。创建嵌套的路由,只需要创建嵌套的文件夹。举个例子,下图的&nbsp;<code>app/dashboard/settings</code>目录对应的路由地址就是&nbsp;<code>/dashboard/settings</code>:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c35a76b0027c4e9fb5bc0d5807f479f4~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=1600&amp;h=594&amp;s=339521&amp;e=png&amp;b=141414" alt="image.png" class="medium-zoom-image"></p>
<h3 data-id="heading-6">4.2. 定义页面(Pages)</h3>
<p>那如何保证这个路由可以被访问呢?你需要创建一个特殊的名为&nbsp;<code>page.js</code>&nbsp;的文件。至于为什么叫&nbsp;<code>page.js</code>呢?除了&nbsp;<code>page</code>&nbsp;有“页面”这个含义之外,你可以理解为这是一种约定或者规范。(如果你是 Next.js 的开发者,你也可以约定为&nbsp;<code>index.js</code>甚至&nbsp;<code>yayu.js</code>!)</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/40820ff4957244899288d7534bd4c525~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=1600&amp;h=687&amp;s=314397&amp;e=png&amp;b=171717" alt="image.png" class="medium-zoom-image"></p>
<p>在上图这个例子中:</p>
<ul>
<li><code>app/page.js</code>&nbsp;对应路由&nbsp;<code>/</code></li>
<li><code>app/dashboard/page.js</code>&nbsp;对应路由&nbsp;<code>/dashboard</code></li>
<li><code>app/dashboard/settings/page.js</code>&nbsp;对应路由<code>/dashboard/settings</code></li>
<li><code>analytics</code>&nbsp;目录下因为没有&nbsp;<code>page.js</code>&nbsp;文件,所以没有对应的路由。这个文件可以被用于存放组件、样式表、图片或者其他文件。</li>
</ul>
<p><strong>当然不止&nbsp;<code>.js</code>文件,Next.js 默认是支持 React、TypeScript 的,所以&nbsp;<code>.js</code>、<code>.jsx</code>、<code>.tsx</code>&nbsp;都是可以的。</strong></p>
<p>那这个&nbsp;<code>page.js</code>&nbsp;代码如何写呢?最常见的是展示 UI,比如:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-comment">// app/page.js
<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">Page(<span class="hljs-params">) {
<span class="hljs-keyword">return <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">h1&gt;Hello, Next.js!<span class="hljs-tag">&lt;/<span class="hljs-name">h1&gt;
}
</span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>访问&nbsp;<code>http://localhost:3000/</code>,效果如下:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/78d38b112da542488c81d5412fc407ab~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=556&amp;h=200&amp;s=19870&amp;e=png&amp;b=000000" alt="image.png" class="medium-zoom-image"></p>
<h3 data-id="heading-7">4.3. 定义布局(Layouts)</h3>
<p>布局是指多个页面共享的 UI。在导航的时候,布局会保留状态,保持可交互性并且不会重新渲染,比如用来实现后台管理系统的侧边导航栏。</p>
<p>定义一个布局,你需要新建一个名为&nbsp;<code>layout.js</code>的文件,该文件默认导出一个 React 组件,该组件应接收一个&nbsp;<code>children</code>&nbsp;prop,<code>chidren</code>&nbsp;表示子布局(如果有的话)或者子页面。</p>
<p>举个例子,我们新建目录和文件如下图所示:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5a7872449f6e4c6fb1808f518db7783f~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=1600&amp;h=606&amp;s=295670&amp;e=png&amp;b=151515" alt="image.png" class="medium-zoom-image"></p>
<p>相关代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-comment">// app/dashboard/layout.js
<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">DashboardLayout(<span class="hljs-params">{
children,
}) {
<span class="hljs-keyword">return (
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">section&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">nav&gt;nav<span class="hljs-tag">&lt;/<span class="hljs-name">nav&gt;
      {children}
    <span class="hljs-tag">&lt;/<span class="hljs-name">section&gt;
)
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-comment">// app/dashboard/page.js
<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">Page(<span class="hljs-params">) {
<span class="hljs-keyword">return <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">h1&gt;Hello, Dashboard!<span class="hljs-tag">&lt;/<span class="hljs-name">h1&gt;
}
</span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>当访问&nbsp;<code>/dashboard</code>的时候,效果如下:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/43c72c2017354f1e9c292c2bbb9aaa40~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=710&amp;h=268&amp;s=27102&amp;e=png&amp;b=000000" alt="image.png" class="medium-zoom-image"></p>
<p>其中,<code>nav</code>&nbsp;来自于&nbsp;<code>app/dashboard/layout.js</code>,<code>Hello, Dashboard!</code>&nbsp;来自于&nbsp;<code>app/dashboard/page.js</code></p>
<p><strong>你可以发现:同一文件夹下如果有 layout.js 和 page.js,page 会作为 children 参数传入 layout。换句话说,layout 会包裹同层级的 page。</strong></p>
<p><code>app/dashboard/settings/page.js</code>&nbsp;代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-comment">// app/dashboard/settings/page.js
<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">Page(<span class="hljs-params">) {
<span class="hljs-keyword">return <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">h1&gt;Hello, Settings!<span class="hljs-tag">&lt;/<span class="hljs-name">h1&gt;
}
</span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>当访问&nbsp;<code>/dashboard/settings</code>的时候,效果如下:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/53456de28a684fe3902eb2ce5f4c07a0~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=808&amp;h=266&amp;s=29753&amp;e=png&amp;b=000000" alt="image.png" class="medium-zoom-image"></p>
<p>其中,<code>nav</code>&nbsp;来自于&nbsp;<code>app/dashboard/layout.js</code>,<code>Hello, Settings!</code>&nbsp;来自于&nbsp;<code>app/dashboard/settings/page.js</code></p>
<p><strong>你可以发现:布局是支持嵌套的</strong>,<code>app/dashboard/settings/page.js</code>&nbsp;会使用&nbsp;<code>app/layout.js</code>&nbsp;和&nbsp;<code>app/dashboard/layout.js</code>&nbsp;两个布局中的内容,不过因为我们没有在&nbsp;<code>app/layout.js</code>&nbsp;写入可以展示的内容,所以图中没有体现出来。</p>
<h4 data-id="heading-8">根布局(Root Layout)</h4>
<p>布局支持嵌套,最顶层的布局我们称之为根布局(Root Layout),也就是&nbsp;<code>app/layout.js</code>。它会应用于所有的路由。除此之外,这个布局还有点特殊。</p>
<p>使用&nbsp;<code>create-next-app</code>&nbsp;默认创建的&nbsp;<code>layout.js</code>&nbsp;代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-comment">// app/layout.js
<span class="hljs-keyword">import <span class="hljs-string">'./globals.css'
<span class="hljs-keyword">import { <span class="hljs-title class_">Inter } <span class="hljs-keyword">from <span class="hljs-string">'next/font/google'

<span class="hljs-keyword">const inter = <span class="hljs-title class_">Inter({ <span class="hljs-attr">subsets: [<span class="hljs-string">'latin'] })

<span class="hljs-keyword">export <span class="hljs-keyword">const metadata = {
<span class="hljs-attr">title: <span class="hljs-string">'Create Next App',
<span class="hljs-attr">description: <span class="hljs-string">'Generated by create next app',
}

<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">RootLayout(<span class="hljs-params">{ children }) {
<span class="hljs-keyword">return (
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">html <span class="hljs-attr">lang=<span class="hljs-string">"en"&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">body <span class="hljs-attr">className=<span class="hljs-string">{inter.className}&gt;{children}<span class="hljs-tag">&lt;/<span class="hljs-name">body&gt;
    <span class="hljs-tag">&lt;/<span class="hljs-name">html&gt;
)
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>其中:</p>
<ol>
<li><code>app</code>&nbsp;目录必须包含根布局,也就是&nbsp;<code>app/layout.js</code>&nbsp;这个文件是必需的。</li>
<li>根布局必须包含&nbsp;<code>html</code>&nbsp;和&nbsp;<code>body</code>标签,其他布局不能包含这些标签。但如果你要更改这些标签,不推荐直接修改,Next.js 提供内置工具帮助你管理诸如&nbsp;<code>&lt;title /&gt;</code>&nbsp;这样的 HTML 元素。</li>
<li>你可以使用路由组创建多个根布局。</li>
<li>默认根布局是服务端组件,且不能设置为客户端组件。</li>
</ol>
<h3 data-id="heading-9">4.4. 定义模板(Templates)</h3>
<p>模板类似于布局,它也会传入每个子布局或者页面。但不会像布局那样维持状态。</p>
<p>模板在路由切换时会为每一个 children 创建一个实例。这就意味着当用户在共享一个模板的路由间跳转的时候,将会重新挂载组件实例,重新创建 DOM 元素,不保留状态。这听起来有点抽象,没有关系,我们先看看模板的写法,再写个 demo 你就明白了。</p>
<p>定义一个模板,你需要新建一个名为&nbsp;<code>template.js</code>&nbsp;的文件,该文件默认导出一个 React 组件,该组件接收一个&nbsp;<code>children</code>&nbsp;prop。我们写个示例代码。在&nbsp;<code>app</code>目录下新建一个&nbsp;<code>template.js</code>文件。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e19139c038fe4c528f89874541928670~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=1600&amp;h=444&amp;s=216678&amp;e=png&amp;b=151515" alt="image.png" class="medium-zoom-image"></p>
<p><code>template.js</code>&nbsp;代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-comment">// app/template.js
<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">Template(<span class="hljs-params">{ children }) {
<span class="hljs-keyword">return <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div&gt;{children}<span class="hljs-tag">&lt;/<span class="hljs-name">div&gt;
}
</span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>你会发现,这用法跟布局一模一样。它们最大的区别就是状态的保持。如果同一目录下既有&nbsp;<code>template.js</code>&nbsp;也有&nbsp;<code>layout.js</code>,最后的输出效果如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs">&lt;<span class="hljs-title class_">Layout&gt;
{<span class="hljs-comment">/* 模板需要给一个唯一的 key */}
&lt;<span class="hljs-title class_">Template key={routeParam}&gt;{children}&lt;/<span class="hljs-title class_">Template&gt;
&lt;/<span class="hljs-title class_">Layout&gt;
</span></span></span></span></span></code></pre>
<p>也就是说&nbsp;<code>layout</code>&nbsp;会包裹&nbsp;<code>template</code>,<code>template</code>&nbsp;又会包裹&nbsp;<code>page</code>。</p>
<p>某些情况下,模板会比布局更适合:</p>
<ul>
<li>
<p>依赖于 useEffect 和 useState 的功能,比如记录页面访问数(维持状态就不会在路由切换时记录访问数了)、用户反馈表单(每次重新填写)等</p>
</li>
<li>
<p>更改框架的默认行为,举个例子,布局内的 Suspense 只会在布局加载的时候展示一次 fallback UI,当切换页面的时候不会展示。但是使用模板,fallback 会在每次路由切换的时候展示。</p>
</li>
</ul>
<h4 data-id="heading-10">布局 VS 模板</h4>
<p>为了帮助大家更好的理解布局和模板,我们写一个 demo,展示下两者的特性。</p>
<p>项目目录如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs">app
└─ dashboard
   ├─ layout.<span class="hljs-property">js
   ├─ page.<span class="hljs-property">js
   ├─ template.<span class="hljs-property">js
   ├─ about
   │└─ page.<span class="hljs-property">js
   └─ settings
      └─ page.<span class="hljs-property">js
</span></span></span></span></span></code></pre>
<p>其中&nbsp;<code>dashboard/layout.js</code>&nbsp;代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-string">'use client'

<span class="hljs-keyword">import { useState } <span class="hljs-keyword">from <span class="hljs-string">'react'
<span class="hljs-keyword">import <span class="hljs-title class_">Link <span class="hljs-keyword">from <span class="hljs-string">'next/link'

<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">Layout(<span class="hljs-params">{ children }) {
<span class="hljs-keyword">const = <span class="hljs-title function_">useState(<span class="hljs-number">0)
<span class="hljs-keyword">return (
    <span class="language-xml"><span class="hljs-tag">&lt;&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">div&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">Link <span class="hljs-attr">href=<span class="hljs-string">"/dashboard/about"&gt;About<span class="hljs-tag">&lt;/<span class="hljs-name">Link&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">br/&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">Link <span class="hljs-attr">href=<span class="hljs-string">"/dashboard/settings"&gt;Settings<span class="hljs-tag">&lt;/<span class="hljs-name">Link&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">div&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">h1&gt;Layout {count}<span class="hljs-tag">&lt;/<span class="hljs-name">h1&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">button <span class="hljs-attr">onClick=<span class="hljs-string">{() =&gt; setCount(count + 1)}&gt;
      Increment
      <span class="hljs-tag">&lt;/<span class="hljs-name">button&gt;
      {children}
    <span class="hljs-tag">&lt;/&gt;
)
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p><code>dashboard/template.js</code>&nbsp;代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-string">'use client'

<span class="hljs-keyword">import { useState } <span class="hljs-keyword">from <span class="hljs-string">'react'

<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">Template(<span class="hljs-params">{ children }) {
<span class="hljs-keyword">const = <span class="hljs-title function_">useState(<span class="hljs-number">0)
<span class="hljs-keyword">return (
    <span class="language-xml"><span class="hljs-tag">&lt;&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">h1&gt;Template {count}<span class="hljs-tag">&lt;/<span class="hljs-name">h1&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">button <span class="hljs-attr">onClick=<span class="hljs-string">{() =&gt; setCount(count + 1)}&gt;
      Increment
      <span class="hljs-tag">&lt;/<span class="hljs-name">button&gt;
      {children}
    <span class="hljs-tag">&lt;/&gt;
)
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p><code>dashboard/page.js</code>代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">Page(<span class="hljs-params">) {
<span class="hljs-keyword">return <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">h1&gt;Hello, Dashboard!<span class="hljs-tag">&lt;/<span class="hljs-name">h1&gt;
}
</span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p><code>dashboard/about/page.js</code>代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">Page(<span class="hljs-params">) {
<span class="hljs-keyword">return <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">h1&gt;Hello, About!<span class="hljs-tag">&lt;/<span class="hljs-name">h1&gt;
}
</span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p><code>dashboard/settings/page.js</code>代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">Page(<span class="hljs-params">) {
<span class="hljs-keyword">return <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">h1&gt;Hello, Settings!<span class="hljs-tag">&lt;/<span class="hljs-name">h1&gt;
}
</span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>最终展示效果如下(为了方便区分,做了部分样式处理):</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5d33ef3073ed46ce9f234880630246dd~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=2558&amp;h=1624&amp;s=480097&amp;e=png&amp;b=000000" alt="image.png" class="medium-zoom-image"></p>
<p>现在点击两个&nbsp;<code>Increment</code>&nbsp;按钮,会开始计数。随便点击下数字,然后再点击&nbsp;<code>About</code>或者&nbsp;<code>Settings</code>切换路由,你会发现,Layout 后的数字没有发生变化,Template 后的数字重置为 0。这就是所谓的状态保持。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/461a47c030d64fc7890e35de58feb950~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=450&amp;h=342&amp;s=33835&amp;e=gif&amp;f=54&amp;b=010101" alt="10.gif" class="medium-zoom-image"></p>
<p>注:当然如果刷新页面,Layout 和 Template 后的数字肯定都重置为 0。</p>
<h3 data-id="heading-11">4.5. 定义加载界面(Loading UI)</h3>
<p>现在我们已经了解了&nbsp;<code>page.js</code>、<code>layout.js</code>、<code>template.js</code>的功能,然而特殊文件还不止这些。App Router 提供了用于展示加载界面的&nbsp;<code>loading.js</code>。</p>
<p>这个功能的实现借助了 React 的<code>Suspense</code>&nbsp;API。关于 Suspense 的用法,可以查看&nbsp;《React 之 Suspense》。它实现的效果就是当发生路由变化的时候,立刻展示 fallback UI,等加载完成后,展示数据。</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-jsx code-block-extension-codeShowNum highlighter-hljs language-javascript"><span class="hljs-comment">// 在 ProfilePage 组件处于加载阶段时显示 Spinner
&lt;<span class="hljs-title class_">Suspense fallback={<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">Spinner /&gt;}&gt;
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">ProfilePage /&gt;
&lt;/<span class="hljs-title class_">Suspense&gt;
</span></span></span></span></span></span></span></span></span></code></pre>
<p>初次接触 Suspense 这个概念的时候,往往会有一个疑惑,那就是——“在哪里控制关闭 fallback UI 的呢?”</p>
<p>哪怕在 React 官网中,对背后的实现逻辑并无过多提及。但其实实现的逻辑很简单,简单的来说,ProfilePage 会 throw 一个数据加载的 promise,Suspense 会捕获这个 promise,追加一个 then 函数,then 函数中实现替换 fallback UI 。当数据加载完毕,promise 进入 resolve 状态,then 函数执行,于是更新替换 fallback UI。</p>
<p>了解了原理,那我们来看看如何写这个&nbsp;<code>loading.js</code>吧。<code>dashboard</code>&nbsp;目录下我们新建一个&nbsp;<code>loading.js</code>。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a410face8c0443bda0bba48a3fa4a602~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=1600&amp;h=606&amp;s=292947&amp;e=png&amp;b=151515" alt="image.png" class="medium-zoom-image"></p>
<p><code>loading.js</code>的代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-comment">// app/dashboard/loading.js
<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">DashboardLoading(<span class="hljs-params">) {
<span class="hljs-keyword">return <span class="language-xml"><span class="hljs-tag">&lt;&gt;Loading dashboard...<span class="hljs-tag">&lt;/&gt;
}
</span></span></span></span></span></span></span></span></span></span></code></pre>
<p>同级的&nbsp;<code>page.js</code>&nbsp;代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-comment">// app/dashboard/page.js
<span class="hljs-keyword">async <span class="hljs-keyword">function <span class="hljs-title function_">getData(<span class="hljs-params">) {
<span class="hljs-keyword">await <span class="hljs-keyword">new <span class="hljs-title class_">Promise(<span class="hljs-function">(<span class="hljs-params">resolve) =&gt; <span class="hljs-built_in">setTimeout(resolve, <span class="hljs-number">3000))
<span class="hljs-keyword">return {
    <span class="hljs-attr">message: <span class="hljs-string">'Hello, Dashboard!',
}
}
<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">async <span class="hljs-keyword">function <span class="hljs-title function_">DashboardPage(<span class="hljs-params">props) {
<span class="hljs-keyword">const { message } = <span class="hljs-keyword">await <span class="hljs-title function_">getData()
<span class="hljs-keyword">return <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">h1&gt;{message}<span class="hljs-tag">&lt;/<span class="hljs-name">h1&gt;
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>不再需要其他的代码,loading 的效果就实现了:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6cd31cc361fb418f9657597e6916cc59~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=450&amp;h=342&amp;s=7964&amp;e=gif&amp;f=9&amp;b=000000" alt="11.gif" class="medium-zoom-image"></p>
<p>就是这么简单。其关键在于&nbsp;<code>page.js</code>导出了一个 async 函数。</p>
<p><code>loading.js</code>&nbsp;的实现原理是将&nbsp;<code>page.js</code>和下面的 children 用&nbsp;<code>&lt;Suspense&gt;</code>&nbsp;包裹。因为<code>page.js</code>导出一个 async 函数,Suspense 得以捕获数据加载的 promise,借此实现了 loading 组件的关闭。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/804284470c16423eb3d3d2d4510996ce~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=1600&amp;h=766&amp;s=442672&amp;e=png&amp;b=191919" alt="image.png" class="medium-zoom-image"></p>
<p>当然实现 loading 效果,不一定非导出一个 async 函数。也可以借助 React 的&nbsp;<code>use</code>&nbsp;函数。现在我们在&nbsp;<code>dashboard</code>下新建一个&nbsp;<code>about</code>目录,在其中新建&nbsp;<code>page.js</code>文件。</p>
<p><code>/dashboard/about/page.js</code>&nbsp;代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-comment">// /dashboard/about/page.js
<span class="hljs-keyword">import { use } <span class="hljs-keyword">from <span class="hljs-string">'react'

<span class="hljs-keyword">async <span class="hljs-keyword">function <span class="hljs-title function_">getData(<span class="hljs-params">) {
<span class="hljs-keyword">await <span class="hljs-keyword">new <span class="hljs-title class_">Promise(<span class="hljs-function">(<span class="hljs-params">resolve) =&gt; <span class="hljs-built_in">setTimeout(resolve, <span class="hljs-number">5000))
<span class="hljs-keyword">return {
    <span class="hljs-attr">message: <span class="hljs-string">'Hello, About!',
}
}

<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">Page(<span class="hljs-params">) {
<span class="hljs-keyword">const {message} = <span class="hljs-title function_">use(<span class="hljs-title function_">getData())
<span class="hljs-keyword">return <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">h1&gt;{message}<span class="hljs-tag">&lt;/<span class="hljs-name">h1&gt;
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>同样实现了 loading 效果:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa3f3e67b3e348348c03a6492e4581f7~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=450&amp;h=342&amp;s=6554&amp;e=gif&amp;f=6&amp;b=000000" alt="12.gif" class="medium-zoom-image"></p>
<p>如果你想针对&nbsp;<code>/dashboard/about</code>&nbsp;单独实现一个 loading 效果,那就在&nbsp;<code>about</code>&nbsp;目录下再写一个&nbsp;<code>loading.js</code>&nbsp;即可。</p>
<p>如果同一文件夹既有&nbsp;<code>layout.js</code>&nbsp;又有&nbsp;<code>template.js</code>&nbsp;又有&nbsp;<code>loading.js</code>&nbsp;,那它们的层级关系是怎样呢?</p>
<p>对于这些特殊文件的层级问题,直接一张图搞定:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a0551d59d32b486e8f869e0e6ca8f157~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=1600&amp;h=641&amp;s=327102&amp;e=png&amp;b=1c1c1c" alt="image.png" class="medium-zoom-image"></p>
<h3 data-id="heading-12">4.6. 定义错误处理(Error Handling)</h3>
<p>再讲讲特殊文件&nbsp;<code>error.js</code>。顾名思义,用来创建发生错误时的展示 UI。</p>
<p>其实现借助了 React 的&nbsp;Error Boundary&nbsp;功能。简单来说,就是给 page.js 和 children 包了一层&nbsp;<code>ErrorBoundary</code>。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b2005b09883440cdab2d9a2be0217883~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=1600&amp;h=901&amp;s=497446&amp;e=png&amp;b=1a1a1a" alt="image.png" class="medium-zoom-image"></p>
<p>我们写一个 demo 演示一下&nbsp;<code>error.js</code>&nbsp;的效果。<code>dashboard</code>&nbsp;目录下新建一个&nbsp;<code>error.js</code>,目录效果如下:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b16665406e384c35870c4aa68ea9875a~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=1600&amp;h=606&amp;s=293046&amp;e=png&amp;b=151515" alt="image.png" class="medium-zoom-image"></p>
<p><code>dashboard/error.js</code>代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-string">'use client' <span class="hljs-comment">// 错误组件必须是客户端组件
<span class="hljs-comment">// dashboard/error.js
<span class="hljs-keyword">import { useEffect } <span class="hljs-keyword">from <span class="hljs-string">'react'

<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">Error(<span class="hljs-params">{ error, reset }) {
<span class="hljs-title function_">useEffect(<span class="hljs-function">() =&gt; {
    <span class="hljs-variable language_">console.<span class="hljs-title function_">error(error)
}, )

<span class="hljs-keyword">return (
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">h2&gt;Something went wrong!<span class="hljs-tag">&lt;/<span class="hljs-name">h2&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">button
      <span class="hljs-attr">onClick=<span class="hljs-string">{
          // 尝试恢复
          () =&gt; reset()
      }
      &gt;
      Try again
      <span class="hljs-tag">&lt;/<span class="hljs-name">button&gt;
    <span class="hljs-tag">&lt;/<span class="hljs-name">div&gt;
)
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>为触发 Error 错误,同级&nbsp;<code>page.js</code>&nbsp;的代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-string">"use client";
<span class="hljs-comment">// dashboard/page.js
<span class="hljs-keyword">import <span class="hljs-title class_">React <span class="hljs-keyword">from <span class="hljs-string">"react";

<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">Page(<span class="hljs-params">) {
<span class="hljs-keyword">const = <span class="hljs-title class_">React.<span class="hljs-title function_">useState(<span class="hljs-literal">false);

<span class="hljs-keyword">const <span class="hljs-title function_">handleGetError = (<span class="hljs-params">) =&gt; {
    <span class="hljs-title function_">setError(<span class="hljs-literal">true);
};

<span class="hljs-keyword">return (
    <span class="language-xml"><span class="hljs-tag">&lt;&gt;{error ? Error() : <span class="hljs-tag">&lt;<span class="hljs-name">button <span class="hljs-attr">onClick=<span class="hljs-string">{handleGetError}&gt;Get Error<span class="hljs-tag">&lt;/<span class="hljs-name">button&gt;}<span class="hljs-tag">&lt;/&gt;
);
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>效果如下:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e09190375f63426fbe4ac89c5f8e246f~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=450&amp;h=342&amp;s=15591&amp;e=gif&amp;f=28&amp;b=000000" alt="13.gif" class="medium-zoom-image"></p>
<p>有时错误是暂时的,只需要重试就可以解决问题。所以 Next.js 会在&nbsp;<code>error.js</code>&nbsp;导出的组件中,传入&nbsp;<code>reset</code>&nbsp;函数,帮助尝试从错误中恢复。该函数会触发重新渲染错误边界里的内容。如果成功,会替换展示重新渲染的内容。</p>
<p>还记得上节讲过的层级问题吗?让我们回顾一下:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eeb2e4b635f0473785c0ba9d79df01b6~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=1600&amp;h=641&amp;s=327102&amp;e=png&amp;b=1c1c1c" alt="image.png" class="medium-zoom-image"></p>
<p>从这张图里你会发现一个问题,因为&nbsp;<code>Layout</code>&nbsp;和&nbsp;<code>Template</code>&nbsp;在&nbsp;<code>ErrorBoundary</code>&nbsp;外面,这说明错误边界不能捕获同级的&nbsp;<code>layout.js</code>&nbsp;或者&nbsp;<code>template.js</code>&nbsp;中的错误。如果你想捕获特定布局或者模板中的错误,那就在父级的&nbsp;<code>error.js</code>&nbsp;里进行捕获。</p>
<p>那问题来了,如果已经到了顶层,就比如根布局中的错误如何捕获呢?为了解决这个问题,Next.js 提供了&nbsp;<code>global-error.js</code>文件,使用它时,将其放在&nbsp;<code>app</code>&nbsp;目录下。</p>
<p><code>global-error.js</code>会包裹整个应用,而且当它触发的时候,它会替换掉根布局的内容。所以,<code>global-error.js</code>&nbsp;中也要定义&nbsp;<code>&lt;html&gt;</code>&nbsp;和&nbsp;<code>&lt;body&gt;</code>&nbsp;标签。</p>
<p><code>global-error.js</code>示例代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-string">'use client'
<span class="hljs-comment">// app/global-error.js
<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">GlobalError(<span class="hljs-params">{ error, reset }) {
<span class="hljs-keyword">return (
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">html&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">body&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">h2&gt;Something went wrong!<span class="hljs-tag">&lt;/<span class="hljs-name">h2&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">button <span class="hljs-attr">onClick=<span class="hljs-string">{() =&gt; reset()}&gt;Try again<span class="hljs-tag">&lt;/<span class="hljs-name">button&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">body&gt;
    <span class="hljs-tag">&lt;/<span class="hljs-name">html&gt;
)
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>注:<code>global-error.js</code>&nbsp;用来处理根布局和根模板中的错误,<code>app/error.js</code>&nbsp;建议还是要写的</p>
<h3 data-id="heading-13">4.7. 定义 404 页面</h3>
<p>最后再讲一个特殊文件 ——&nbsp;<code>not-found.js</code>。顾名思义,当该路由不存在的时候展示的内容。</p>
<p>Next.js 项目默认的 not-found 效果如下:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/92bd888fdae94703885dcec24825c2d6~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=872&amp;h=340&amp;s=18455&amp;e=png&amp;b=000000" alt="image.png" class="medium-zoom-image"></p>
<p>如果你要替换这个效果,只需要在&nbsp;<code>app</code>&nbsp;目录下新建一个&nbsp;<code>not-found.js</code>,代码示例如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-keyword">import <span class="hljs-title class_">Link <span class="hljs-keyword">from <span class="hljs-string">'next/link'

<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">NotFound(<span class="hljs-params">) {
<span class="hljs-keyword">return (
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">h2&gt;Not Found<span class="hljs-tag">&lt;/<span class="hljs-name">h2&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">p&gt;Could not find requested resource<span class="hljs-tag">&lt;/<span class="hljs-name">p&gt;
      <span class="hljs-tag">&lt;<span class="hljs-name">Link <span class="hljs-attr">href=<span class="hljs-string">"/"&gt;Return Home<span class="hljs-tag">&lt;/<span class="hljs-name">Link&gt;
    <span class="hljs-tag">&lt;/<span class="hljs-name">div&gt;
)
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>not-found 的效果就会更改为:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aff4d53c41c94d55b597f8d924f13187~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=806&amp;h=326&amp;s=37554&amp;e=png&amp;b=000000" alt="image.png" class="medium-zoom-image"></p>
<p>考虑到&nbsp;<code>layout.js</code>&nbsp;和&nbsp;<code>template.js</code>&nbsp;的使用效果,如果我把&nbsp;<code>not-found.js</code>添加到一个子目录里,比如&nbsp;<code>/dashboard/blog</code>下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-md code-block-extension-codeShowNum highlighter-hljs language-markdown">dashboard
└─ blog
   ├─ page.js
   └─ not-found.js
</code></pre>
<p>当访问&nbsp;<code>/dashboard/blog/yayu</code>&nbsp;(该路由没有声明)时,它的 not-found 效果会是自定义的吗?</p>
<p>答案是不会。访问&nbsp;<code>/dashboard/blog/yayu</code>&nbsp;依然会走到默认 not-found 路由效果。这是因为&nbsp;<code>not-found.js</code>&nbsp;被用于当&nbsp;<code>notFound</code>&nbsp;函数被抛出的时候。(<code>notFound</code>&nbsp;函数是&nbsp;<code>next/navigation</code>这个包提供的一个方法)</p>
<p>写个示例代码:</p>
<p><code>/dashboard/blog</code>&nbsp;下新建一个&nbsp;<code>page.js</code>和&nbsp;<code>not-found.js</code>。<code>page.js</code>&nbsp;代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-comment">// /dashboard/blog/page.js
<span class="hljs-keyword">import { notFound } <span class="hljs-keyword">from <span class="hljs-string">'next/navigation'

<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">Page(<span class="hljs-params">) {
<span class="hljs-title function_">notFound()
<span class="hljs-keyword">return <span class="language-xml"><span class="hljs-tag">&lt;&gt;<span class="hljs-tag">&lt;/&gt;
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p><code>not-found.js</code>&nbsp;代码则直接复制上面的&nbsp;<code>not-found.js</code>。</p>
<p>当访问&nbsp;<code>/dashboard/blog</code>时,因为&nbsp;<code>page.js</code>&nbsp;丢出了&nbsp;<code>notFound</code>&nbsp;函数,所以会触发&nbsp;<code>not-found.js</code>&nbsp;的执行。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f4dafc1a8c8d486d8c8c0e65ae9ede0d~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=812&amp;h=332&amp;s=38758&amp;e=png&amp;b=000000" alt="image.png" class="medium-zoom-image"></p>
<p>但当访问&nbsp;<code>/dashboard/blog/yayu</code>时,因为没有对应的路由处理程序,依然是默认的 not-found 效果</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/23dd0869b1224f2388b3240f19d80d59~tplv-k3u1fbpfcp-jj-mark:1512:0:0:0:q75.awebp#?w=874&amp;h=578&amp;s=38025&amp;e=png&amp;b=000000" alt="image.png" class="medium-zoom-image"></p>
<p>如果我们添加对应的处理程序,在&nbsp;<code>app/dashboard/blog/yayu/page.js</code>中也执行 notFound 函数,就会渲染&nbsp;<code>/dashboard/blog/not-found.js</code>的 UI 内容。</p>
<p>对应到实际开发,当我们请求一个用户的数据时或是请求一篇文章的数据时,如果该数据不存在,就可以直接丢出&nbsp;<code>notFound</code>&nbsp;函数,渲染自定义的<code>not-found</code>&nbsp;界面。一个示例代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-comment">// app/dashboard/blog//page.js
<span class="hljs-keyword">import { notFound } <span class="hljs-keyword">from <span class="hljs-string">'next/navigation'

<span class="hljs-keyword">async <span class="hljs-keyword">function <span class="hljs-title function_">fetchUser(<span class="hljs-params">id) {
<span class="hljs-keyword">const res = <span class="hljs-keyword">await <span class="hljs-title function_">fetch(<span class="hljs-string">'https://...')
<span class="hljs-keyword">if (!res.<span class="hljs-property">ok) <span class="hljs-keyword">return <span class="hljs-literal">undefined
<span class="hljs-keyword">return res.<span class="hljs-title function_">json()
}

<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">async <span class="hljs-keyword">function <span class="hljs-title function_">Profile(<span class="hljs-params">{ params }) {
<span class="hljs-keyword">const user = <span class="hljs-keyword">await <span class="hljs-title function_">fetchUser(params.<span class="hljs-property">id)

<span class="hljs-keyword">if (!user) {
    <span class="hljs-title function_">notFound()
}

<span class="hljs-comment">// ...
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<h2 data-id="heading-14">5. 链接和导航(Linking and Navigating)</h2>
<p>知道了如何定义路由,最后我们再讲讲如何在 Next.js 中实现链接和导航。Next.js 提供了两种方式:</p>
<ol>
<li>使用&nbsp;<code>&lt;Link&gt;</code>&nbsp;组件</li>
<li>使用&nbsp;<code>useRouter</code>&nbsp;Hook</li>
</ol>
<h3 data-id="heading-15">5.1.&nbsp;<code>&lt;Link&gt;</code></h3>
<p><code>&lt;Link&gt;</code>&nbsp;是一个拓展了 HTML&nbsp;<code>&lt;a&gt;</code>&nbsp;标签的内置组件,用来实现预获取(prefetching) 和客户端路由导航。这是 Next.js 中路由导航的主要方式。</p>
<h4 data-id="heading-16">5.1.1. 基础使用</h4>
<p>使用示例如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-keyword">import <span class="hljs-title class_">Link <span class="hljs-keyword">from <span class="hljs-string">'next/link'

<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">Page(<span class="hljs-params">) {
<span class="hljs-keyword">return <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">Link <span class="hljs-attr">href=<span class="hljs-string">"/dashboard"&gt;Dashboard<span class="hljs-tag">&lt;/<span class="hljs-name">Link&gt;
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>支持 hash 值用于实现跳转到页面的某个位置:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs">&lt;<span class="hljs-title class_">Link href=<span class="hljs-string">"/dashboard#settings"&gt;<span class="hljs-title class_">Settings&lt;/<span class="hljs-title class_">Link&gt;

<span class="hljs-comment">// Output
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">a <span class="hljs-attr">href=<span class="hljs-string">"/dashboard#settings"&gt;Settings<span class="hljs-tag">&lt;/<span class="hljs-name">a&gt;
</span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<h4 data-id="heading-17">5.1.2. 动态渲染链接</h4>
<p>支持路由链接动态渲染:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-keyword">import <span class="hljs-title class_">Link <span class="hljs-keyword">from <span class="hljs-string">'next/link'

<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">PostList(<span class="hljs-params">{ posts }) {
<span class="hljs-keyword">return (
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">ul&gt;
      {posts.map((post) =&gt; (
      <span class="hljs-tag">&lt;<span class="hljs-name">li <span class="hljs-attr">key=<span class="hljs-string">{post.id}&gt;
          <span class="hljs-tag">&lt;<span class="hljs-name">Link <span class="hljs-attr">href=<span class="hljs-string">{`/<span class="hljs-attr">blog/${<span class="hljs-attr">post.slug}`}&gt;{post.title}<span class="hljs-tag">&lt;/<span class="hljs-name">Link&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">li&gt;
      ))}
    <span class="hljs-tag">&lt;/<span class="hljs-name">ul&gt;
)
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<h4 data-id="heading-18">5.1.3. 获取当前路径名</h4>
<p>如果需要对当前链接进行判断,你可以使用&nbsp;usePathname()&nbsp;这个方法,它会读取当前 URL 的路径名(pathname),这是一段示例代码:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-string">'use client'

<span class="hljs-keyword">import { usePathname } <span class="hljs-keyword">from <span class="hljs-string">'next/navigation'
<span class="hljs-keyword">import <span class="hljs-title class_">Link <span class="hljs-keyword">from <span class="hljs-string">'next/link'

<span class="hljs-keyword">export <span class="hljs-keyword">function <span class="hljs-title function_">Navigation(<span class="hljs-params">{ navLinks }) {
<span class="hljs-keyword">const pathname = <span class="hljs-title function_">usePathname()

<span class="hljs-keyword">return (
    <span class="language-xml"><span class="hljs-tag">&lt;&gt;
      {navLinks.map((link) =&gt; {
      const isActive = pathname === link.href

      return (
          <span class="hljs-tag">&lt;<span class="hljs-name">Link
            <span class="hljs-attr">className=<span class="hljs-string">{isActive ? '<span class="hljs-attr">text-blue' <span class="hljs-attr">: '<span class="hljs-attr">text-black'}
            <span class="hljs-attr">href=<span class="hljs-string">{link.href}
            <span class="hljs-attr">key=<span class="hljs-string">{link.name}
          &gt;
            {link.name}
          <span class="hljs-tag">&lt;/<span class="hljs-name">Link&gt;
      )
      })}
    <span class="hljs-tag">&lt;/&gt;
)
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<h4 data-id="heading-19">5.1.4. 跳转行为设置</h4>
<p>App Router 的默认行为是滚动到新路由的顶部,或者在前进后退导航时维持之前的滚动距离。</p>
<p>如果你想要禁用这个行为,你可以给&nbsp;<code>&lt;Link&gt;</code>&nbsp;组件传递一个&nbsp;<code>scroll={false}</code>,或者在使用&nbsp;<code>router.push</code>和&nbsp;<code>router.replace</code>的时候,设置&nbsp;<code>scroll: false</code>。</p>
<p>示例代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-comment">// next/link
&lt;<span class="hljs-title class_">Link href=<span class="hljs-string">"/dashboard" scroll={<span class="hljs-literal">false}&gt;
<span class="hljs-title class_">Dashboard
&lt;/<span class="hljs-title class_">Link&gt;
</span></span></span></span></span></span></code></pre>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-comment">// useRouter
<span class="hljs-keyword">import { useRouter } <span class="hljs-keyword">from <span class="hljs-string">'next/navigation'

<span class="hljs-keyword">const router = <span class="hljs-title function_">useRouter()

router.<span class="hljs-title function_">push(<span class="hljs-string">'/dashboard', { <span class="hljs-attr">scroll: <span class="hljs-literal">false })
</span></span></span></span></span></span></span></span></span></span></code></pre>
<p>注:关于&nbsp;<code>&lt;Link&gt;</code>&nbsp;组件的用法,我们还会在《组件篇 | Link 和 Script》中详细介绍。</p>
<h3 data-id="heading-20">5.2. useRouter</h3>
<p>第二种方式是使用 useRouter。示例代码如下:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs"><span class="hljs-string">'use client'

<span class="hljs-keyword">import { useRouter } <span class="hljs-keyword">from <span class="hljs-string">'next/navigation'

<span class="hljs-keyword">export <span class="hljs-keyword">default <span class="hljs-keyword">function <span class="hljs-title function_">Page(<span class="hljs-params">) {
<span class="hljs-keyword">const router = <span class="hljs-title function_">useRouter()

<span class="hljs-keyword">return (
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">button <span class="hljs-attr">type=<span class="hljs-string">"button" <span class="hljs-attr">onClick=<span class="hljs-string">{() =&gt; router.push('/dashboard')}&gt;
      Dashboard
    <span class="hljs-tag">&lt;/<span class="hljs-name">button&gt;
)
}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>注意使用该 hook 需要在客户端组件中。(顶层的&nbsp;<code>'use client'</code>&nbsp;就是声明这是客户端组件)</p>
<p>注:关于 useRouter,我们还会在《API 篇 | 请求相关的常用函数与方法》&nbsp;篇中详细介绍。</p>
<h2 data-id="heading-21">小结</h2>
<p>恭喜你,完成了本节内容的学习!</p>
<p>这一节我们重点讲解了 Next.js 基于文件系统的路由解决方案 App Router,介绍了用于定义页面的<code>page.js</code>、定义布局的<code>layout.js</code>、定义模板的<code>template.js</code>、定义加载界面的<code>loading.js</code>、定义错误处理的<code>error.js</code>、定义 404 页面的<code>not-found.js</code>。现在你再看 App Router 的这个目录结构:</p>
<pre></pre>
<pre class="highlighter-hljs"><code class="hljs language-javascript code-block-extension-codeShowNum highlighter-hljs">src/
└── app
    ├── page.<span class="hljs-property">js
    ├── layout.<span class="hljs-property">js
    ├── template.<span class="hljs-property">js
    ├── loading.<span class="hljs-property">js
    ├── error.<span class="hljs-property">js
    └── not-found.<span class="hljs-property">js
    ├── about
    │   └── page.<span class="hljs-property">js
    └── more
      └── page.<span class="hljs-property">js
</span></span></span></span></span></span></span></span></code></pre>
<blockquote>
<p>简单的来说,App Router 制定了更加完善的规范,使代码更好被组织和管理。</p>
</blockquote>
<p>对此是不是有了更加深刻的理解呢?然而这还只有 Next.js 强大的路由功能的一小部分。下篇我们开始介绍 Next.js 的高级路由功能。</p>
<p>&nbsp;</p>
<p>知识星球【Next.js开发指南】(已更新至第33章)</p>
<ol>
<li>初始篇 | Next.js CLI</li>
<li>路由篇 | App Router</li>
<li>路由篇 | 动态路由、路由组、平行路由和拦截路由</li>
<li>路由篇 | 路由处理程序和中间件</li>
<li>路由篇 | 国际化</li>
<li>数据获取篇 | 数据获取、缓存与重新验证</li>
<li>数据获取篇 | Server Actions 与表单</li>
<li>渲染篇 | 从 CSR、SSR、SSG、ISR 开始说起</li>
<li>渲染篇 | 服务端组件和客户端组件</li>
<li>渲染篇 | Streaming 和 Edge Runtime</li>
<li>缓存篇 | Caching</li>
<li>样式篇 | Tailwind CSS、CSS-in-JS 与 Sass</li>
<li>组件篇 | Images</li>
<li>组件篇 | Font</li>
<li>组件篇 | Link 和 Script</li>
<li>优化篇 | 懒加载</li>
<li>配置篇 | TypeScript 和 ESLint</li>
<li>配置篇 | 环境变量、路径别名与 src 目录</li>
<li>配置篇 | MDX</li>
<li>配置篇 | 草稿模式和内容安全策略</li>
<li>配置篇 | 路由段配置项</li>
<li>部署篇 | 静态导出</li>
<li>Metadata 篇 | 基于配置</li>
<li>Metadata 篇 | 基于文件</li>
<li>API 篇 | next.config.js(上)</li>
<li>API 篇 | next.config.js(下)</li>
<li>API 篇 | 请求相关的常用函数与方法</li>
<li>API 篇 | 常用函数与方法</li>
<li>实战篇 | React Notes | 项目介绍与创建</li>
<li>实战篇 | React Notes | 侧边栏笔记列表</li>
<li>实战篇 | React Notes | 笔记预览界面</li>
<li>实战篇 | React Notes | 笔记编辑界面</li>
<li>实战篇 | React Notes | 笔记搜索</li>
<li>实战篇 | React Notes | 国际化</li>
<li>实战篇 | React Notes | Auth</li>
<li>实战篇 | React Notes | 文件上传</li>
<li>实战篇 | React Notes | 部署(一)</li>
<li>实战篇 | React Notes | 部署(二)</li>
<li>实战篇 | 博客 | 项目创建</li>
<li>实战篇 | 博客 | 博客后台</li>
<li>实战篇 | 博客 | MDX</li>
<li>实战篇 | 博客 | Server Actions</li>
<li>实战篇 | 博客 | 渲染原理</li>
<li>实战篇 | App | 需求分析</li>
<li>实战篇 | App | 数据库设计</li>
<li>实战篇 | App | 项目创建</li>
<li>实战篇 | App | 移动端处理</li>
<li>实战篇 | App | 接口开发</li>
<li>实战篇 | App | 数据请求</li>
<li>实战篇 | App | 构建部署</li>
<li>源码篇 | 源码架构</li>
<li>源码篇 | 调试代码</li>
<li>源码篇 | 路由实现</li>
<li>源码篇 | 渲染原理</li>
<li>源码篇 | 手写 SSR</li>
<li>源码篇 | mini-next</li>
<li>源码篇 | mini-next</li>
<li>源码篇 | mini-next</li>
<li>源码篇 | mini-next</li>
<li>面试篇 | 常见面试题及解析</li>
<li>面试篇 | 常见面试题及解析</li>
<li>面试篇 | 常见面试题及解析</li>
</ol></div>
</div>

</div>
<div id="MySignature" role="contentinfo">
    漫思<br><br>
来源:https://www.cnblogs.com/sexintercourse/p/17962900
頁: [1]
查看完整版本: Next.js 开发指南 路由篇 | App Router