Next.js 中的客户端路由

已发表: 2022-03-10
快速总结 ↬ Next.js 有一个基于文件的路由系统,其中每个页面根据其文件名自动成为一个路由。 每个页面都是从 pages 目录导出的默认 React 组件,可用于定义最常见的路由模式。 本文将指导您了解有关 Next.js 中路由的几乎所有知识,并为您指明相关主题和概念的方向。

超链接从一开始就是 Web 的瑰宝之一。 根据 MDN,超链接是使 Web成为 Web 的原因。 虽然用于文档之间的链接等目的,但其主要用途是引用可通过唯一网址或 URL 识别的不同网页。

路由是每个 Web 应用程序的一个重要方面,就像指向 Web 的超链接一样。 它是一种将请求路由到处理它们的代码的机制。 关于路由,Next.js 页面由唯一的 URL 路径引用和识别。 如果 Web 由通过超链接互连的导航网页组成,那么每个 Next.js 应用程序都由通过路由器互连的可路由页面(路由处理程序或路由)组成。

Next.js 内置了对路由的支持,但解包起来很麻烦,尤其是在考虑渲染和数据获取时。 作为理解 Next.js 中客户端路由的先决条件,有必要对 Next.js 中的路由、渲染和数据获取等概念有一个概述。

本文将对熟悉 Next.js 并希望了解它如何处理路由的 React 开发人员有所帮助。 您需要具备 React 和 Next.js 的工作知识才能充分利用本文,本文仅介绍 Next.js 中的客户端路由和相关概念。

路由和渲染

路由和渲染是相辅相成的,将在本文的整个过程中发挥重要作用。 我喜欢 Gaurav 对它们的解释:

路由是将用户导航到网站上不同页面的过程。

渲染是将这些页面放在 UI 上的过程。 每次您请求到特定页面的路由时,您也在渲染该页面,但并非每次渲染都是路由的结果。

花五分钟时间考虑一下。

关于 Next.js 中的渲染,您需要了解的是,每个页面都预先与最少的 JavaScript 代码一起预先渲染,以使其通过称为水合的过程变得完全交互。 Next.js 如何做到这一点高度依赖于预渲染的形式:静态生成服务器端渲染,它们都与所使用的数据获取技术高度耦合,并在生成页面的 HTML分开。

根据您的数据获取要求,您可能会发现自己使用内置的数据获取功能,如getStaticPropsgetStaticPathsgetServerSideProps 、客户端数据获取工具(如 SWR、react-query)或传统的数据获取方法(如 fetch-on-渲染,获取然后渲染,渲染为你获取(带有悬念)。

预渲染(在渲染之前 -到 UI )是对路由的补充,并且与数据获取高度耦合 - 在 Next.js 中它自己的整个主题。 因此,尽管这些概念是互补的或密切相关的,但本文将仅关注页面之间的导航(路由),并在必要时引用相关概念。

说完这些,让我们从基本要点开始:Next.js 有一个基于页面概念的基于文件系统的路由器。

跳跃后更多! 继续往下看↓

页面

Next.js 中的页面是自动作为路由可用的 React 组件。 它们作为默认导出从 pages 目录导出,支持的文件扩展名为.js.jsx.ts.tsx

一个典型的 Next.js 应用程序将具有一个包含顶级目录的文件夹结构,例如pagespublicstyles 。

 next-app ├── node_modules ├── pages │ ├── index.js // path: base-url (/) │ ├── books.jsx // path: /books │ └── book.ts // path: /book ├── public ├── styles ├── .gitignore ├── package.json └── README.md

每个页面都是一个 React 组件:

 // pages/books.js — `base-url/book` export default function Book() { return

图书

}

注意请记住,页面也可以称为“路由处理程序”。

自定义页面

这些是驻留在页面目录中但不参与路由的特殊页面。 它们以下划线符号为前缀,如_app.js_document.js

  • _app.js
    这是一个位于 pages 文件夹中的自定义组件。 Next.js 使用这个组件来初始化页面。
  • _document.js
    _app.js一样, _document.js是 Next.js 用来增强应用程序<html><body>标记的自定义组件。 这是必要的,因为 Next.js 页面跳过了周围文档标记的定义。
 next-app ├── node_modules ├── pages │ ├── _app.js // ️ Custom page (unavailable as a route) │ ├── _document.jsx // ️ Custom page (unavailable as a route) │ └── index.ts // path: base-url (/) ├── public ├── styles ├── .gitignore ├── package.json └── README.md

