幽默天赋怪 發表於 2024-1-7 11:37:00

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

<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 的路由基于的是文件系统,也就是说,一个文件就可以是一个路由。举个例子,你在 <code>app/pages</code> 目录下创建一个 <code>index.js</code> 文件,它会直接映射到 <code>/</code> 路由地址:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// app/pages/index.js
<span class="code-block-extension-codeLine" data-line-num="2"><span class="hljs-keyword">import <span class="hljs-title class_">React <span class="hljs-keyword">from <span class="hljs-string">'react'
<span class="code-block-extension-codeLine" data-line-num="3"><span class="hljs-keyword">export <span class="hljs-keyword">default () =&gt; <span class="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></span></span></span></code></pre>
<p>在 <code>app/pages</code> 目录下创建一个 <code>about.js</code> 文件,它会直接映射到 <code>/about</code> 路由地址:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// app/pages/about.js
<span class="code-block-extension-codeLine" data-line-num="2"><span class="hljs-keyword">import <span class="hljs-title class_">React <span class="hljs-keyword">from <span class="hljs-string">'react'
<span class="code-block-extension-codeLine" data-line-num="3"><span class="hljs-keyword">export <span class="hljs-keyword">default () =&gt; <span class="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></span></span></span></code></pre>
<h2 data-id="heading-2">2. 从 Pages Router 到 App Router</h2>
<p>现在你打开使用 <code>create-next-app</code> 创建的项目,你会发现默认并没有 <code>pages</code> 这个目录。查看 <code>packages.json</code>中的 Next.js 版本,如果版本号大于 <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 呢?知其然知其所以然,让我们简单追溯一下。以前我们声明一个路由,只用在 <code>pages</code> 目录下创建一个文件就可以了,以前的目录结构类似于:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1">└── pages
<span class="code-block-extension-codeLine" data-line-num="2">    ├── index.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="3">    ├── about.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="4">    └── more.<span class="hljs-property">js
</span></span></span></span></span></span></span></code></pre>
<p>这种方式有一个弊端,那就是 <code>pages</code> 目录的所有 js 文件都会被当成路由文件,这就导致比如组件不能写在 <code>pages</code> 目录下,这就不符合开发者的使用习惯。(当然 Pages Router 还有很多其他的问题,只不过目前我们介绍的内容还太少,为了不增加大家的理解成本,就不多说了)</p>
<p>升级为新的 App Router 后,现在的目录结构类似于:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1">src/
<span class="code-block-extension-codeLine" data-line-num="2">└── app
<span class="code-block-extension-codeLine" data-line-num="3">    ├── page.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="4">    ├── layout.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="5">    ├── template.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="6">    ├── loading.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="7">    ├── error.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="8">    └── not-found.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="9">    ├── about
<span class="code-block-extension-codeLine" data-line-num="10">    │   └── page.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="11">    └── more
<span class="code-block-extension-codeLine" data-line-num="12">      └── page.<span class="hljs-property">js
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>使用新的模式后,你会发现 <code>app</code> 下多了很多文件。这些文件的名字并不是我乱起的,而是 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,只需要在 <code>src</code> 目录下创建一个 <code>pages</code> 文件夹或者在根目录下创建一个 <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 片段的路由片段。创建嵌套的路由,只需要创建嵌套的文件夹。举个例子,下图的 <code>app/dashboard/settings</code>目录对应的路由地址就是 <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>那如何保证这个路由可以被访问呢?你需要创建一个特殊的名为 <code>page.js</code> 的文件。至于为什么叫 <code>page.js</code>呢?除了 <code>page</code> 有“页面”这个含义之外,你可以理解为这是一种约定或者规范。(如果你是 Next.js 的开发者,你也可以约定为 <code>index.js</code>甚至 <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> 对应路由 <code>/</code></li>
<li><code>app/dashboard/page.js</code> 对应路由 <code>/dashboard</code></li>
<li><code>app/dashboard/settings/page.js</code> 对应路由<code>/dashboard/settings</code></li>
<li><code>analytics</code> 目录下因为没有 <code>page.js</code> 文件,所以没有对应的路由。这个文件可以被用于存放组件、样式表、图片或者其他文件。</li>
</ul>
<p><strong>当然不止 <code>.js</code>文件,Next.js 默认是支持 React、TypeScript 的,所以 <code>.js</code>、<code>.jsx</code>、<code>.tsx</code> 都是可以的。</strong></p>
<p>那这个 <code>page.js</code> 代码如何写呢?最常见的是展示 UI,比如:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// app/page.js
<span class="code-block-extension-codeLine" data-line-num="2"><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="code-block-extension-codeLine" data-line-num="3"><span class="hljs-keyword">return <span class="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 class="code-block-extension-codeLine" data-line-num="4">}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>访问 <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>定义一个布局,你需要新建一个名为 <code>layout.js</code>的文件,该文件默认导出一个 React 组件,该组件应接收一个 <code>children</code> prop,<code>chidren</code> 表示子布局(如果有的话)或者子页面。</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><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// app/dashboard/layout.js
<span class="code-block-extension-codeLine" data-line-num="2"><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">{
<span class="code-block-extension-codeLine" data-line-num="3">children,
<span class="code-block-extension-codeLine" data-line-num="4">}) {
<span class="code-block-extension-codeLine" data-line-num="5"><span class="hljs-keyword">return (
<span class="code-block-extension-codeLine" data-line-num="6">    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">section&gt;
<span class="code-block-extension-codeLine" data-line-num="7">      <span class="hljs-tag">&lt;<span class="hljs-name">nav&gt;nav<span class="hljs-tag">&lt;/<span class="hljs-name">nav&gt;
<span class="code-block-extension-codeLine" data-line-num="8">      {children}
<span class="code-block-extension-codeLine" data-line-num="9">    <span class="hljs-tag">&lt;/<span class="hljs-name">section&gt;
<span class="code-block-extension-codeLine" data-line-num="10">)
<span class="code-block-extension-codeLine" data-line-num="11">}
</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>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// app/dashboard/page.js
<span class="code-block-extension-codeLine" data-line-num="2"><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="code-block-extension-codeLine" data-line-num="3"><span class="hljs-keyword">return <span class="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 class="code-block-extension-codeLine" data-line-num="4">}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>当访问 <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> 来自于 <code>app/dashboard/layout.js</code>,<code>Hello, Dashboard!</code> 来自于 <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> 代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// app/dashboard/settings/page.js
<span class="code-block-extension-codeLine" data-line-num="2"><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="code-block-extension-codeLine" data-line-num="3"><span class="hljs-keyword">return <span class="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 class="code-block-extension-codeLine" data-line-num="4">}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>当访问 <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> 来自于 <code>app/dashboard/layout.js</code>,<code>Hello, Settings!</code> 来自于 <code>app/dashboard/settings/page.js</code></p>
<p><strong>你可以发现:布局是支持嵌套的</strong>,<code>app/dashboard/settings/page.js</code> 会使用 <code>app/layout.js</code> 和 <code>app/dashboard/layout.js</code> 两个布局中的内容,不过因为我们没有在 <code>app/layout.js</code> 写入可以展示的内容,所以图中没有体现出来。</p>
<h4 data-id="heading-8">根布局(Root Layout)</h4>
<p>布局支持嵌套,最顶层的布局我们称之为根布局(Root Layout),也就是 <code>app/layout.js</code>。它会应用于所有的路由。除此之外,这个布局还有点特殊。</p>
<p>使用 <code>create-next-app</code> 默认创建的 <code>layout.js</code> 代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// app/layout.js
<span class="code-block-extension-codeLine" data-line-num="2"><span class="hljs-keyword">import <span class="hljs-string">'./globals.css'
<span class="code-block-extension-codeLine" data-line-num="3"><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="code-block-extension-codeLine" data-line-num="4">
<span class="code-block-extension-codeLine" data-line-num="5"><span class="hljs-keyword">const inter = <span class="hljs-title class_">Inter({ <span class="hljs-attr">subsets: [<span class="hljs-string">'latin'] })
<span class="code-block-extension-codeLine" data-line-num="6">
<span class="code-block-extension-codeLine" data-line-num="7"><span class="hljs-keyword">export <span class="hljs-keyword">const metadata = {
<span class="code-block-extension-codeLine" data-line-num="8"><span class="hljs-attr">title: <span class="hljs-string">'Create Next App',
<span class="code-block-extension-codeLine" data-line-num="9"><span class="hljs-attr">description: <span class="hljs-string">'Generated by create next app',
<span class="code-block-extension-codeLine" data-line-num="10">}
<span class="code-block-extension-codeLine" data-line-num="11">
<span class="code-block-extension-codeLine" data-line-num="12"><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="code-block-extension-codeLine" data-line-num="13"><span class="hljs-keyword">return (
<span class="code-block-extension-codeLine" data-line-num="14">    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">html <span class="hljs-attr">lang=<span class="hljs-string">"en"&gt;
<span class="code-block-extension-codeLine" data-line-num="15">      <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="code-block-extension-codeLine" data-line-num="16">    <span class="hljs-tag">&lt;/<span class="hljs-name">html&gt;
<span class="code-block-extension-codeLine" data-line-num="17">)
<span class="code-block-extension-codeLine" data-line-num="18">}
</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></span></span></span></span></span></span></code></pre>
<p>其中:</p>
<ol>
<li><code>app</code> 目录必须包含根布局,也就是 <code>app/layout.js</code> 这个文件是必需的。</li>
<li>根布局必须包含 <code>html</code> 和 <code>body</code>标签,其他布局不能包含这些标签。但如果你要更改这些标签,不推荐直接修改,Next.js 提供内置工具帮助你管理诸如 <code>&lt;title /&gt;</code> 这样的 HTML 元素。</li>
<li>你可以使用路由组创建多个根布局。</li>
<li>默认根布局是服务端组件,且不能设置为客户端组件。</li>
</ol>
<h3 data-id="heading-9">4.4. 定义模板(Templates)</h3>
<p>模板类似于布局,它也会传入每个子布局或者页面。但不会像布局那样维持状态。</p>
<p>模板在路由切换时会为每一个 children 创建一个实例。这就意味着当用户在共享一个模板的路由间跳转的时候,将会重新挂载组件实例,重新创建 DOM 元素,不保留状态。这听起来有点抽象,没有关系,我们先看看模板的写法,再写个 demo 你就明白了。</p>
<p>定义一个模板,你需要新建一个名为 <code>template.js</code> 的文件,该文件默认导出一个 React 组件,该组件接收一个 <code>children</code> prop。我们写个示例代码。在 <code>app</code>目录下新建一个 <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> 代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// app/template.js
<span class="code-block-extension-codeLine" data-line-num="2"><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="code-block-extension-codeLine" data-line-num="3"><span class="hljs-keyword">return <span class="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 class="code-block-extension-codeLine" data-line-num="4">}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>你会发现,这用法跟布局一模一样。它们最大的区别就是状态的保持。如果同一目录下既有 <code>template.js</code> 也有 <code>layout.js</code>,最后的输出效果如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1">&lt;<span class="hljs-title class_">Layout&gt;
<span class="code-block-extension-codeLine" data-line-num="2">{<span class="hljs-comment">/* 模板需要给一个唯一的 key */}
<span class="code-block-extension-codeLine" data-line-num="3">&lt;<span class="hljs-title class_">Template key={routeParam}&gt;{children}&lt;/<span class="hljs-title class_">Template&gt;
<span class="code-block-extension-codeLine" data-line-num="4">&lt;/<span class="hljs-title class_">Layout&gt;
</span></span></span></span></span></span></span></span></span></code></pre>
<p>也就是说 <code>layout</code> 会包裹 <code>template</code>,<code>template</code> 又会包裹 <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><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1">app
<span class="code-block-extension-codeLine" data-line-num="2">└─ dashboard
<span class="code-block-extension-codeLine" data-line-num="3">   ├─ layout.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="4">   ├─ page.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="5">   ├─ template.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="6">   ├─ about
<span class="code-block-extension-codeLine" data-line-num="7">   │└─ page.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="8">   └─ settings
<span class="code-block-extension-codeLine" data-line-num="9">      └─ page.<span class="hljs-property">js
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>其中 <code>dashboard/layout.js</code> 代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-string">'use client'
<span class="code-block-extension-codeLine" data-line-num="2">
<span class="code-block-extension-codeLine" data-line-num="3"><span class="hljs-keyword">import { useState } <span class="hljs-keyword">from <span class="hljs-string">'react'
<span class="code-block-extension-codeLine" data-line-num="4"><span class="hljs-keyword">import <span class="hljs-title class_">Link <span class="hljs-keyword">from <span class="hljs-string">'next/link'
<span class="code-block-extension-codeLine" data-line-num="5">
<span class="code-block-extension-codeLine" data-line-num="6"><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="code-block-extension-codeLine" data-line-num="7"><span class="hljs-keyword">const = <span class="hljs-title function_">useState(<span class="hljs-number">0)
<span class="code-block-extension-codeLine" data-line-num="8"><span class="hljs-keyword">return (
<span class="code-block-extension-codeLine" data-line-num="9">    <span class="xml"><span class="hljs-tag">&lt;&gt;
<span class="code-block-extension-codeLine" data-line-num="10">      <span class="hljs-tag">&lt;<span class="hljs-name">div&gt;
<span class="code-block-extension-codeLine" data-line-num="11">      <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="code-block-extension-codeLine" data-line-num="12">      <span class="hljs-tag">&lt;<span class="hljs-name">br/&gt;
<span class="code-block-extension-codeLine" data-line-num="13">      <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="code-block-extension-codeLine" data-line-num="14">      <span class="hljs-tag">&lt;/<span class="hljs-name">div&gt;
<span class="code-block-extension-codeLine" data-line-num="15">      <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="code-block-extension-codeLine" data-line-num="16">      <span class="hljs-tag">&lt;<span class="hljs-name">button <span class="hljs-attr">onClick=<span class="hljs-string">{() =&gt; setCount(count + 1)}&gt;
<span class="code-block-extension-codeLine" data-line-num="17">      Increment
<span class="code-block-extension-codeLine" data-line-num="18">      <span class="hljs-tag">&lt;/<span class="hljs-name">button&gt;
<span class="code-block-extension-codeLine" data-line-num="19">      {children}
<span class="code-block-extension-codeLine" data-line-num="20">    <span class="hljs-tag">&lt;/&gt;
<span class="code-block-extension-codeLine" data-line-num="21">)
<span class="code-block-extension-codeLine" data-line-num="22">}
</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></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> 代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-string">'use client'
<span class="code-block-extension-codeLine" data-line-num="2">
<span class="code-block-extension-codeLine" data-line-num="3"><span class="hljs-keyword">import { useState } <span class="hljs-keyword">from <span class="hljs-string">'react'
<span class="code-block-extension-codeLine" data-line-num="4">
<span class="code-block-extension-codeLine" data-line-num="5"><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="code-block-extension-codeLine" data-line-num="6"><span class="hljs-keyword">const = <span class="hljs-title function_">useState(<span class="hljs-number">0)
<span class="code-block-extension-codeLine" data-line-num="7"><span class="hljs-keyword">return (
<span class="code-block-extension-codeLine" data-line-num="8">    <span class="xml"><span class="hljs-tag">&lt;&gt;
<span class="code-block-extension-codeLine" data-line-num="9">      <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="code-block-extension-codeLine" data-line-num="10">      <span class="hljs-tag">&lt;<span class="hljs-name">button <span class="hljs-attr">onClick=<span class="hljs-string">{() =&gt; setCount(count + 1)}&gt;
<span class="code-block-extension-codeLine" data-line-num="11">      Increment
<span class="code-block-extension-codeLine" data-line-num="12">      <span class="hljs-tag">&lt;/<span class="hljs-name">button&gt;
<span class="code-block-extension-codeLine" data-line-num="13">      {children}
<span class="code-block-extension-codeLine" data-line-num="14">    <span class="hljs-tag">&lt;/&gt;
<span class="code-block-extension-codeLine" data-line-num="15">)
<span class="code-block-extension-codeLine" data-line-num="16">}
</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/page.js</code>代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><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="code-block-extension-codeLine" data-line-num="2"><span class="hljs-keyword">return <span class="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 class="code-block-extension-codeLine" data-line-num="3">}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p><code>dashboard/about/page.js</code>代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><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="code-block-extension-codeLine" data-line-num="2"><span class="hljs-keyword">return <span class="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 class="code-block-extension-codeLine" data-line-num="3">}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p><code>dashboard/settings/page.js</code>代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><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="code-block-extension-codeLine" data-line-num="2"><span class="hljs-keyword">return <span class="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 class="code-block-extension-codeLine" data-line-num="3">}
</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/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>现在点击两个 <code>Increment</code> 按钮,会开始计数。随便点击下数字,然后再点击 <code>About</code>或者 <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>现在我们已经了解了 <code>page.js</code>、<code>layout.js</code>、<code>template.js</code>的功能,然而特殊文件还不止这些。App Router 提供了用于展示加载界面的 <code>loading.js</code>。</p>
<p>这个功能的实现借助了 React 的<code>Suspense</code> API。关于 Suspense 的用法,可以查看 《React 之 Suspense》。它实现的效果就是当发生路由变化的时候,立刻展示 fallback UI,等加载完成后,展示数据。</p>
<pre></pre>
<pre><code class="hljs language-jsx code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// 在 ProfilePage 组件处于加载阶段时显示 Spinner
<span class="code-block-extension-codeLine" data-line-num="2">&lt;<span class="hljs-title class_">Suspense fallback={<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Spinner /&gt;}&gt;
<span class="code-block-extension-codeLine" data-line-num="3"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">ProfilePage /&gt;
<span class="code-block-extension-codeLine" data-line-num="4">&lt;/<span class="hljs-title class_">Suspense&gt;
</span></span></span></span></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>了解了原理,那我们来看看如何写这个 <code>loading.js</code>吧。<code>dashboard</code> 目录下我们新建一个 <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><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// app/dashboard/loading.js
<span class="code-block-extension-codeLine" data-line-num="2"><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="code-block-extension-codeLine" data-line-num="3"><span class="hljs-keyword">return <span class="xml"><span class="hljs-tag">&lt;&gt;Loading dashboard...<span class="hljs-tag">&lt;/&gt;
<span class="code-block-extension-codeLine" data-line-num="4">}
</span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p>同级的 <code>page.js</code> 代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// app/dashboard/page.js
<span class="code-block-extension-codeLine" data-line-num="2"><span class="hljs-keyword">async <span class="hljs-keyword">function <span class="hljs-title function_">getData(<span class="hljs-params">) {
<span class="code-block-extension-codeLine" data-line-num="3"><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="code-block-extension-codeLine" data-line-num="4"><span class="hljs-keyword">return {
<span class="code-block-extension-codeLine" data-line-num="5">    <span class="hljs-attr">message: <span class="hljs-string">'Hello, Dashboard!',
<span class="code-block-extension-codeLine" data-line-num="6">}
<span class="code-block-extension-codeLine" data-line-num="7">}
<span class="code-block-extension-codeLine" data-line-num="8"><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="code-block-extension-codeLine" data-line-num="9"><span class="hljs-keyword">const { message } = <span class="hljs-keyword">await <span class="hljs-title function_">getData()
<span class="code-block-extension-codeLine" data-line-num="10"><span class="hljs-keyword">return <span class="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 class="code-block-extension-codeLine" data-line-num="11">}
</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>不再需要其他的代码,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>就是这么简单。其关键在于 <code>page.js</code>导出了一个 async 函数。</p>
<p><code>loading.js</code> 的实现原理是将 <code>page.js</code>和下面的 children 用 <code>&lt;Suspense&gt;</code> 包裹。因为<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 的 <code>use</code> 函数。现在我们在 <code>dashboard</code>下新建一个 <code>about</code>目录,在其中新建 <code>page.js</code>文件。</p>
<p><code>/dashboard/about/page.js</code> 代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// /dashboard/about/page.js
<span class="code-block-extension-codeLine" data-line-num="2"><span class="hljs-keyword">import { use } <span class="hljs-keyword">from <span class="hljs-string">'react'
<span class="code-block-extension-codeLine" data-line-num="3">
<span class="code-block-extension-codeLine" data-line-num="4"><span class="hljs-keyword">async <span class="hljs-keyword">function <span class="hljs-title function_">getData(<span class="hljs-params">) {
<span class="code-block-extension-codeLine" data-line-num="5"><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="code-block-extension-codeLine" data-line-num="6"><span class="hljs-keyword">return {
<span class="code-block-extension-codeLine" data-line-num="7">    <span class="hljs-attr">message: <span class="hljs-string">'Hello, About!',
<span class="code-block-extension-codeLine" data-line-num="8">}
<span class="code-block-extension-codeLine" data-line-num="9">}
<span class="code-block-extension-codeLine" data-line-num="10">
<span class="code-block-extension-codeLine" data-line-num="11"><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="code-block-extension-codeLine" data-line-num="12"><span class="hljs-keyword">const {message} = <span class="hljs-title function_">use(<span class="hljs-title function_">getData())
<span class="code-block-extension-codeLine" data-line-num="13"><span class="hljs-keyword">return <span class="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 class="code-block-extension-codeLine" data-line-num="14">}
</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>同样实现了 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>如果你想针对 <code>/dashboard/about</code> 单独实现一个 loading 效果,那就在 <code>about</code> 目录下再写一个 <code>loading.js</code> 即可。</p>
<p>如果同一文件夹既有 <code>layout.js</code> 又有 <code>template.js</code> 又有 <code>loading.js</code> ,那它们的层级关系是怎样呢?</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>再讲讲特殊文件 <code>error.js</code>。顾名思义,用来创建发生错误时的展示 UI。</p>
<p>其实现借助了 React 的 Error Boundary 功能。简单来说,就是给 page.js 和 children 包了一层 <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 演示一下 <code>error.js</code> 的效果。<code>dashboard</code> 目录下新建一个 <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><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-string">'use client' <span class="hljs-comment">// 错误组件必须是客户端组件
<span class="code-block-extension-codeLine" data-line-num="2"><span class="hljs-comment">// dashboard/error.js
<span class="code-block-extension-codeLine" data-line-num="3"><span class="hljs-keyword">import { useEffect } <span class="hljs-keyword">from <span class="hljs-string">'react'
<span class="code-block-extension-codeLine" data-line-num="4">
<span class="code-block-extension-codeLine" data-line-num="5"><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="code-block-extension-codeLine" data-line-num="6"><span class="hljs-title function_">useEffect(<span class="hljs-function">() =&gt; {
<span class="code-block-extension-codeLine" data-line-num="7">    <span class="hljs-variable language_">console.<span class="hljs-title function_">error(error)
<span class="code-block-extension-codeLine" data-line-num="8">}, )
<span class="code-block-extension-codeLine" data-line-num="9">
<span class="code-block-extension-codeLine" data-line-num="10"><span class="hljs-keyword">return (
<span class="code-block-extension-codeLine" data-line-num="11">    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div&gt;
<span class="code-block-extension-codeLine" data-line-num="12">      <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="code-block-extension-codeLine" data-line-num="13">      <span class="hljs-tag">&lt;<span class="hljs-name">button
<span class="code-block-extension-codeLine" data-line-num="14">      <span class="hljs-attr">onClick=<span class="hljs-string">{
<span class="code-block-extension-codeLine" data-line-num="15">          // 尝试恢复
<span class="code-block-extension-codeLine" data-line-num="16">          () =&gt; reset()
<span class="code-block-extension-codeLine" data-line-num="17">      }
<span class="code-block-extension-codeLine" data-line-num="18">      &gt;
<span class="code-block-extension-codeLine" data-line-num="19">      Try again
<span class="code-block-extension-codeLine" data-line-num="20">      <span class="hljs-tag">&lt;/<span class="hljs-name">button&gt;
<span class="code-block-extension-codeLine" data-line-num="21">    <span class="hljs-tag">&lt;/<span class="hljs-name">div&gt;
<span class="code-block-extension-codeLine" data-line-num="22">)
<span class="code-block-extension-codeLine" data-line-num="23">}
</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></span></span></span></span></span></span></code></pre>
<p>为触发 Error 错误,同级 <code>page.js</code> 的代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-string">"use client";
<span class="code-block-extension-codeLine" data-line-num="2"><span class="hljs-comment">// dashboard/page.js
<span class="code-block-extension-codeLine" data-line-num="3"><span class="hljs-keyword">import <span class="hljs-title class_">React <span class="hljs-keyword">from <span class="hljs-string">"react";
<span class="code-block-extension-codeLine" data-line-num="4">
<span class="code-block-extension-codeLine" data-line-num="5"><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="code-block-extension-codeLine" data-line-num="6"><span class="hljs-keyword">const = <span class="hljs-title class_">React.<span class="hljs-title function_">useState(<span class="hljs-literal">false);
<span class="code-block-extension-codeLine" data-line-num="7">
<span class="code-block-extension-codeLine" data-line-num="8"><span class="hljs-keyword">const <span class="hljs-title function_">handleGetError = (<span class="hljs-params">) =&gt; {
<span class="code-block-extension-codeLine" data-line-num="9">    <span class="hljs-title function_">setError(<span class="hljs-literal">true);
<span class="code-block-extension-codeLine" data-line-num="10">};
<span class="code-block-extension-codeLine" data-line-num="11">
<span class="code-block-extension-codeLine" data-line-num="12"><span class="hljs-keyword">return (
<span class="code-block-extension-codeLine" data-line-num="13">    <span class="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 class="code-block-extension-codeLine" data-line-num="14">);
<span class="code-block-extension-codeLine" data-line-num="15">}
</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>效果如下:</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 会在 <code>error.js</code> 导出的组件中,传入 <code>reset</code> 函数,帮助尝试从错误中恢复。该函数会触发重新渲染错误边界里的内容。如果成功,会替换展示重新渲染的内容。</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>从这张图里你会发现一个问题,因为 <code>Layout</code> 和 <code>Template</code> 在 <code>ErrorBoundary</code> 外面,这说明错误边界不能捕获同级的 <code>layout.js</code> 或者 <code>template.js</code> 中的错误。如果你想捕获特定布局或者模板中的错误,那就在父级的 <code>error.js</code> 里进行捕获。</p>
<p>那问题来了,如果已经到了顶层,就比如根布局中的错误如何捕获呢?为了解决这个问题,Next.js 提供了 <code>global-error.js</code>文件,使用它时,将其放在 <code>app</code> 目录下。</p>
<p><code>global-error.js</code>会包裹整个应用,而且当它触发的时候,它会替换掉根布局的内容。所以,<code>global-error.js</code> 中也要定义 <code>&lt;html&gt;</code> 和 <code>&lt;body&gt;</code> 标签。</p>
<p><code>global-error.js</code>示例代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-string">'use client'
<span class="code-block-extension-codeLine" data-line-num="2"><span class="hljs-comment">// app/global-error.js
<span class="code-block-extension-codeLine" data-line-num="3"><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="code-block-extension-codeLine" data-line-num="4"><span class="hljs-keyword">return (
<span class="code-block-extension-codeLine" data-line-num="5">    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">html&gt;
<span class="code-block-extension-codeLine" data-line-num="6">      <span class="hljs-tag">&lt;<span class="hljs-name">body&gt;
<span class="code-block-extension-codeLine" data-line-num="7">      <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="code-block-extension-codeLine" data-line-num="8">      <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="code-block-extension-codeLine" data-line-num="9">      <span class="hljs-tag">&lt;/<span class="hljs-name">body&gt;
<span class="code-block-extension-codeLine" data-line-num="10">    <span class="hljs-tag">&lt;/<span class="hljs-name">html&gt;
<span class="code-block-extension-codeLine" data-line-num="11">)
<span class="code-block-extension-codeLine" data-line-num="12">}
</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>global-error.js</code> 用来处理根布局和根模板中的错误,<code>app/error.js</code> 建议还是要写的</p>
<h3 data-id="heading-13">4.7. 定义 404 页面</h3>
<p>最后再讲一个特殊文件 —— <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>如果你要替换这个效果,只需要在 <code>app</code> 目录下新建一个 <code>not-found.js</code>,代码示例如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-keyword">import <span class="hljs-title class_">Link <span class="hljs-keyword">from <span class="hljs-string">'next/link'
<span class="code-block-extension-codeLine" data-line-num="2">
<span class="code-block-extension-codeLine" data-line-num="3"><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="code-block-extension-codeLine" data-line-num="4"><span class="hljs-keyword">return (
<span class="code-block-extension-codeLine" data-line-num="5">    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div&gt;
<span class="code-block-extension-codeLine" data-line-num="6">      <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="code-block-extension-codeLine" data-line-num="7">      <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="code-block-extension-codeLine" data-line-num="8">      <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="code-block-extension-codeLine" data-line-num="9">    <span class="hljs-tag">&lt;/<span class="hljs-name">div&gt;
<span class="code-block-extension-codeLine" data-line-num="10">)
<span class="code-block-extension-codeLine" data-line-num="11">}
</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>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>考虑到 <code>layout.js</code> 和 <code>template.js</code> 的使用效果,如果我把 <code>not-found.js</code>添加到一个子目录里,比如 <code>/dashboard/blog</code>下:</p>
<pre></pre>
<pre><code class="hljs language-md code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1">dashboard
<span class="code-block-extension-codeLine" data-line-num="2">└─ blog
<span class="code-block-extension-codeLine" data-line-num="3">   ├─ page.js
<span class="code-block-extension-codeLine" data-line-num="4">   └─ not-found.js
</span></span></span></span></code></pre>
<p>当访问 <code>/dashboard/blog/yayu</code> (该路由没有声明)时,它的 not-found 效果会是自定义的吗?</p>
<p>答案是不会。访问 <code>/dashboard/blog/yayu</code> 依然会走到默认 not-found 路由效果。这是因为 <code>not-found.js</code> 被用于当 <code>notFound</code> 函数被抛出的时候。(<code>notFound</code> 函数是 <code>next/navigation </code>这个包提供的一个方法)</p>
<p>写个示例代码:</p>
<p><code>/dashboard/blog</code> 下新建一个 <code>page.js</code>和 <code>not-found.js</code>。<code>page.js</code> 代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// /dashboard/blog/page.js
<span class="code-block-extension-codeLine" data-line-num="2"><span class="hljs-keyword">import { notFound } <span class="hljs-keyword">from <span class="hljs-string">'next/navigation'
<span class="code-block-extension-codeLine" data-line-num="3">
<span class="code-block-extension-codeLine" data-line-num="4"><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="code-block-extension-codeLine" data-line-num="5"><span class="hljs-title function_">notFound()
<span class="code-block-extension-codeLine" data-line-num="6"><span class="hljs-keyword">return <span class="xml"><span class="hljs-tag">&lt;&gt;<span class="hljs-tag">&lt;/&gt;
<span class="code-block-extension-codeLine" data-line-num="7">}
</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>not-found.js</code> 代码则直接复制上面的 <code>not-found.js</code>。</p>
<p>当访问 <code>/dashboard/blog</code>时,因为 <code>page.js</code> 丢出了 <code>notFound</code> 函数,所以会触发 <code>not-found.js</code> 的执行。</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>但当访问 <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>如果我们添加对应的处理程序,在 <code>app/dashboard/blog/yayu/page.js</code>中也执行 notFound 函数,就会渲染 <code>/dashboard/blog/not-found.js</code>的 UI 内容。</p>
<p>对应到实际开发,当我们请求一个用户的数据时或是请求一篇文章的数据时,如果该数据不存在,就可以直接丢出 <code>notFound</code> 函数,渲染自定义的<code>not-found</code> 界面。一个示例代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// app/dashboard/blog//page.js
<span class="code-block-extension-codeLine" data-line-num="2"><span class="hljs-keyword">import { notFound } <span class="hljs-keyword">from <span class="hljs-string">'next/navigation'
<span class="code-block-extension-codeLine" data-line-num="3">
<span class="code-block-extension-codeLine" data-line-num="4"><span class="hljs-keyword">async <span class="hljs-keyword">function <span class="hljs-title function_">fetchUser(<span class="hljs-params">id) {
<span class="code-block-extension-codeLine" data-line-num="5"><span class="hljs-keyword">const res = <span class="hljs-keyword">await <span class="hljs-title function_">fetch(<span class="hljs-string">'https://...')
<span class="code-block-extension-codeLine" data-line-num="6"><span class="hljs-keyword">if (!res.<span class="hljs-property">ok) <span class="hljs-keyword">return <span class="hljs-literal">undefined
<span class="code-block-extension-codeLine" data-line-num="7"><span class="hljs-keyword">return res.<span class="hljs-title function_">json()
<span class="code-block-extension-codeLine" data-line-num="8">}
<span class="code-block-extension-codeLine" data-line-num="9">
<span class="code-block-extension-codeLine" data-line-num="10"><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="code-block-extension-codeLine" data-line-num="11"><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="code-block-extension-codeLine" data-line-num="12">
<span class="code-block-extension-codeLine" data-line-num="13"><span class="hljs-keyword">if (!user) {
<span class="code-block-extension-codeLine" data-line-num="14">    <span class="hljs-title function_">notFound()
<span class="code-block-extension-codeLine" data-line-num="15">}
<span class="code-block-extension-codeLine" data-line-num="16">
<span class="code-block-extension-codeLine" data-line-num="17"><span class="hljs-comment">// ...
<span class="code-block-extension-codeLine" data-line-num="18">}
</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></span></code></pre>
<h2 data-id="heading-14">5. 链接和导航(Linking and Navigating)</h2>
<p>知道了如何定义路由,最后我们再讲讲如何在 Next.js 中实现链接和导航。Next.js 提供了两种方式:</p>
<ol>
<li>使用 <code>&lt;Link&gt;</code> 组件</li>
<li>使用 <code>useRouter</code> Hook</li>
</ol>
<h3 data-id="heading-15">5.1. <code>&lt;Link&gt;</code></h3>
<p><code>&lt;Link&gt;</code> 是一个拓展了 HTML <code>&lt;a&gt;</code> 标签的内置组件,用来实现预获取(prefetching) 和客户端路由导航。这是 Next.js 中路由导航的主要方式。</p>
<h4 data-id="heading-16">5.1.1. 基础使用</h4>
<p>使用示例如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-keyword">import <span class="hljs-title class_">Link <span class="hljs-keyword">from <span class="hljs-string">'next/link'
<span class="code-block-extension-codeLine" data-line-num="2">
<span class="code-block-extension-codeLine" data-line-num="3"><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="code-block-extension-codeLine" data-line-num="4"><span class="hljs-keyword">return <span class="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 class="code-block-extension-codeLine" data-line-num="5">}
</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>支持 hash 值用于实现跳转到页面的某个位置:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1">&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="code-block-extension-codeLine" data-line-num="2">
<span class="code-block-extension-codeLine" data-line-num="3"><span class="hljs-comment">// Output
<span class="code-block-extension-codeLine" data-line-num="4"><span class="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></span></span></span></span></code></pre>
<h4 data-id="heading-17">5.1.2. 动态渲染链接</h4>
<p>支持路由链接动态渲染:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-keyword">import <span class="hljs-title class_">Link <span class="hljs-keyword">from <span class="hljs-string">'next/link'
<span class="code-block-extension-codeLine" data-line-num="2">
<span class="code-block-extension-codeLine" data-line-num="3"><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="code-block-extension-codeLine" data-line-num="4"><span class="hljs-keyword">return (
<span class="code-block-extension-codeLine" data-line-num="5">    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">ul&gt;
<span class="code-block-extension-codeLine" data-line-num="6">      {posts.map((post) =&gt; (
<span class="code-block-extension-codeLine" data-line-num="7">      <span class="hljs-tag">&lt;<span class="hljs-name">li <span class="hljs-attr">key=<span class="hljs-string">{post.id}&gt;
<span class="code-block-extension-codeLine" data-line-num="8">          <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="code-block-extension-codeLine" data-line-num="9">      <span class="hljs-tag">&lt;/<span class="hljs-name">li&gt;
<span class="code-block-extension-codeLine" data-line-num="10">      ))}
<span class="code-block-extension-codeLine" data-line-num="11">    <span class="hljs-tag">&lt;/<span class="hljs-name">ul&gt;
<span class="code-block-extension-codeLine" data-line-num="12">)
<span class="code-block-extension-codeLine" data-line-num="13">}
</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>
<h4 data-id="heading-18">5.1.3. 获取当前路径名</h4>
<p>如果需要对当前链接进行判断,你可以使用 usePathname() 这个方法,它会读取当前 URL 的路径名(pathname),这是一段示例代码:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-string">'use client'
<span class="code-block-extension-codeLine" data-line-num="2">
<span class="code-block-extension-codeLine" data-line-num="3"><span class="hljs-keyword">import { usePathname } <span class="hljs-keyword">from <span class="hljs-string">'next/navigation'
<span class="code-block-extension-codeLine" data-line-num="4"><span class="hljs-keyword">import <span class="hljs-title class_">Link <span class="hljs-keyword">from <span class="hljs-string">'next/link'
<span class="code-block-extension-codeLine" data-line-num="5">
<span class="code-block-extension-codeLine" data-line-num="6"><span class="hljs-keyword">export <span class="hljs-keyword">function <span class="hljs-title function_">Navigation(<span class="hljs-params">{ navLinks }) {
<span class="code-block-extension-codeLine" data-line-num="7"><span class="hljs-keyword">const pathname = <span class="hljs-title function_">usePathname()
<span class="code-block-extension-codeLine" data-line-num="8">
<span class="code-block-extension-codeLine" data-line-num="9"><span class="hljs-keyword">return (
<span class="code-block-extension-codeLine" data-line-num="10">    <span class="xml"><span class="hljs-tag">&lt;&gt;
<span class="code-block-extension-codeLine" data-line-num="11">      {navLinks.map((link) =&gt; {
<span class="code-block-extension-codeLine" data-line-num="12">      const isActive = pathname === link.href
<span class="code-block-extension-codeLine" data-line-num="13">
<span class="code-block-extension-codeLine" data-line-num="14">      return (
<span class="code-block-extension-codeLine" data-line-num="15">          <span class="hljs-tag">&lt;<span class="hljs-name">Link
<span class="code-block-extension-codeLine" data-line-num="16">            <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="code-block-extension-codeLine" data-line-num="17">            <span class="hljs-attr">href=<span class="hljs-string">{link.href}
<span class="code-block-extension-codeLine" data-line-num="18">            <span class="hljs-attr">key=<span class="hljs-string">{link.name}
<span class="code-block-extension-codeLine" data-line-num="19">          &gt;
<span class="code-block-extension-codeLine" data-line-num="20">            {link.name}
<span class="code-block-extension-codeLine" data-line-num="21">          <span class="hljs-tag">&lt;/<span class="hljs-name">Link&gt;
<span class="code-block-extension-codeLine" data-line-num="22">      )
<span class="code-block-extension-codeLine" data-line-num="23">      })}
<span class="code-block-extension-codeLine" data-line-num="24">    <span class="hljs-tag">&lt;/&gt;
<span class="code-block-extension-codeLine" data-line-num="25">)
<span class="code-block-extension-codeLine" data-line-num="26">}
</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></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>如果你想要禁用这个行为,你可以给 <code>&lt;Link&gt;</code> 组件传递一个 <code>scroll={false}</code>,或者在使用 <code>router.push</code>和 <code>router.replace</code>的时候,设置 <code>scroll: false</code>。</p>
<p>示例代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// next/link
<span class="code-block-extension-codeLine" data-line-num="2">&lt;<span class="hljs-title class_">Link href=<span class="hljs-string">"/dashboard" scroll={<span class="hljs-literal">false}&gt;
<span class="code-block-extension-codeLine" data-line-num="3"><span class="hljs-title class_">Dashboard
<span class="code-block-extension-codeLine" data-line-num="4">&lt;/<span class="hljs-title class_">Link&gt;
</span></span></span></span></span></span></span></span></span></span></code></pre>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-comment">// useRouter
<span class="code-block-extension-codeLine" data-line-num="2"><span class="hljs-keyword">import { useRouter } <span class="hljs-keyword">from <span class="hljs-string">'next/navigation'
<span class="code-block-extension-codeLine" data-line-num="3">
<span class="code-block-extension-codeLine" data-line-num="4"><span class="hljs-keyword">const router = <span class="hljs-title function_">useRouter()
<span class="code-block-extension-codeLine" data-line-num="5">
<span class="code-block-extension-codeLine" data-line-num="6">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></span></span></span></span></span></span></code></pre>
<p>注:关于 <code>&lt;Link&gt;</code> 组件的用法,我们还会在《组件篇 | Link 和 Script》中详细介绍。</p>
<h3 data-id="heading-20">5.2. useRouter</h3>
<p>第二种方式是使用 useRouter。示例代码如下:</p>
<pre></pre>
<pre><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1"><span class="hljs-string">'use client'
<span class="code-block-extension-codeLine" data-line-num="2">
<span class="code-block-extension-codeLine" data-line-num="3"><span class="hljs-keyword">import { useRouter } <span class="hljs-keyword">from <span class="hljs-string">'next/navigation'
<span class="code-block-extension-codeLine" data-line-num="4">
<span class="code-block-extension-codeLine" data-line-num="5"><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="code-block-extension-codeLine" data-line-num="6"><span class="hljs-keyword">const router = <span class="hljs-title function_">useRouter()
<span class="code-block-extension-codeLine" data-line-num="7">
<span class="code-block-extension-codeLine" data-line-num="8"><span class="hljs-keyword">return (
<span class="code-block-extension-codeLine" data-line-num="9">    <span class="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;
<span class="code-block-extension-codeLine" data-line-num="10">      Dashboard
<span class="code-block-extension-codeLine" data-line-num="11">    <span class="hljs-tag">&lt;/<span class="hljs-name">button&gt;
<span class="code-block-extension-codeLine" data-line-num="12">)
<span class="code-block-extension-codeLine" data-line-num="13">}
</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>注意使用该 hook 需要在客户端组件中。(顶层的 <code>'use client'</code> 就是声明这是客户端组件)</p>
<p>注:关于 useRouter,我们还会在《API 篇 | 请求相关的常用函数与方法》 篇中详细介绍。</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><code class="hljs language-javascript code-block-extension-codeShowNum"><span class="code-block-extension-codeLine" data-line-num="1">src/
<span class="code-block-extension-codeLine" data-line-num="2">└── app
<span class="code-block-extension-codeLine" data-line-num="3">    ├── page.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="4">    ├── layout.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="5">    ├── template.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="6">    ├── loading.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="7">    ├── error.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="8">    └── not-found.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="9">    ├── about
<span class="code-block-extension-codeLine" data-line-num="10">    │   └── page.<span class="hljs-property">js
<span class="code-block-extension-codeLine" data-line-num="11">    └── more
<span class="code-block-extension-codeLine" data-line-num="12">      └── page.<span class="hljs-property">js
</span></span></span></span></span></span></span></span></span></span></span></span></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><br><br>
来源:https://www.cnblogs.com/silva/p/17948723
頁: [1]
查看完整版本: Next.js 开发指南 路由篇 | App Router