Маршрутизация на стороне клиента в Next.js
Опубликовано: 2022-03-10Гиперссылки были одной из жемчужин Сети с момента ее создания. Согласно MDN, гиперссылки — это то, что делает Сеть паутиной. Несмотря на то, что он используется для таких целей, как связывание между документами, его основное применение заключается в ссылке на различные веб-страницы, идентифицируемые по уникальному веб-адресу или URL-адресу.
Маршрутизация является таким же важным аспектом каждого веб-приложения, как и гиперссылки на Интернет. Это механизм, с помощью которого запросы перенаправляются в код, который их обрабатывает. Что касается маршрутизации, на страницы Next.js можно ссылаться и идентифицировать их по уникальному URL-адресу. Если Интернет состоит из навигационных веб-страниц , соединенных между собой гиперссылками , то каждое приложение Next.js состоит из страниц с возможностью маршрутизации (обработчиков маршрутов или маршрутов), соединенных маршрутизатором.
Next.js имеет встроенную поддержку маршрутизации, которая может быть громоздкой для распаковки, особенно при рассмотрении рендеринга и выборки данных. В качестве предварительного условия для понимания маршрутизации на стороне клиента в Next.js необходимо иметь обзор таких концепций, как маршрутизация, рендеринг и выборка данных в Next.js.
Эта статья будет полезна разработчикам React, которые знакомы с Next.js и хотят узнать, как он обрабатывает маршрутизацию. Вам необходимо иметь практические знания о React и Next.js, чтобы получить максимальную отдачу от статьи, которая посвящена исключительно маршрутизации на стороне клиента и связанным с ней концепциям в Next.js.
Маршрутизация и рендеринг
Маршрутизация и рендеринг дополняют друг друга и будут играть огромную роль в ходе этой статьи. Мне нравится, как Гаурав их объясняет:
Маршрутизация — это процесс, посредством которого пользователь переходит на разные страницы веб-сайта.
Рендеринг — это процесс размещения этих страниц в пользовательском интерфейсе. Каждый раз, когда вы запрашиваете маршрут к определенной странице, вы также визуализируете эту страницу, но не каждый рендеринг является результатом маршрута.
Потратьте пять минут, чтобы подумать об этом.
Что вам нужно знать об рендеринге в Next.js, так это то, что каждая страница предварительно визуализируется вместе с минимальным кодом JavaScript, необходимым для того, чтобы она стала полностью интерактивной благодаря процессу, известному как гидратация. То, как это делает Next.js, сильно зависит от формы предварительного рендеринга: статическая генерация или рендеринг на стороне сервера , которые тесно связаны с используемой техникой выборки данных и разделены временем генерации HTML для страницы.
В зависимости от ваших требований к выборке данных, вы можете использовать встроенные функции выборки данных, такие как getStaticProps
, getStaticPaths
или getServerSideProps
, инструменты выборки данных на стороне клиента, такие как SWR, response-query, или традиционные подходы к выборке данных, такие как выборка по запросу. рендеринг, выборка-затем-рендеринг, рендеринг-по мере извлечения (с задержкой).
Предварительный рендеринг (перед рендерингом — в UI ) дополняет роутинг и тесно связан с выборкой данных — целая отдельная тема в Next.js. Таким образом, несмотря на то, что эти концепции либо дополняют друг друга, либо тесно связаны, эта статья будет сосредоточена исключительно на простой навигации между страницами (маршрутизации) со ссылками на связанные концепции, где это необходимо.
Оставив это в стороне, давайте начнем с фундаментальной сути: Next.js имеет маршрутизатор на основе файловой системы, построенный на концепции страниц.
Страницы
Страницы в Next.js — это компоненты React, которые автоматически доступны как маршруты. Они экспортируются как экспорт по умолчанию из каталога страниц с поддерживаемыми расширениями файлов, такими как .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
Книги
}
Примечание . Имейте в виду, что страницы также могут называться «обработчиками маршрутов».
Пользовательские страницы
Это специальные страницы, которые находятся в каталоге 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 предоставляет компонент Link
из API next/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
, имеет один уровень глубины. Чтобы углубиться, нужно создать вложенные маршруты, для чего требуется структура вложенных папок. С базовым URL-адресом https://www.smashingmagazine.com
вы можете получить доступ к маршруту 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
Динамический сегмент(ы) маршрута представлен как параметр запроса, к которому можно получить доступ в любом компоненте подключения, участвующем в маршруте, с объектом query
useRouter()
(подробнее об этом в следующем разделе 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 он идет еще дальше. Вы можете сделать вывод, что это делает из названия: он перехватывает все маршруты.
Когда мы рассмотрели динамический пример, мы узнали, как он помогает устранить избыточность создания файлов для одного маршрута для доступа к нескольким книгам с их идентификатором. Но есть кое-что еще, что мы могли бы сделать.
В частности, у нас был путь /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]
). Два пути:
- В случае примера с печатными книгами конечной целью является книга, и к каждой информации о книге будут прикреплены свои метаданные или
- Сегменты «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», «клик»] } |
Как и в случае с универсальным маршрутом, маршрут /printed-books
выдаст ошибку 404, если вы не предоставите резервный индексный маршрут.
next-app └── pages ├── index.js └── printed-books ├── index.js // path: /printed-books └── [...slug].js
Это связано с тем, что универсальный маршрут является «строгим». Он либо соответствует слагу, либо выдает ошибку. Если вы хотите избежать создания индексных маршрутов наряду с универсальными маршрутами, вы можете вместо этого использовать необязательные универсальные маршруты .
Расширение сегментов динамического маршрута с помощью дополнительных маршрутов Catch-All
Синтаксис такой же, как у всех маршрутов, но вместо этого используются двойные квадратные скобки.
next-app └── pages ├── index.js └── printed-books └── [[...slug]].js
В этом случае универсальный маршрут (slug) является необязательным и, если недоступен, откат к пути /printed-books
, отображаемому с помощью обработчика маршрута [[…slug]].js
без каких-либо параметров запроса.
Используйте всеобъемлющие маршруты вместе с индексными маршрутами или необязательные всеобъемлющие маршруты отдельно. Избегайте одновременного использования универсальных и необязательных универсальных маршрутов.
Приоритет маршрутов
Возможность определения наиболее распространенных шаблонов маршрутизации может оказаться «черным лебедем». Возможность столкновения маршрутов является надвигающейся угрозой, особенно когда вы начинаете работать с динамическими маршрутами.
Когда это имеет смысл, 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 | Универсальный маршрут |
API next/link
API next/link
предоставляет компонент 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
к динамическим маршрутам означала, что вы должны были предоставить как href
, так и as
prop для Link
, как в следующем примере:
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> )); }
См. Автоматическое разрешение 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
, поскольку он ожидает только одного дочернего элемента.
См. документы, чтобы узнать больше.
URL-объекты
Свойство href
компонента Link
также может быть объектом 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
.
Если вы проверите атрибут href
в VSCode, вы обнаружите тип 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
.
Узнайте больше в документах.
API next/router
Если 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 9.5 и более поздних версиях.
Заключение
Маршрутизация — одна из наиболее важных частей вашего приложения Next.js, и она отражается в маршрутизаторе на основе файловой системы, построенном на концепции страниц. Страницы можно использовать для определения наиболее распространенных шаблонов маршрутов. Понятия маршрутизации и рендеринга тесно связаны. Используйте уроки из этой статьи, когда будете создавать собственное приложение Next.js или работать над кодовой базой Next.js. И проверьте ресурсы ниже, чтобы узнать больше.
Связанные ресурсы
- Официальная документация Next.js для страниц
- Официальная документация Next.js для получения данных
- Официальная документация Next.js для next.config.js
- Next.js 10: Автоматическое разрешение
href
- Официальная документация Next.js для next/link
- Официальная документация Next.js для next/router