页面之间的链接

Next.js 从next/link API 公开了一个Link组件,可用于在页面之间执行客户端路由转换。

 // Import the <Link/> component import Link from "next/link"; // This could be a page component export default function TopNav() { return ( <nav> <Link href="/">Home</Link> <Link href="/">Publications</Link> <Link href="/">About</Link> </nav> ) } // This could be a non-page component export default function Publications() { return ( <section> <TopNav/> {/* ... */} </section> ) }

Link组件可以在任何组件内部使用,无论是否是页面。 当以上面示例中最基本的形式使用时, Link组件将转换为具有href属性的超链接。 (更多关于Link在下面的下一个/链接部分。)

路由

Next.js 基于文件的路由系统可用于定义最常见的路由模式。 为了适应这些模式,每条路由都根据其定义进行分离。

索引路线

默认情况下,在您的 Next.js 应用程序中,初始/默认路由是pages/index.js ,它会自动作为/用作应用程序的起点。 使用localhost:3000的基本 URL,可以在浏览器中应用程序的基本 URL 级别访问此索引路由。

索引路由自动充当每个目录的默认路由,并且可以消除命名冗余。 下面的目录结构公开了两个路由路径: //home

 next-app └── pages ├── index.js // path: base-url (/) └── home.js // path: /home

嵌套路由的消除更为明显。

嵌套路由

pages/book这样的路由是一层深度。 更深入的是创建嵌套路由,这需要嵌套的文件夹结构。 使用https://www.smashingmagazine.com的基本 URL,您可以通过创建类似于以下的文件夹结构来访问路线https://www.smashingmagazine.com/printed-books/printed-books

 next-app └── pages ├── index.js // top index route └── printed-books // nested route └── printed-books.js // path: /printed-books/printed-books

或者通过索引路径消除路径冗余,并在https://www.smashingmagazine.com/printed-books访问印刷书籍的路径。

 next-app └── pages ├── index.js // top index route └── printed-books // nested route └── index.js // path: /printed-books

动态路由在消除冗余方面也发挥着重要作用。

动态路线

在前面的示例中,我们使用索引路由访问所有印刷书籍。 要访问单个书籍,需要为每本书创建不同的路径,例如:

 // ️ Don't do this. next-app └── pages ├── index.js // top index route └── printed-books // nested route ├── index.js // path: /printed-books ├── typesript-in-50-lessons.js // path: /printed-books/typesript-in-50-lessons ├── checklist-cards.js // path: /printed-books/checklist-cards ├── ethical-design-handbook.js // path: /printed-books/ethical-design-handbook ├── inclusive-components.js // path: /printed-books/inclusive-components └── click.js // path: /printed-books/click

这是高度冗余、不可扩展的,并且可以通过动态路由进行补救,例如:

 // Do this instead. next-app └── pages ├── index.js // top index route └── printed-books ├── index.js // path: /printed-books └── [book-id].js // path: /printed-books/:book-id

括号语法 - [book-id] - 是动态段,并且不仅限于文件。 它也可以与下面示例中的文件夹一起使用,从而使作者在路径/printed-books/:book-id/author中可用。

 next-app └── pages ├── index.js // top index route └── printed-books ├── index.js // path: /printed-books └── [book-id] └── author.js // path: /printed-books/:book-id/author

路由的动态段作为查询参数公开,可以在路由中涉及的任何连接组件中使用useRouter()钩子的query对象访问该参数 - (有关此内容的更多信息,请参见下一个/路由器 API 部分)。

 // printed-books/:book-id import { useRouter } from 'next/router'; export default function Book() { const { query } = useRouter(); return ( <div> <h1> book-id <em>{query['book-id']}</em> </h1> </div> ); }
 // /printed-books/:book-id/author import { useRouter } from 'next/router'; export default function Author() { const { query } = useRouter(); return ( <div> <h1> Fetch author with book-id <em>{query['book-id']}</em> </h1> </div> ); }

使用 Catch All 路由扩展动态路由段

您已经看到了与[book-id].js相同的前一个示例中的动态路由段括号语法。 这种语法的美妙之处在于它使Catch-All Routes更进一步。 您可以从名称中推断出它的作用:它捕获所有路由。

当我们查看动态示例时,我们了解到它如何帮助消除文件创建冗余,以便通过单个路径访问具有其 ID 的多本书。 但我们还可以做一些别的事情。

