自网络诞生以来,超链接一直是网络的瑰宝之一。根据MDN的说法,超链接是使网络成为网络的原因*。*虽然用于文件之间的链接等目的,但其主要用途是引用不同的网页,可通过一个独特的网址或URL来识别。
路由是每个网络应用的一个重要方面,就像超链接对网络一样。它是一种机制,通过它将请求路由到处理它们的代码。在路由方面,Next.js页面是通过一个独特的URL路径来引用和识别的。如果网络是由超链接相互连接的导航 网页组成的,那么每个Next.js应用程序是由路由器相互连接的可路由的页面(路由处理程序或路由)组成的。
Next.js内置了对路由的支持,这可能会让人难以理解,尤其是在考虑渲染和数据获取的时候。作为理解Next.js中客户端路由的前提,有必要对Next.js中的路由、渲染和数据获取等概念有一个概述。
这篇文章对熟悉Next.js并想了解它如何处理路由的React开发者来说是有益的。你需要对React和Next.js有一定的了解,才能从这篇文章中获得最大的收获,这篇文章只涉及客户端路由和Next.js的相关概念。
路由和渲染
路由和渲染是相辅相成的,在本文中会起到很大的作用。我喜欢[Gaurav对]它们的[解释]。
路由是指用户被导航到网站上不同页面的过程。
渲染是将这些页面放在用户界面上的过程。每次你请求路由到一个特定的页面时,你也在渲染该页面,但不是每次渲染都是路由的结果。
关于Next.js的渲染,你需要了解的是,每一个页面都是提前预渲染的,同时还需要最小的JavaScript代码,以便通过一个被称为 "水合 "的过程实现完全交互。Next.js如何做到这一点,在很大程度上取决于预渲染的形式。静态生成或服务器端渲染,它们都与所使用的数据获取技术高度耦合,并与页面的HTML生成时间分开。
根据你的数据获取要求,你可能会发现自己在使用内置的数据获取功能,如getStaticProps,getStaticPaths, 或getServerSideProps, 客户端数据获取工具,如SWR, react-query, 或传统的数据获取方法,如fetch-on-render,fetch-then-render,render-as-you-fetch(withSuspense) 。
预渲染(在渲染之前--到用户界面)是对Routing的补充,并且与数据获取高度耦合--在Next.js中是一个完整的主题。因此,虽然这些概念要么是互补的,要么是密切相关的,但本文将只关注页面之间的单纯导航(Routing),并在必要时引用相关概念。
说完这些,让我们从基本要点开始。Next.js有一个基于文件系统的路由器,建立在页面的概念上。
页面
Next.js中的页面是React组件,可自动作为路由使用。它们作为默认导出文件从pages目录中导出,支持的文件扩展名为.js 、.jsx 、.ts 、.tsx 。
一个典型的Next.js应用程序将有一个文件夹结构,其中有顶级目录,如页面、公共和样式。
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 Books
}
注意:请记住,页面也可以被称为 "路由处理程序"。
自定义页面
这些是驻留在pages目录中的特殊页面,但不参与路由。它们的前缀是下划线符号,如:_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 ,它自动作为你的应用程序的起点,即/ 。由于基本的URL是localhost:3000 ,这个索引路由可以在浏览器中的应用程序的基本URL级别被访问。
索引路由自动作为每个目录的默认路由,可以消除命名上的冗余。下面的目录结构暴露了两个路由路径:/ 和/home 。
next-app
└── pages
├── index.js // path: base-url (/)
└── home.js // path: /home
嵌套路由的消除作用更为明显。
嵌套路由
像pages/book 这样的路由是一个级别的深度。更深入的是创建嵌套路由,这需要一个嵌套的文件夹结构。有了一个https://www.smashingmagazine.com 的base-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 Routes扩展动态路由段
你已经看到了动态路由段的括号语法,如前面的例子中的[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的扩散语法。你可能想知道。如果我使用catch-all路线,我如何访问类别([category]),和发布年份([release-year])。有两种方法。
- 在印刷书的例子中,最终的目标是书,每本书的信息都会有它的元数据附加,或者
- 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 | { "slug":["click"] } |
/printed-books/2020/click.js | { "slug":["2020", "点击" ] } |
/printed-books/design/2020/click.js | { "slug":["设计", "2020", "点击" ] } |
与catch-all路由一样,除非你提供一个后备索引路由,否则路由/printed-books ,会抛出404错误。
next-app
└── pages
├── index.js
└── printed-books
├── index.js // path: /printed-books
└── [...slug].js
这是因为catch-all路由是 "严格 "的。它要么匹配一个slug,要么抛出一个错误。如果你想避免在catch-all路由旁边创建索引路由,你可以使用可选的catch-all路由代替。
用可选的catch-all路由来扩展动态路由段
语法与catch-all-routes相同,但用双方括号代替。
next-app
└── pages
├── index.js
└── printed-books
└── [[...slug]].js
在这种情况下,catch-all路由(slug)是可选的,如果不可用,则回落到路径/printed-books ,用[[…slug]].js 路由处理程序渲染,没有任何查询参数。
将catch-all与索引路由一起使用,或者单独使用可选的catch-all路由。避免同时使用 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.
答案就在于路由处理程序的 "特殊性"。预定义路由排在第一位,其次是动态路由,然后是全能路由。你可以把路由请求/处理模型看作是一个伪代码,有以下步骤。
- 是否有一个预定义的路由处理程序可以处理路由?
true- 处理路由请求。false- 转到2。
- 是否有一个动态路由处理程序可以处理该路由?
true- 处理路由请求。false- 转到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之前,Linking到动态路由意味着你必须提供href 和as 两个道具给Link 作为in。
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的发布,对大多数的使用情况都进行了修复。
这个修正也是向后兼容的。如果你一直在使用as 和href ,就不会有任何问题。要采用新的语法,请丢弃href 这个道具及其值,并将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>
));
}
关于passHref 的用例
仔细看一下下面的片段。
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 组件传递多个子组件,因为它只期望有一个子组件。
URL对象
Link 组件的href 托词也可以是一个 URL 对象,其属性与query 一样,被自动格式化为一个 URL 字符串。
使用printedBooks 对象,下面的例子将链接到。
/printed-books/ethical-design?name=Ethical+Design和/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>
));
}
上面的例子有路径。
/printed-books/ethical-design, 和/printed-books/design-systems.
如果你在VSCode中检查href 属性,你会发现LinkProps 的类型,href 属性是Url 的类型,如前所述,它是string 或UrlObject 。

进一步检查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 高阶组件,都返回一个路由器对象,其属性如pathname,query,asPath, 和basePath ,给你关于当前页面的URL状态的信息,locale,locales, 和defaultLocale ,给你关于活动的、支持的或当前默认的语言的信息。
路由器对象也有一些方法,如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应用程序中最重要的部分之一,它反映在基于文件系统的路由器中,该路由器建立在页面的概念上。页面可以用来定义最常见的路由模式。路由和渲染的概念是密切相关的。当你建立自己的Next.js应用或在Next.js代码库上工作时,请带着本文的教训。