具体来说,我们有路径/printed-books/:book-id ,目录结构:

 next-app └── pages ├── index.js └── printed-books ├── index.js └── [book-id].js

如果我们更新路径以包含更多的类别(如类别),我们最终可能会得到以下内容: /printed-books/design/:book-id/printed-books/engineering/:book-id ,或者更好的是/printed-books/:category/:book-id

让我们添加发行年份: /printed-books/:category/:release-year/:book-id 。 你能看到一个模式吗? 目录结构变为:

 next-app └── pages ├── index.js └── printed-books └── [category] └── [release-year] └── [book-id].js

我们用命名文件代替了动态路由,但不知何故仍然以另一种形式的冗余告终。 好吧,有一个解决方法:Catch All Routes 消除了对深度嵌套路由的需求:

 next-app └── pages ├── index.js └── printed-books └── [...slug].js

它使用相同的括号语法,只是它以三个点为前缀。 想想像 JavaScript 扩展语法这样的点。 您可能想知道:如果我使用包罗万象的路线,我如何访问类别( [category] ​​)和发布年份( [release-year] )。 两种方式:

  1. 在印刷书籍示例的情况下,最终目标是书籍,并且每个书籍信息都将附带其元数据,或者
  2. “slug”段作为查询参数数组返回。
 import { useRouter } from 'next/router'; export default function Book() { const { query } = useRouter(); // There's a brief moment where `slug` is undefined // so we use the Optional Chaining (?.) and Nullish coalescing operator (??) // to check if slug is undefined, then fall back to an empty array const [category, releaseYear, bookId] = query?.slug ?? []; return ( <table> <tbody> <tr> <th>Book Id</th> <td>{bookId}</td> </tr> <tr> <th>Category</th> <td>{category}</td> </tr> <tr> <th>Release Year</th> <td>{releaseYear}</td> </tr> </tbody> </table> ); }

以下是路线/printed-books/[…slug]的更多示例:

小路查询参数
/printed-books/click.js {“蛞蝓”:[“点击”]}
/printed-books/2020/click.js {“蛞蝓”:[“2020”,“点击”]}
/printed-books/design/2020/click.js {“蛞蝓”:[“设计”,“2020”,“点击”]}

与 catch-all 路由一样,路由/printed-books将抛出 404 错误,除非您提供备用索引路由。

 next-app └── pages ├── index.js └── printed-books ├── index.js // path: /printed-books └── [...slug].js

这是因为包罗万象的路线是“严格的”。 它要么匹配一个蛞蝓,要么抛出一个错误。 如果您想避免在 catch-all 路由旁边创建索引路由,则可以使用可选的 catch-all 路由

使用可选的 Catch-All 路由扩展动态路由段

语法与 catch-all-routes 相同,但使用双方括号。

 next-app └── pages ├── index.js └── printed-books └── [[...slug]].js

在这种情况下,包罗万象的路由(slug)是可选的,如果不可用,则回退到路径/printed-books ,使用[[…slug]].js路由处理程序呈现,没有任何查询参数。

在索引路线旁边使用 catch-all,或单独使用可选的 catch-all 路线。 避免同时使用包罗万象和可选包罗万象的路线。

路由优先级

能够定义最常见的路由模式的能力可能是“黑天鹅”。 路线冲突的可能性是一个迫在眉睫的威胁,尤其是当您开始处理动态路线时。

当这样做有意义时,Next.js 会以错误的形式让您了解路由冲突。 如果没有,它会根据路由的特殊性对路由应用优先级。

例如,在同一级别上有多个动态路由是错误的。

 // This is an error // Failed to reload dynamic routes: Error: You cannot use different slug names for the // same dynamic path ('book-id' !== 'id'). next-app └── pages ├── index.js └── printed-books ├── [book-id].js └── [id].js

如果您仔细查看下面定义的路线,您会注意到发生冲突的可能性。

 // Directory structure flattened for simplicity next-app └── pages ├── index.js // index route (also a predefined route) └── printed-books ├── index.js ├── tags.js // predefined route ├── [book-id].js // handles dynamic route └── [...slug].js // handles catch all route

例如,尝试回答这个问题:什么路由处理路径/printed-books/inclusive-components

  • /printed-books/[book-id].js ,或
  • /printed-books/[…slug].js

答案在于路由处理程序的“特异性”。 首先是预定义路由,然后是动态路由,然后是包罗万象的路由。 您可以将路由请求/处理模型视为具有以下步骤的伪代码:

  1. 是否有可以处理路由的预定义路由处理程序
    • true — 处理路由请求。
    • false — 转到 2。
  2. 是否有可以处理路由的动态路由处理程序
    • true — 处理路由请求。
    • false — 转到 3。
  3. 是否有可以处理路由的包罗万象的路由处理程序
    • true — 处理路由请求。
    • false — 抛出未找到的 404 页面。

因此, /printed-books/[book-id].js获胜。

以下是更多示例:

路线路由处理程序路线类型
/printed-books /printed-books 索引路由
/printed-books/tags /printed-books/tags.js 预定义路线
/printed-books/inclusive-components /printed-books/[book-id].js 动态路由
/printed-books/design/inclusive-components /printed-books/[...slug].js 包罗万象的路线

next/link API

next/link API 将Link组件公开为执行客户端路由转换的声明性方式。

 import Link from 'next/link' function TopNav() { return ( <nav> <Link href="/">Smashing Magazine</Link> <Link href="/articles">Articles</Link> <Link href="/guides">Guides</Link> <Link href="/printed-books">Books</Link> </nav> ) }

Link组件将解析为常规的 HTML 超链接。 也就是说, <Link href="/">Smashing Magazine</Link>将解析为<a href="/">Smashing Magazine</a>

href属性是Link组件唯一需要的属性。 有关Link组件上可用道具的完整列表,请参阅文档。

Link组件还有其他机制需要注意。

具有动态路段的路线

在 Next.js 9.5.3 之前, Link到动态路由意味着您必须同时为Link提供hrefas属性,如下所示:

 import Link from 'next/link'; const printedBooks = [ { name: 'Ethical Design', id: 'ethical-design' }, { name: 'Design Systems', id: 'design-systems' }, ]; export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href="/printed-books/[printed-book-id]" as={`/printed-books/${printedBook.id}`} > {printedBook.name} </Link> )); }

尽管这允许 Next.js 为动态参数插入 href,但它很繁琐、容易出错并且有些必要,现在随着 Next.js 10 的发布,大多数用例都得到了修复。

此修复程序也是向后兼容的。 如果您一直在使用ashref ,则没有任何问题。 要采用新语法,请丢弃href属性及其值,并将 as 属性重命名as href ,如下例所示:

 import Link from 'next/link'; const printedBooks = [ { name: 'Ethical Design', id: 'ethical-design' }, { name: 'Design Systems', id: 'design-systems' }, ]; export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href={`/printed-books/${printedBook.id}`}>{printedBook.name}</Link> )); }

请参阅自动解析 href。

passHref Prop 的用例

仔细看看下面的片段:

 import Link from 'next/link'; const printedBooks = [ { name: 'Ethical Design', id: 'ethical-design' }, { name: 'Design Systems', id: 'design-systems' }, ]; // Say this has some sort of base styling attached function CustomLink({ href, name }) { return <a href={href}>{name}</a>; } export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href={`/printed-books/${printedBook.id}`} passHref> <CustomLink name={printedBook.name} /> </Link> )); }

passHref强制Link组件将href属性向下传递给CustomLink子组件。 如果Link组件覆盖了返回超链接<a>标记的组件,则这是强制性的。 您的用例可能是因为您使用的是 styled-components 之类的库,或者您需要将多个子组件传递给Link组件,因为它只需要一个子组件。

请参阅文档以了解更多信息。

网址对象

Link组件的href属性也可以是具有query等属性的 URL 对象,该对象会自动格式化为 URL 字符串。

使用printedBooks对象,下面的示例将链接到:

  1. /printed-books/ethical-design?name=Ethical+Design and
  2. /printed-books/design-systems?name=Design+Systems
 import Link from 'next/link'; const printedBooks = [ { name: 'Ethical Design', id: 'ethical-design' }, { name: 'Design Systems', id: 'design-systems' }, ]; export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href={{ pathname: `/printed-books/${printedBook.id}`, query: { name: `${printedBook.name}` }, }} > {printedBook.name} </Link> )); }

如果在pathname中包含动态段,则还必须将其作为属性包含在查询对象中,以确保在pathname中插入查询:

 import Link from 'next/link'; const printedBooks = [ { name: 'Ethical Design', id: 'ethical-design' }, { name: 'Design Systems', id: 'design-systems' }, ]; // In this case the dynamic segment `[book-id]` in pathname // maps directly to the query param `book-id` export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href={{ pathname: `/printed-books/[book-id]`, query: { 'book-id': `${printedBook.id}` }, }} > {printedBook.name} </Link> )); }

上面的例子有路径:

  1. /printed-books/ethical-design
  2. /printed-books/design-systems

如果您检查 VSCode 中的href属性,您会发现LinkProps类型,其href属性是Url类型,如前所述,它可以是stringUrlObject

VSCode 中检查的 LinkProps 类型的屏幕截图
在 VSCode 中检查LinkProps 。 (大预览)

检查UrlObject进一步导致具有属性的接口:

在 VSCode 中检查的 <code>UrlObject</code> 的屏幕截图
在 VSCode 中检查 UrlObject。 (大预览)

您可以在 Node.js URL 模块文档中了解有关这些属性的更多信息。

哈希的一个用例是链接到页面中的特定部分。

 import Link from 'next/link'; const printedBooks = [{ name: 'Ethical Design', id: 'ethical-design' }]; export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href={{ pathname: `/printed-books/${printedBook.id}`, hash: 'faq', }} > {printedBook.name} </Link> )); }

超链接将解析为/printed-books/ethical-design#faq

在文档中了解更多信息。

next/router API

如果next/link是声明性的,那么next/router是必须的。 它公开了一个useRouter钩子,允许访问任何功能组件内的router对象。 您可以使用此挂钩手动执行路由,尤其是在某些情况下, next/link不够,或者您需要“挂钩”到路由中。

 import { useRouter } from 'next/router'; export default function Home() { const router = useRouter(); function handleClick(e) { e.preventDefault(); router.push(href); } return ( <button type="button" onClick={handleClick}>Click me</button> ) }

useRouter是一个 React 钩子,不能与类一起使用。 需要类组件中的router对象吗? withRouter使用。

 import { withRouter } from 'next/router'; function Home({router}) { function handleClick(e) { e.preventDefault(); router.push(href); } return ( <button type="button" onClick={handleClick}>Click me</button> ) } export default withRouter(Home);

router对象

useRouter钩子和withRouter高阶组件都返回一个路由器对象,该对象具有pathnamequeryasPathbasePath等属性,为您提供有关当前页面的 URL 状态的信息, localelocalesdefaultLocale提供有关活动的、受支持的或当前的默认语言环境。

路由器对象还具有诸如push等方法,用于通过将新 URL 条目添加到历史堆栈中来导航到新 URL, replace类似于 push ,但替换当前 URL 而不是将新 URL 条目添加到历史堆栈中。

了解有关路由器对象的更多信息。

使用next.config.js自定义路由配置

这是一个常规的 Node.js 模块,可用于配置某些 Next.js 行为。

 module.exports = { // configuration options }

请记住在更新next.config.js时重新启动服务器。 了解更多。

基本路径

有人提到 Next.js 中的初始/默认路由是pages/index.js和路径/ 。 这是可配置的,您可以将默认路由设置为域的子路径。

 module.exports = { // old default path: / // new default path: /dashboard basePath: '/dashboard', };

这些更改将在您的应用程序中自动生效,所有/路径都路由到/dashboard

此功能只能与 Next.js 9.5 及更高版本一起使用。 了解更多。

尾随斜线

默认情况下,每个 URL 的末尾都没有尾部斜杠。 但是,您可以使用以下命令进行切换:

 module.exports = { trailingSlash: true };
 # trailingSlash: false /printed-books/ethical-design#faq # trailingSlash: true /printed-books/ethical-design/#faq

基本路径和斜杠功能都只能用于 Next.js 9.5 及更高版本。

结论

路由是 Next.js 应用程序中最重要的部分之一,它体现在基于页面概念的基于文件系统的路由中。 页面可用于定义最常见的路由模式。 路由和渲染的概念密切相关。 在您构建自己的 Next.js 应用程序或处理 Next.js 代码库时,学习本文的课程。 并查看以下资源以了解更多信息。

相关资源

  • 页面的 Next.js 官方文档
  • Next.js 获取数据的官方文档
  • 用于 next.config.js 的 Next.js 官方文档
  • Next.js 10:自动解析href
  • Next/link 的 Next.js 官方文档
  • Next/router 的 Next.js 官方文档