Руководство по Next.js. 1/3
- среда, 17 апреля 2024 г. в 00:00:11
Hello world!
Представляю вашему вниманию первую часть обновленного руководства по Next.js.
На мой взгляд, Next.js — это лучший на сегодняшний день инструмент для разработки веб-приложений.
Предполагается, что вы хорошо знаете JavaScript и React, а также хотя бы поверхностно знакомы с Node.js.
Обратите внимание: руководство актуально для Next.js версии 14.
При подготовке руководства я опирался в основном на официальную документацию, но в "отсебятине" мог и приврать (или просто очепятаться) 😁 При обнаружении подобного не стесняйтесь писать в личку 😉
Парочка полезных ссылок:
Что такое Next.js?
Next.js — это фреймворк React для создания клиент-серверных (fullstack) веб-приложений. Мы используем компоненты React для разработки UI (user interface — пользовательский интерфейс) и Next.js для дополнительных возможностей и оптимизаций.
Под капотом Next.js также абстрагирует и автоматически настраивает инструменты, необходимые React, такие как сборка, компиляция и др. Это позволяет сосредоточиться на разработке приложения вместо того, чтобы тратить время на настройку этих инструментов.
Next.js помогает разрабатывать интерактивные, динамичные и быстрые приложения React.
Основные возможности
Некоторые из основных возможностей, предоставляемых Next.js:
Возможность | Описание |
---|---|
Маршрутизация (далее также — роутинг) | Основанный на файловой системе маршрутизатор (далее также — роутер), разработанный на основе серверных компонентов, поддерживающий макеты (layouts), вложенный роутинг, состояние загрузки, обработку ошибок и др. |
Рендеринг | Клиентский и серверный рендеринг с помощью соответствующих компонентов. Возможность дальнейшей оптимизации с помощью статического и динамического рендеринга на сервере за счет граничной (edge) потоковой передачи Next.js и среды выполнения Node.js |
Запрос/получение данных | Упрощенное получение данных с помощью async/await в серверных компонентах. Расширенный Fetch API для мемоизации запросов, кеширования и ревалидации данных |
Стилизация | Поддержка разных способов стилизации, включая модули CSS, TailwindCSS и CSS-в-JS |
Оптимизации | Оптимизация изображений, шрифтов и скриптов для улучшения показателей Core Web Vitals приложения и UX (user experience — пользовательский опыт) |
TypeScript | Улучшенная поддержка TS с лучшей проверкой типов и более эффективной компиляцией, а также кастомный плагин TS и средство проверки типов |
Установка
Требования к системе:
Для создания нового проекта рекомендуется использовать CLI (command line interface — интерфейс командной строки) create-next-app
:
npx create-next-app@latest
Структура проекта Next.js
Директории верхнего уровня
Директории верхнего уровня предназначены для организации кода приложения и статических ресурсов.
Директория | Назначение |
---|---|
app | Роутер приложения |
pages | Роутер страниц |
public | Статические файлы |
src | Опциональная директория кода приложения |
Файлы верхнего уровня
Файлы верхнего уровня используются для настройки приложения, управления зависимостями, запуска посредников (middleware — промежуточное программное обеспечение), интеграции инструментов мониторинга и определения переменных окружения.
Файл | Назначение |
---|---|
next.config.js | Настройки Next.js |
package.json | Зависимости и скрипты проекта |
instrumentation.ts | Телеметрия |
middleware.ts | Посредники |
.env | Переменные окружения |
.env.local | Переменные локального окружения |
.env.production | Переменные производственного окружения |
.env.development | Переменные рабочего окружения |
.eslintrc.json | Настройки ESLint |
.gitignore | Файлы и директории, игнорируемые [Git]() |
next-env.d.ts | Файл определений типов Next.js |
tsconfig.json | Настройки TypeScript |
jsconfig.json | Настройки JavaScript |
Соглашения о файлах директории app
В роутере приложения используются следующие соглашения для определения роутов и обработки метаданных:
Файлы роутов
Файл | Расширение | Назначение |
---|---|---|
layout | .js .jsx .tsx | Макет |
page | .js .jsx .tsx | Страница |
loading | .js .jsx .tsx | UI загрузки |
not-found | .js .jsx .tsx | UI отсутствующей страницы |
error | .js .jsx .tsx | UI ошибки |
global-error | .js .jsx .tsx | UI глобальной ошибки |
route | .js .ts | Конечная точка API |
template | .js .jsx .tsx | Повторно используемый макет |
default | .js .jsx .tsx | Резервная страница параллельных роутов |
Вложенные роуты
Название | Назначение |
---|---|
folder |
Сегмент роута |
folder/folder |
Вложенный сегмент роута |
Динамические роуты
Название | Назначение |
---|---|
[folder] |
Сегмент динамического роута |
[...folder] |
Сегмент роута-перехватчика |
[[..folder]] |
Сегмент опционального роута-перехватчика (catch-all route) |
Группы роутов и закрытые директории
Название | Назначение |
---|---|
(folder) |
Группировка роутов без влияния на роутинг (вид URL (uniform resource locator — единообразный указатель местонахождения ресурса)) |
_folder |
Опциональная директория, потомки которой не влияют на роутинг |
Параллельные и перехваченные роуты
Название | Назначение |
---|---|
@folder |
Именованный слот |
(.)folder |
Перехват роута того же уровня |
(..)folder |
Перехват роута верхнего уровня |
(..)(..)folder |
Перехват роута на два уровня выше |
(...)folder |
Перехват роута корневого уровня |
Соглашения о файлах с метаданными
Иконки приложения
Файл | Расширение | Назначение |
---|---|---|
favicon | .ico | Фавиконка |
icon | .ico .jpg .jpeg .png .svg | Иконка приложения |
icon | .js .ts .tsx | Генерируемая иконка приложения |
apple-icon | .jpg .jpeg, .png | Иконка приложения Apple |
apple-icon | .js .ts .tsx | Генерируемая иконка приложения Apple |
Изображения Open Graph и Twitter
Файл | Расширение | Назначение |
---|---|---|
opengraph-image | .jpg .jpeg .png .gif | Изображение Open Graph |
opengraph-image | .js .ts .tsx | Генерируемое изображение Open Graph |
twitter-image | .jpg .jpeg .png .gif | Изображение Twitter |
twitter-image | .js .ts .tsx | Генерируемое изображение Twitter |
SEO (search engine optimization — оптимизация движка поиска)
Файл | Расширение | Назначение |
---|---|---|
sitemap | .xml | Sitemap |
sitemap | .js .ts | Генерируемый Sitemap |
robots | .txt | Robots |
robots | .js .ts | Генерируемый Robots |
Терминология
Роутер app
В версии 13 Next.js представил новый роутер приложения, разработанный на основе серверных компонентов React, поддерживающий общие (shared) макеты, вложенный роутинг, состояние загрузки, обработку ошибок и др.
Роуты приложения теперь находятся в директории app
. Раньше такой директорией являлась pages
. Эти директории могут существовать совместно, например, во время перехода с pages
на app
(или до того, как это сделают библиотеки экосистемы Next.js), но app
имеет приоритет.
По умолчанию компоненты внутри app
являются серверными. Существуют также клиентские компоненты. Мы поговорим об этом позже.
Роли директорий и файлов
В Next.js используется роутинг на основе файловой системы, где:
page.js
Сегменты роута
Каждая директория в роуте представляет его сегмент. Каждый сегмент роута связан с соответствующим сегментом URL.
Вложенные роуты
Для создания вложенных роутов директории вкладываются друг в друга. Например, мы можем добавить новый роут /dashboard/settings
путем вложения двух новых директорий в директорию app
.
Роут /dashboard/settings
состоит из трех сегментов:
/
(корневой сегмент)dashboard
(сегмент)settings
(листовой сегмент)Соглашения о файлах
Next.js предоставляет специальные файлы для создания UI с определенным поведением во вложенных роутах:
Название | Назначение |
---|---|
layout | Общий UI для сегмента и его потомков |
page | Уникальный UI роута. Делает роут открытым (публично доступным) |
loading | UI загрузки для сегмента и его потомков |
not-found | UI отсутствующей страницы для сегмента и его потомков |
error | UI ошибки для сегмента и его потомков |
global-error | UI глобальной ошибки |
route | Серверная конечная точка API |
template | Специальный повторно используемый UI макета |
default | Резервный UI для параллельных роутов |
Для специальных файлов могут использоваться расширения .js,
.jsx
или .tsx
.
Иерархия компонентов
Компоненты, определенные в специальных файлах сегмента роута, рендерятся в следующем порядке:
-layout.js
-template.js
-error.js
(React error boundary (предохранитель))
-loading.js
(React suspense boundary (для ленивой загрузка, частичного рендеринга и др.))
-not-found.js
(предохранитель)
-page.js
или вложенный layout.js
Во вложенном роуте компоненты сегмента будет вложенными внутри компонентов их родительского сегмента.
Совместное размещение
В дополнение к специальным, мы можем добавлять собственные файлы (компоненты, стили, тесты и др.) в директории app
.
Это возможно благодаря тому, что публично доступным является только содержимое файлов page.js
и route.js
.
Продвинутые паттерны роутинга
Роутер приложения также позволяет реализовывать более продвинутые паттерны роутинга:
Эти паттерны позволяют разрабатывать более богатые и сложные UI.
Создание роута
В Next.js для определения роутов используются директории.
Каждая директория представляет сегмент роута, связанный с сегментом URL. Для создания вложенного роута директории вкладываются друг в друга.
Для того, чтобы сделать сегмент роута доступным публично, используется специальный файл page.js
.
В этом примере URL /dashboard/analytics
не доступен публично, поскольку в соответствующей директории нет файла page.js
. Эта директория может использоваться для хранения компонентов, таблиц стилей, изображений и других файлов.
Создание UI
Для создания UI для сегмента роута используются специальные файлы. Самыми распространенными являются страницы для отображения уникального UI роута и макеты для отображения UI, который является общим для нескольких роутов.
Например, для создания первой страницы добавьте файл page.js
в директорию app
и экспортируйте по умолчанию какой-нибудь компонент:
// app/page.tsx
export default function Page() {
return <h1>Привет, Next.js!</h1>
}
Страницы
Страница — это UI, который является уникальным для роута. Страница представляет собой экспортируемый по умолчанию из файла page.js
компонент.
Например, для создания страницы index
добавьте файл page.js
в директорию app
:
// `app/page.tsx` - это UI для URL `/`
export default function Page() {
return <h1>Главная страница</h1>
}
Макеты
Макет — это UI, который является общим для нескольких роутов. При навигации (переходах между страницами) состояние макета сохраняется, он остается интерактивным и не рендерится повторно. Макеты также могут быть вложенными.
Макет представляет собой компонент, экспортируемый по умолчанию из файла layout.js
. Этот компонент должен принимать проп children
, который заполняется (populate) дочерним макетом (при наличии) или страницей в процессе рендеринга.
Например, следующий макет будет общим для страниц /dashboard
и /dashboard/settings
:
// app/page.tsx
export default function DashboardLayout({
children, // страница или вложенный макет
}: {
children: React.ReactNode
}) {
return (
<section>
{/* Общий UI, например, шапка или боковая панель */}
<nav></nav>
{children}
</section>
)
}
Корневой макет (обязательный)
Корневой макет определяется на верхнем уровне директории app
и применяется ко всем роутам приложения. Этот макет является обязательным и должен содержать теги html
и body
. Он позволяет модифицировать начальный HTML, возвращаемый сервером.
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{/* UI макета */}
<main>{children}</main>
</body>
</html>
)
}
Вложенные макеты
По умолчанию макеты в иерархии директорий являются вложенными. Это означает, что они оборачивают дочерние макеты в проп children
. Макеты могу вкладываться друг в друга путем добавления layout.js
в определенные сегменты роута (директории).
Например, для создания макета роута /dashboard
добавьте новый файл layout.js
в директорию dashboard
:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return <section>{children}</section>
}
При комбинации этих двух макетов, корневой макет (app/layout.js
) будет оборачивать макет панели управления (app/dashboard/layout.js
), который будет оборачивать сегменты роута внутри app/dashboard/*
.
Два макета буду вложены друг в друга следующим образом:
Шаблоны
Шаблоны похожи на макеты в том, что они оборачивают каждый дочерний макет или страницу. В отличие от макетов, которые сохраняются между роутами и поддерживают состояние, шаблоны создают новый экземпляр для каждого потомка при навигации. Это означает, что когда пользователь перемещается между роутами, которые делят шаблон, монтируется новый экземпляр компонента, элементы DOM (document object model — объектная модель документа) создаются заново, состояние сбрасывается и эффекты заново синхронизируются.
В некоторых случаях шаблоны предпочтительнее макетов:
useEffect
(например, логирование показов страницы) и useState
(например, постраничная форма обратной связи)Шаблон определяется путем дефолтного экспорта компонента из файла template.js
. Этот компонент должен принимать проп children
.
// app/template.tsx
export default function Template({ children }: { children: React.ReactNode }) {
return <div>{children}</div>
}
С точки зрения вложенности, шаблон рендерится между макетом и его потомками. Упрощенно это выглядит так:
<Layout>
{/* Обратите внимание, что шаблон имеет уникальный ключ */}
<Template key={routeParam}>{children}</Template>
</Layout>
Метаданные
Metadata API
позволяет модифицировать элементы head
, такие как title
и meta
, в директории app
.
Метаданные могут определяться путем экспорта объекта metadata
или функции generateMetadata
в файлах layout.js
или page.js
.
// app/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Next.js',
}
export default function Page() {
return '...'
}
Существует 4 способа навигации между роутами:
Link
useRouter
(клиентские компоненты)redirect
(серверные компоненты)History API
Компонент Link
Link
— это встроенный компонент, расширяющий HTML-элемент a
для предоставления предварительного получения данных (prefetching) и клиентской навигации между роутами. Это основной и рекомендуемый способ навигации между роутами в Next.js.
Этот компонент импортируется из next/link
и принимает проп href
:
// app/page.tsx
import Link from 'next/link'
export default function Page() {
return <Link href="/dashboard">Панель управления</Link>
}
Примеры
Ссылка на динамические сегменты
При ссылке на динамические сегменты можно использовать шаблонные литералы и интерполяцию для генерации списка ссылок. Пример генерации списка постов блога:
// app/blog/PostList.js
import Link from 'next/link'
export default function PostList({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
)
}
Проверка активных ссылок
Для определения активности ссылки можно использовать хук usePathname
. Например, для добавления класса к активной ссылке можно проверять совпадение pathname
со значением пропа href
ссылки:
// app/components/links.tsx
'use client'
import { usePathname } from 'next/navigation'
import Link from 'next/link'
export function Links() {
const pathname = usePathname()
return (
<nav>
<ul>
<li>
<Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
Главная
</Link>
</li>
<li>
<Link
className={`link ${pathname === '/about' ? 'active' : ''}`}
href="/about"
>
Контакты
</Link>
</li>
</ul>
</nav>
)
}
Прокрутка к id
Дефолтный поведением роутера приложения является прокрутка в начало новой страницы или сохранение положения прокрутки при навигации вперед-назад.
Для прокрутки к определенному id
при навигации можно добавить хэш (#
) к URL или передать хэш в проп href
. Это возможно благодаря тому, что компонент Link
рендерится в элемент a
.
<Link href="/dashboard#settings">Настройки</Link>
// Результат
<a href="/dashboard#settings">Настройки</a>
Отключение восстановления прокрутки
Для отключения дефолтного поведения прокрутки можно передать scroll={false}
в компонент Link
или scroll: false
в методы router.push
или router.replace
.
// next/link
<Link href="/dashboard" scroll={false}>
Панель управления
</Link>
// useRouter
import { useRouter } from 'next/navigation'
const router = useRouter()
router.push('/dashboard', { scroll: false })
Хук useRouter
Хук useRouter
позволяет программно менять роуты в клиентских компонентах:
// app/page.tsx
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Панель управления
</button>
)
}
Функция redirect
В серверных компонентах вместо хука useRouter
следует использовать функцию redirect
для программной навигации между роутами:
// app/team/[id]/page.tsx
import { redirect } from 'next/navigation'
async function fetchTeam(id: string) {
const res = await fetch('https://...')
if (!res.ok) return undefined
return res.json()
}
export default async function Profile({ params }: { params: { id: string } }) {
const team = await fetchTeam(params.id)
if (!team) {
redirect('/login')
}
// ...
}
Нативный History API
Next.js позволяет использовать нативные методы window.history.pushState и window.history.replaceState для обновления стека истории браузера без перезагрузки страницы.
Вызовы методов pushState
и replaceState
интегрированы в роутер Next.js, что позволяет выполнять синхронизацию с хуками usePathname
и useSearchParams
.
window.history.pushState
Этот метод позволяет добавлять новую сущность в стек истории браузера. Пользователь может возвращаться к предыдущему состоянию. Пример сортировки списка товаров:
'use client'
import { useSearchParams } from 'next/navigation'
export default function SortProducts() {
const searchParams = useSearchParams()
function updateSorting(sortOrder: string) {
const params = new URLSearchParams(searchParams.toString())
params.set('sort', sortOrder)
window.history.pushState(null, '', `?${params.toString()}`)
}
return (
<>
<button onClick={() => updateSorting('asc')}>Сортировать по убыванию</button>
<button onClick={() => updateSorting('desc')}>Сортировать по возрастанию</button>
</>
)
}
window.history.replaceState
Этот метод позволяет заменять текущую сущность в стеке истории браузера. Пользователь не может возвращаться к предыдущему состоянию. Пример переключения языка приложения:
'use client'
import { usePathname } from 'next/navigation'
export function LocaleSwitcher() {
const pathname = usePathname()
function switchLocale(locale: string) {
// Например, '/en/about' или '/fr/contact'
const newPath = `/${locale}${pathname}`
window.history.replaceState(null, '', newPath)
}
return (
<>
<button onClick={() => switchLocale('en')}>Английский</button>
<button onClick={() => switchLocale('fr')}>Французский</button>
</>
)
}
Как работают роутинг и навигация?
Роутер приложения использует гибридный подход для роутинга и навигации. На сервере код приложения автоматически разделяется на части (code splitting) по сегментам роута. На клиенте Next.js предварительно получает данные (prefetching) и кеширует сегменты роута. Это означает, что когда пользователь переходит к новому роуту, браузер не перезагружает страницу, и только изменившиеся сегменты роута рендерятся повторно — это улучшает опыт навигации и производительность.
1. Разделение кода
Разделение кода позволяет разделить код приложения на небольшие части (chunks) для загрузки и выполнения браузером. Это уменьшает количество передаваемых данных и время выполнения каждого запроса, что улучшает производительность.
Серверные компоненты позволяют автоматически разделять код по сегментам роута. Это означает, что при навигации загружается только код, необходимый для текущего роута.
2. Предварительное получение данных
Предварительное получение данных — это способ предварительной загрузки данных роута в фоновом режиме перед посещением роута пользователем.
Существует два способа предварительного получения данных роута:
Link
— данные роута автоматически запрашиваются при попадании ссылки в область видимости. Это происходит при загрузке страницы или во время прокруткиrouter.prefetch
— для программного предварительного получения данных роута может использоваться роутер, возвращаемый хуком useRouter
Поведение Link
в части предварительного получения данных различается для статических и динамических роутов:
prefetch
по умолчанию является true
. Весь роут предварительно запрашивается и кешируетсяloading.js
предварительно запрашивается и кешируется на 30 секунд
. Это уменьшает цену запроса всего динамического роута и позволяет незамедлительно отображать состояние загрузки для лучшего визуального отклика на действия пользователейПредварительное получение данных можно отключить путем установки prefetch
в значение false
.
3. Кеширование
Next.js использует клиентский кеш в памяти, который называется кешем роутера (router cache). При навигации пользователя по приложению полезная нагрузка серверных компонентов, предварительно запрошенных сегментов роута и посещенные роуты записываются в кеш.
Это означает максимальное использование кеша при навигации вместо отправки запросов на сервер — улучшение производительности путем уменьшения количества запросов и передаваемых между клиентом и сервером данных.
4. Частичный рендеринг
Частичный рендеринг означает, что при навигации повторно рендерятся только сегменты роута, которые изменились, а любые общие сегменты сохраняются.
Например, при навигации между двумя соседними роутами, /dashboard/settings
и /dashboard/analytics
, будут отрендерены страницы settings
и analytics
, а общий макет dashboard
будет сохранен.
Без частичного рендеринга каждая навигация будет приводить к повторному рендерингу всей страницы на клиенте. Рендеринг только изменившихся сегментов уменьшает количество передаваемых данных и время выполнения, что приводит к улучшению производительности.
5. Мягкая навигация
Браузеры выполняют "жесткую навигацию" (hard navigation) при переключении между страницами. Роутер приложения Next.js выполняет "мягкую навигацию" (soft navigation) между страницами, обеспечивая повторный рендеринг только изменившихся сегментов роута (частичный рендеринг). Это позволяет сохранять клиентское состояние в процессе навигации.
6. Навигация вперед-назад
По умолчанию Next.js сохраняет положение прокрутки для навигации вперед-назад и повторно использует сегменты роута из кеша роутера.
Специальный файл loading.js
помогает создавать осмысленный UI загрузки с помощью компонента Suspense
. Он позволяет мгновенно отображать состояние загрузки во время получения содержимого сегмента роута. Новое содержимое автоматически добавляется после завершения рендеринга.
Мгновенное состояние загрузки
Мгновенное состояние загрузки — это резервный UI, который отображается сразу после навигации. Мы можем предварительно рендерить индикаторы загрузки, такие как скелеты и спинеры или небольшие части будущего экрана, такие как обложка, заголовок и др. Это помогает пользователю понять, что приложение работает и обеспечивает лучший UX.
Создайте состояние загрузки путем добавления файла loading.js
в директорию.
export default function Loading() {
// Мы можем добавлять любой UI внутрь `Loading`, такой как скелет
return <LoadingSkeleton />
}
В той же директории loading.js
будет вложен в layout.js
. Он будет оборачивать page.js
и его потомков в компонент Suspense
.
Потоковая передача данных с помощью Suspense
В дополнение к loading.js
мы можем создавать Suspense
для своих компонентов UI. Роутер приложения поддерживает потоковую передачу (streaming, далее также — стриминг) как для Node.js, так и для граничной среды выполнения.
Что такое стриминг?
Для того, чтобы понять, что такое стриминг в React и Next.js, нужно понимать рендеринг на стороне сервера (server side rendering, SSR) и его ограничения.
В SSR существует несколько шагов, который должны завершиться перед тем, как пользователь сможет увидеть и взаимодействовать со страницей:
Эти шаги являются последовательными и блокирующими. Сервер может отрендерить HTML для страницы только после получения всех данных. На клиенте React может гидратировать UI только после загрузки кода всех компонентов.
SSR с помощью React и Next.js помогает улучшить производительной загрузки путем отображения неинтерактивной страницы пользователю как можно быстрее.
Однако это все равно может быть медленным, поскольку получение данных на сервере должно быть завершено перед отображением страницы пользователю.
Стриминг позволяет разбить HTML страницы на небольшие части (chunks) и отправлять их клиенту по-отдельности.
Это позволяет отображать части страницы быстрее, без ожидания получения всех данных для UI.
Стриминг хорошо работает с компонентной моделью React, поскольку каждый компонент может рассматриваться как "чанк" (chunk — часть). Компоненты, которые имеют высший приоритет (например, информация о товаре) или не зависят от данных (например, макет), могут быть отправлены первыми и React может начать их гидратацию раньше. Компоненты с более низким приоритетом (например, отзывы или связанные товары) могут быть отправлены в том же запросе к серверу после получения всех данных.
Стриминг особенно полезен в случаях, когда мы хотим избежать блокировки рендеринга страницы долгими запросами, поскольку это может ухудшить Time To First Byte (TTFB) и First Contentful Paint (FCP). Это также помогает улучшить Time to Interactive (TTI), особенно на медленных устройствах.
Пример
<Suspense>
оборачивает компонент, выполняющий асинхронную операцию (например, запрос данных), отображает резервный UI (например, скелет или спинер) во время выполнения операции и заменяет содержимое компонента после завершения операции.
import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
export default function Posts() {
return (
<section>
<Suspense fallback={<p>Загрузка ленты новостей...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Загрузка погоды...</p>}>
<Weather />
</Suspense>
</section>
)
}
Использование Suspense
дает следующие преимущества:
SEO
generateMetadata
перед началом стриминга UI клиенту. Это гарантирует, что первая часть такого ответа будет содержать теги <head>
Статус-коды
При стриминге статус-код 200
является индикатором успешного запроса.
Сервер может отправлять клиенту ошибки в потоковом контенте, например, когда используются функции redirect
или notFound
, но статус-код обновляться не будет. Это не влияет на SEO.
Файл error.js
позволяет мягко обрабатывать ошибки времени выполнения во вложенных роутах:
Создайте UI ошибки, добавив файл error.js
в директорию и экспортировав из него компонент по умолчанию.
// app/dashboard/error.tsx
'use client' // компоненты `Error` должны быть клиентскими
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Отправляем ошибку в сервис обработки ошибок
console.error(error)
}, [error])
return (
<div>
<h2>Что-то пошло не так</h2>
<button
onClick={
// Пытаемся восстановиться путем повторного рендеринга сегмента
() => reset()
}
>
Попробовать снова
</button>
</div>
)
}
Как error.js
работает?
error.js
автоматически создает предохранитель, оборачивающий вложенный дочерний сегмент или компонент page.js
error.js
, используется в качестве резервного компонентаВосстановление после ошибки
Причина ошибки может временной. В этом случае повторное выполнение операции, например, может решить проблему.
Компонент ошибки может использовать функцию reset
для восстановления. Эта функция повторно рендерит содержимое предохранителя. При успехе резервный компонент ошибки заменяется результатом повторного рендеринга.
// app/dashboard/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Что-то пошло не так</h2>
<button onClick={() => reset()}>Попробовать снова</button>
</div>
)
}
Вложенные роуты
Компоненты, созданные с помощью специальных файлов, рендерятся в определенном порядке.
Например, вложенный роут с двумя сегментами, включающими layout.js
и error.js
, рендерится в такую (упрощенную) иерархию:
Иерархия компонентов влияет на поведение error.js
во вложенном роуте:
error.js
будет обрабатывать ошибки всех вложенных дочерних сегментов. Детализация UI ошибки достигается размещением файлов error.js
на разных уровнях (в разных директориях) роутаerror.js
не обрабатывает ошибки, возникшие в layout.js
того же уровня, поскольку предохранитель оборачивается в макетОбработка ошибок в макетах
error.js
не перехватывает ошибки, возникшие в layout.js
или template.js
того же уровня. Это объясняется тем, что layout.js
или template.js
содержат важный общий UI для нескольких соседних роутов, который должен функционировать, несмотря на ошибку.
Для обработки ошибок, возникающих в layout.js
или template.js
, используется error.js
родительского сегмента.
Для обработки ошибок, возникающих в корневом макете или шаблоне, используется global-error.js
.
Обработка ошибок в корневом макете
Предохранитель global-error.js
оборачивает все приложение, его резервный компонент заменяет корневой макет, поэтому он должен содержать теги <html>
и <body>
.
global-error.js
— это наименее детальный UI ошибки, который может рассматриваться на перехватчик всех ошибок приложения. Он не предназначен для частого использования, поскольку большая часть ошибок должна перехватываться и обрабатываться соответствующими error.js
.
Даже при наличии global-error.js
рекомендуется определять корневой error.js
, чей резервный компонент будет рендериться внутри корневого макета — глобальный общий UI и бренд, например, будут сохраняться.
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<h2>Что-то пошло не так</h2>
<button onClick={() => reset()}>Попробовать снова</button>
</body>
</html>
)
}
Обработка серверных ошибок
Если ошибка возникает внутри серверного компонента, Next.js перенаправляет объект Error
(лишенный конфиденциальной информации об ошибке в производственной среде) в ближайший файл error.js
в качестве пропа error
.
В продакшне Error
содержит только свойства message
и digest
. Это мера безопасности, позволяющая избежать утечки потенциально конфиденциальной информации, содержащейся в ошибке.
message
содержит общее сообщение об ошибке, digest
— автоматически генерируемый хэш ошибки, который может использоваться для поиска соответствующей ошибки в логах сервера.
В режиме разработки Error
сериализуется и содержит message
оригинальной ошибки для облегчения отладки.
В Next.js существует несколько способов обработки перенаправлений.
API | Назначение | Где | Статус-код |
---|---|---|---|
redirect |
Перенаправляет пользователя после мутации или события | Серверные компоненты, серверные операции, обработчики роута | 307 (временное) или 303 (серверная операция) |
permanentRedirect |
Перенаправляет пользователя после мутации или события | Серверные компоненты, серверные операции, обработчики роута | 308 (постоянное) |
useRouter |
Выполняет навигацию на стороне клиента | Обработчики событий в клиентских компонентах | - |
redirects |
Перенаправляет входящий запрос на основе пути | Файл next.config.js |
307 (временное) или 308 (постоянное) |
NextResponse.redirect |
Перенаправляет входящий запрос на основе условия | Посредник | Любой |
Функция redirect
Функция redirect
позволяет перенаправлять пользователя на другой URL. Ее можно вызывать в серверных компонентах, серверных операциях и обработчиках роута.
redirect
часто используется после мутации или события. Пример создания поста:
// app/actions.tsx
'use server'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
export async function createPost(id: string) {
try {
// Обращение к базе данных
} catch (error) {
// Обработка ошибок
}
revalidatePath('/posts') // обновление кешированных постов
redirect(`/post/${id}`) // перенаправление на страницу нового поста
}
Функция permanentRedirect
Функция permanentRedirect
позволяет постоянно (permanently) перенаправлять пользователя на другой URL. Ее можно вызывать в серверных компонентах, серверных операциях и обработчиках роута.
permanentRedirect
часто используется после мутации или события, которое меняет канонический URL сущности, например, обновления URL профиля пользователя после изменения имени пользователя:
// app/actions.ts
'use server'
import { permanentRedirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export async function updateUsername(username: string, formData: FormData) {
try {
// Обращение к БД
} catch (error) {
// Обработка ошибок
}
revalidateTag('username') // обновляем все ссылки на `username`
permanentRedirect(`/profile/${username}`) // перенаправляем в новый профиль пользователя
}
Хук useRouter
Для выполнения перенаправления в обработчике события в клиентском компоненте используется метод push
роутера, возвращаемого хуком useRouter
, например:
// app/page.tsx
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Панель управления
</button>
)
}
redirects
Настройка redirects
файла next.config.js
позволяет перенаправлять входящие запросы на другой URL. Это может быть полезным, когда мы изменили структуру URL страниц и нам известен список перенаправлений.
redirects
поддерживает поиск совпадения с путем, заголовками, куки и строкой запроса, что предоставляет гибкость для перенаправления пользователя на основе входящего запроса.
Для использования redirects
достаточно добавить следующую настройку в next.config.js
:
module.exports = {
async redirects() {
return [
// Обычное перенаправление
{
source: '/about',
destination: '/',
permanent: true,
},
// Поиск совпадения пути с подстановочными знаками
{
source: '/blog/:slug',
destination: '/news/:slug',
permanent: true,
},
]
},
}
NextResponse.redirect
Посредник позволяет запускать код перед завершением запроса. Функция NextResponse.redirect
позволяет перенаправлять пользователя на другой URL на основе входящего запроса. Это может быть полезным, когда мы хотим перенаправлять пользователя на основе определенного условия (например, аутентификация, управление сессией и др.) или у нас имеется большое количество перенаправлений.
Пример перенаправления неавторизованного пользователя на страницу /login
:
// middleware.ts
import { NextResponse, NextRequest } from 'next/server'
import { authenticate } from 'auth-provider'
export function middleware(request: NextRequest) {
const isAuthenticated = authenticate(request)
// Если пользователь авторизован, пропускаем запрос
if (isAuthenticated) {
return NextResponse.next()
}
// Если пользователь не авторизован, перенаправляем его на страницу авторизации
return NextResponse.redirect(new URL('/login', request.url))
}
// Посредник запускается для любого роута панели управления
export const config = {
matcher: '/dashboard/:path*',
}
В директории app
вложенные директории, как правило, влияют на URL. Однако, мы можем сделать директорию группой роутов, чтобы она не включалась в URL роута.
Это позволяет организовать сегменты роута и файлы проекта в логические группы без влияния на структуру URL.
Группы роутов могут быть полезны для:
Соглашение
Для создания группы роутов достаточно обернуть название директории в круглые скобки: (folderName)
.
Примеры
Организация роутов без влияния на URL
Для организации роутов без влияния на URL, создайте группы для того, чтобы держать связанные роуты вместе. Директории в скобках не включаются в URL ((marketing)
или (shop)
).
Несмотря на то, что роуты внутри (marketing)
и (shop)
используют одну иерархию URL, мы можем создавать разные макеты для каждой группы путем добавления в директории файлов layout.js
.
Создание макета для определенных сегментов
Для создания макета для определенных роутов нужно создать группу роутов ((shop)
) с файлом layout.js
в ней и переместить туда роуты, которые должны использовать один макет (account
и cart
). Роуты, не входящие в группу, не будут использовать общий макет (checkout
).
Создание нескольких корневых макетов
Для создания нескольких корневых макетов нужно удалить верхнеуровневый layout.js
и добавить layout.js
в каждую группу. Это может быть полезным для разделения приложения на части, которые имеют совершенно разный UI или UX. Теги <html>
и <body>
должны содержаться в каждом макете.
В приведенном примере (marketing)
и (shop)
имеют собственные корневые макеты.
Кроме соглашений о файлах и директориях для роутинга, Next.js не ограничивает нас в организации и совместном размещении (colocation) файлов проекта.
Безопасная колокация по умолчанию
В директории app
вложенная иерархия директорий определяет структуру роутов.
Каждая директория представляет сегмент роута и определяет соответствующий сегмент URL.
Однако директории по умолчанию являются закрытыми (не доступны публично).
Публично доступным является только содержимое файлов page.js
и route.js
.
Это означает, что файлы проекта, размещаемые в сегментах роута, никогда не буду доступны извне.
Возможности организации проекта
Next.js предоставляет несколько возможностей по организации кода проекта.
Закрытые директории
Директории, названия которых начинаются с нижнего подчеркивания, являются закрытыми: _folderName
.
Такие директории исключаются из роутинга.
Поскольку файлы в директории app
могут безопасно размещаться по умолчанию, закрытые директории для этого не требуются. Однако они могут использоваться для:
Группы роутов
Директории, названия которых заключены в круглые скобки, считаются группами роутов: (folderName)
.
Такие директории нужны для организации файлов и не влияют на URL роута.
Группы роутов могут быть полезны для:
Директория src
Next.js поддерживает хранение кода приложения (включая директорию app
) в опциональной директории src
. Это позволяет отделить код приложения от файлов настроек.
Синонимы путей модулей
Next.js поддерживает синонимы путей модулей, которые облегчают чтение и поддержку импортов в глубоко вложенных файлах проекта:
// app/dashboard/settings/analytics/page.js
// До
import { Button } from '../../../components/button'
// После
import { Button } from '@/components/button'
Стратегии организации проекта
Когда речь заходит об организации файлов и директорий в проекте Next.js, не существует правильных или неправильных подходов.
Ниже предлагается несколько распространенных стратегий. Какой вариант выбрать или придумать свой, зависит от потребностей приложения.
Хранение файлов проекта за пределами директории app
Данная стратегия предполагает хранение кода приложения в общих директориях в корне проекта и использование директории app
только для целей роутинга.
Хранение файлов в директориях верхнего уровня директории app
Данная стратегия предполагает хранение кода приложения в общих директориях на верхнем уровне директории app
.
Разделение файлов по функционалу или роуту
Данная стратегия предполагает хранение глобального общего кода приложения в корне директории app
и разделение более специфического кода на сегменты роута, которые этот код используют.
Динамические сегменты предназначены для создания роутов во время запросов или их предварительного рендеринга во время сборки, когда мы не знаем точных названий сегментов во время разработки и хотим создавать роуты из динамических данных.
Соглашение
Динамический сегмент создается путем оборачивания названия директории в квадратные скобки: [folderName]
, например, [id]
или [slug]
.
Динамические сегменты передаются макету, странице, роуту и функции generateMetadata
в качестве пропа params
.
Пример
Блог может включать роут app/blog/[slug]/page.js
, где [slug]
— это динамический сегмент постов блога.
// app/blog/[slug]/page.tsx
export default function Page({ params }: { params: { slug: string } }) {
return <div>Пост: {params.slug}</div>
}
Роут | Пример URL | params |
---|---|---|
app/blog/[slug]/page.js |
/blog/a |
{ slug: 'a' } |
app/blog/[slug]/page.js |
/blog/b |
{ slug: 'b' } |
app/blog/[slug]/page.js |
/blog/c |
{ slug: 'c' } |
Генерация статических параметров
Функция generateStaticParams
может быть использована в сочетании с динамическими сегментами для статической генерации роутов по время сборки, а не во время запроса.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then((res) => res.json())
return posts.map((post) => ({
slug: post.slug,
}))
}
Основным преимуществом generateStaticParams
является умное извлечение данных. Если содержимое запрашивается с помощью функции fetch
в generateStaticParams
, запросы автоматически мемоизируются. Это означает, что fetch
с одинаковыми параметрами в разных generateStaticParams
, макетах и страницах будет выполнен один раз, что снижает время сборки.
Сегменты-перехватчики
Динамические сегменты могут расширяться до перехватчиков (catch-all) путем добавления многоточия к названию директории в квадратных скобках: [...folderName]
.
Например, app/shop/[...slug]/page.js
будет совпадать не только с /shop/clothes
, но также с /shop/clothes/tops
, /shop/clothes/tops/t-shirts
и т.д.
Роут | Пример URL | params |
---|---|---|
app/shop/[...slug]/page.js |
/shop/a |
{ slug: ['a'] } |
app/shop/[...slug]/page.js |
/shop/a/b |
{ slug: ['a', 'b'] } |
app/shop/[...slug]/page.js |
/shop/a/b/c |
{ slug: ['a', 'b', 'c'] } |
Опциональные сегменты-перехватчики
Сегменты-перехватчики можно сделать опциональными, заключив параметр в двойные квадратные скобки: [[...folderName]]
.
Например, app/shop/[[...slug]]/page.js
будет совпадать также с /shop
в дополнение к /shop/clothes
, /shop/clothes/tops
и /shop/clothes/tops/t-shirts
.
Отличие опциональных сегментов-перехватчиков от обычных состоит в том, что опциональные совпадают с роутом без параметра (/shop
).
Роут | Пример URL | params |
---|---|---|
app/shop/[[...slug]]/page.js |
/shop |
{} |
app/shop/[[...slug]]/page.js |
/shop/a |
{ slug: ['a'] } |
app/shop/[[...slug]]/page.js |
/shop/a/b |
{ slug: ['a', 'b'] } |
app/shop/[[...slug]]/page.js |
/shop/a/b/c |
{ slug: ['a', 'b', 'c'] } |
TypeScript
params
можно типизировать, например:
export default function Page({ params }: { params: { slug: string } }) {
return <h1>Страница</h1>
}
Роут | Тип params |
---|---|
app/blog/[slug]/page.js |
{ slug: string } |
app/shop/[...slug]/page.js |
{ slug: string[] } |
app/shop/[[...slug]]/page.js |
{ slug?: string[] } |
app/[categoryId]/[itemId]/page.js |
{ categoryId: string, itemId: string } |
Параллельные роуты позволяют одновременно или условно рендерить несколько страниц в одном макете. Они полезны для высокодинамичных разделов страницы, таких как панели управления и ленты новостей.
В качестве примера рассмотрим панель управления, в которой параллельные роуты используются для одновременного рендеринга страниц team
и analytics
:
Слоты
Параллельные роуты создаются с помощью именованных слотов. Слоты определяются с помощью @folderName
. Пример определения двух слотов, @analytics
и @team
:
Слоты передаются как пропы общему родительскому макету. В приведенном примере компонент в app/layout.js
принимает пропы analytics
и team
и может рендерить их одновременно, наряду с пропом children
:
// app/layout.tsx
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<>
{children}
{team}
{analytics}
</>
)
}
Слоты не являются сегментами роута и не влияют на URL. Например, для /dashboard/@analytics/views
URL будет выглядеть как /dashboard/views
, поскольку @analytics
— это слот.
Активное состояние и навигация
По умолчанию Next.js отслеживает активное состояние (подстраницу) каждого слота. Однако контент, который рендерится внутри слота, будет зависеть от типа навигации:
default.js
для не совпавших слотов или страницу ошибки 404, если default.js
отсутствуетdefault.js
Файл default.js
позволяет определить резервный контент для несовпадающих слотов во время начальной загрузки или полной перезагрузки страницы.
Рассмотрим следующую структуру директорий. Слот @team
содержит страницу /settings
, а слот @analytics
не содержит.
При переходе на /dashboard/settings
, слот @team
отрендерит страницу /settings
, сохранив активную страницу для слота @analytics
.
При перезагрузке Next.js отрендерит default.js
для @analytics
. Если default.js
отсутствует, рендерится 404.js
.
Кроме того, поскольку children
— это неявный слот, для него также требуется default.js
для рендеринга резервного контента, когда Next.js не может восстановить активное состояние родительской страницы.
useSelectedLayoutSegment(s)
Хуки useSelectedLayoutSegment
и useSelectedLayoutSegments
принимают параметр parallelRoutesKey
, который позволяет читать активный сегмент роута в слоте:
// app/layout.tsx
'use client'
import { useSelectedLayoutSegment } from 'next/navigation'
export default function Layout({ auth }: { auth: React.ReactNode }) {
const loginSegments = useSelectedLayoutSegment('auth')
// ...
}
Когда пользователь переходит на /login
, loginSegments
возвращает "login"
.
Примеры
Условные роуты
Параллельные роуты могут использоваться для условного рендеринга роутов, например, на основе роли пользователя. Пример рендеринга разных панелей управления для ролей admin
и user
:
// app/dashboard/layout.tsx
import { checkUserRole } from '@/lib/auth'
export default function Layout({
user,
admin,
}: {
user: React.ReactNode
admin: React.ReactNode
}) {
const role = checkUserRole()
return <>{role === 'admin' ? admin : user}</>
}
Группы табов
Если добавить в слот файл layout.js
, то макет будет доступен отдельно. Это полезно для создания табов.
Пример слота @analytics
с двумя подстраницами, /page-views
и /visitors
:
// app/dashboard/@analytics/layout.tsx
import Link from 'next/link'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Link href="/dashboard/page-views">Просмотры страницы</Link>
<Link href="/dashboard/visitors">Посетители</Link>
</nav>
<div>{children}</div>
</>
)
}
Модальные окна
Параллельные роуты могут использоваться совместно с перехватывающими роутами для создания модалок. Это позволяет решать такие задачи, как:
Рассмотрим паттерн UI, когда пользователь может открыть модалку авторизации из макета с помощью клиентской навигации или через отдельную страницу /login
:
Начнем с создания роута /login
, который рендерит основную страницу авторизации:
// app/login/page.tsx
import { Login } from '@/app/ui/login'
export default function Page() {
return <Login />
}
Добавляем файл default.js
в слот @auth
, возвращающий null
. Это гарантирует, что будет рендерится только активная модалка.
// app/@auth/default.tsx
export default function Default() {
return null
}
В слоте @auth
перехватываем роут /login
путем изменения названия директории на (.)login
. Импортируем компонент Modal
и его потомков в файл (.)login/page.tsx
:
import { Modal } from '@/app/ui/modal'
import { Login } from '@/app/ui/login'
export default function Page() {
return (
<Modal>
<Login />
</Modal>
)
}
Теперь для открытия/закрытия модалки можно использовать роутер Next.js. Это обеспечивает правильное обновление URL при открытии модалки, а также при навигации "вперед-назад".
Для открытия модалки передаем слот @auth
как проп в родительский макет и рендерим его наряду с пропом children
:
// app/layout.tsx
import Link from 'next/link'
export default function Layout({
auth,
children,
}: {
auth: React.ReactNode
children: React.ReactNode
}) {
return (
<>
<nav>
<Link href="/login">Открыть модальное окно</Link>
</nav>
<div>{auth}</div>
<div>{children}</div>
</>
)
}
Когда пользователь кликает по ссылке, открывается модалка вместо перехода на страницу /login
. Однако при перезагрузке или начальной загрузке пользователь окажется на основной странице авторизации.
Для закрытия окна можно вызвать метод router.back
или использовать компонент Link
:
// app/ui/modal.tsx
'use client'
import { useRouter } from 'next/navigation'
export function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter()
return (
<>
<button
onClick={() => {
router.back()
}}
>
Закрыть модальное окно
</button>
<div>{children}</div>
</>
)
}
При использовании компонента Link
для того, чтобы уйти со страницы, которая больше не должна отображать слот @auth
, можно использовать роут-перехватчик, возвращающий null
:
// app/ui/modal.tsx
import Link from 'next/link'
export function Modal({ children }: { children: React.ReactNode }) {
return (
<>
<Link href="/">Закрыть модальное окно</Link>
<div>{children}</div>
</>
)
}
// app/@auth/[...catchAll]/page.tsx
export default function CatchAll() {
return null
}
UI загрузки и ошибки
Параллельные роуты могут передаваться по-отдельности, что позволяет определить состояния загрузки и ошибки для каждого роута:
Перехватывающие роуты позволяют загружать роут из другой части приложения в текущий макет. Эта парадигма роутинга может быть полезна, когда мы хотим отображать контент роута без перемещения пользователя в другой контекст.
Например, при клике по фото в ленте новостей, мы можем отображать фото в модалке, перекрывающей ленту. В этом случае Next.js перехватывает роут /photo/123
, маскирует URL и перекрываем им /feed
.
Однако при переходе к фото путем клика по ссылке или после перезагрузки страницы, вместо модалки должна рендериться страница фотографии. В этих случаях роут не должен перехватываться.
Соглашение
Перехватывающие роуты определяются с помощью (..)
, что похоже на относительный путь ../
, но для сегментов.
Мы можем использовать:
(.)
для поиска совпадений с сегментами того же уровня(..)
для поиска совпадений с сегментами уровнем выше(..)(..)
для поиска совпадений с сегментами двумя уровнями выше(...)
для поиска совпадений с сегментами, начиная с корня директории app
Например, мы можем перехватить сегмент photo
из сегмента feed
, создав директорию (..)photo
:
Примеры
Модальные окна
Перехватывающие роуты могут использоваться совместно с параллельными роутами для создания модалок. Это позволяет решать такие задачи, как:
Рассмотрим паттерн UI, когда пользователь может открыть модалку с фото из галереи с помощью клиентской навигации или клика по ссылке:
В этом примере путь к сегменту photo
может быть перехвачен с помощью (..)
, поскольку @modal
— это не слот и не сегмент. Это означает, что роут photo
находится всего на один уровень выше, несмотря на то, что в иерархии файловой системы он находится на два уровня выше.
Обработчики роута позволяют создавать кастомные обработчики запросов для роутов с помощью Request и Response Web API
.
Соглашение
Обработчик роута определяется в файле route.js
внутри директории app
:
// app/api/route.ts
export const dynamic = 'force-dynamic' // по умолчанию `auto`
export async function GET(request: Request) {}
В одном сегменте роута не может быть одновременно и route.js
, и page.js
.
Поддерживаемые методы HTTP
Поддерживаются следующие методы HTTP: GET
, POST
, PUT
, PATCH
, DELETE
, HEAD
и OPTIONS
. При использовании неподдерживаемого метода Next.js возвращает ответ 405 Method Not Allowed
.
NextRequest
и NextResponse
Next.js расширяет Request
и Response
API с помощью NextRequest
и NextResponse
, соответственно, предоставляя утилиты для продвинутых случаев использования.
Поведение
Кеширование
Обработчики роута кешируются по умолчанию при использовании метода GET
и объекта Response
:
// app/items/route.ts
export async function GET() {
const res = await fetch('https://data.mongodb-api.com/...', {
headers: {
'Content-Type': 'application/json',
'API-Key': process.env.DATA_API_KEY,
},
})
const data = await res.json()
return Response.json({ data })
}
Отключение кеширования
Автоматическое кеширование отключается в случае:
Request
с методом GET
cookies
и headers
Пример использования объекта Request
:
// app/products/api/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
const res = await fetch(`https://data.mongodb-api.com/product/${id}`, {
headers: {
'Content-Type': 'application/json',
'API-Key': process.env.DATA_API_KEY,
},
})
const product = await res.json()
return Response.json({ product })
}
Пример использования метода POST
:
// app/items/route.ts
export async function POST() {
const res = await fetch('https://data.mongodb-api.com/...', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'API-Key': process.env.DATA_API_KEY!,
},
body: JSON.stringify({ time: new Date().toISOString() }),
})
const data = await res.json()
return Response.json(data)
}
Разрешение роута
route
можно считать простейшим примитивом роутинга:
page
route.js
, и page.js
Каждый route.js
или page.js
потребляет все глаголы HTTP для этого роута:
// app/page.js
export default function Page() {
return <h1>Привет, Next.js!</h1>
}
// ❌ Конфликт
// app/route.js
export async function POST(request) {}
Примеры
Ревалидация кешированных данных
Кешированные данные можно обновлять с помощью настройки next.revalidate
:
// app/items/route.ts
export async function GET() {
const res = await fetch('https://data.mongodb-api.com/...', {
next: { revalidate: 60 }, // Ревалидировать каждые 60 секунд
})
const data = await res.json()
return Response.json(data)
}
Также можно использовать настройку сегмента роута revalidate
:
export const revalidate = 60
Динамические функции
В обработчиках роутов могут использоваться динамические функции, такие как cookies
и headers
.
cookies
Функция cookies
из next/headers
позволяет читать и устанавливать куки. Эта серверная функция может вызываться прямо в обработчике роута или из другой функции.
В качестве альтернативы можно вернуть новый Response
с заголовком Set-Cookie
:
// app/api/route.ts
import { cookies } from 'next/headers'
export async function GET(request: Request) {
const cookieStore = cookies()
const token = cookieStore.get('token')
return new Response('Привет, Next.js!', {
status: 200,
headers: { 'Set-Cookie': `token=${token.value}` },
})
}
Мы также можем использовать Request
для чтения куки из запроса:
// app/api/route.ts
import type { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const token = request.cookies.get('token')
}
headers
Функция headers
из next/headers
позволяет читать и устанавливать заголовки. Эта серверная функция может вызываться прямо в обработчике роута или из другой функции.
Экземпляр headers
доступен только для чтения. Для установки заголовков нужно вернуть новый Response
с новыми headers
:
// app/api/route.ts
import { headers } from 'next/headers'
export async function GET(request: Request) {
const headersList = headers()
const referer = headersList.get('referer')
return new Response('Привет, Next.js!', {
status: 200,
headers: { referer: referer },
})
}
Мы также можем использовать Request
для чтения заголовка из запроса:
// app/api/route.ts
import { type NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const requestHeaders = new Headers(request.headers)
}
redirect
// app/api/route.ts
import { redirect } from 'next/navigation'
export async function GET(request: Request) {
redirect('https://nextjs.org/')
}
Динамические сегменты роута
Обработчики роута могут использовать динамические сегменты роута для создания обработчиков запроса из динамических данных:
// app/items/[slug]/route.ts
export async function GET(
request: Request,
{ params }: { params: { slug: string } }
) {
const slug = params.slug // 'a', 'b' или 'c'
}
Роут | Пример URL | params |
---|---|---|
app/items/[slug]/route.js |
/items/a |
{ slug: 'a' } |
app/items/[slug]/route.js |
/items/b |
{ slug: 'b' } |
app/items/[slug]/route.js |
/items/c |
{ slug: 'c' } |
Строка запроса URL
Объект запроса, передаваемый обработчику роута, это экземпляр NextRequest
, который содержит некоторые дополнительные методы, облегчающие обработку запроса:
// app/api/search/route.ts
import type { NextRequest } from 'next/server'
export function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('query')
// `query` будет иметь значение `"hello"` для `/api/search?query=hello`
}
Потоковая передача
Потоковая передача данных часто используется совместно с большими языковыми моделями, такими как OpenAI, для контента, генерируемого искусственным интеллектом:
// app/api/chat/route.ts
import OpenAI from 'openai'
import { OpenAIStream, StreamingTextResponse } from 'ai'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
})
export const runtime = 'edge'
export async function POST(req: Request) {
const { messages } = await req.json()
const response = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
stream: true,
messages,
})
const stream = OpenAIStream(response)
return new StreamingTextResponse(stream)
}
Эти абстракции используют Web API
для создания потока. Это также можно сделать вручную:
// app/api/route.ts
// https://developer.mozilla.org/docs/Web/API/ReadableStream#convert_async_iterator_to_stream
function iteratorToStream(iterator: any) {
return new ReadableStream({
async pull(controller) {
const { value, done } = await iterator.next()
if (done) {
controller.close()
} else {
controller.enqueue(value)
}
},
})
}
function sleep(time: number) {
return new Promise((resolve) => {
setTimeout(resolve, time)
})
}
const encoder = new TextEncoder()
async function* makeIterator() {
yield encoder.encode('<p>Один</p>')
await sleep(200)
yield encoder.encode('<p>Два</p>')
await sleep(200)
yield encoder.encode('<p>Три</p>')
}
export async function GET() {
const iterator = makeIterator()
const stream = iteratorToStream(iterator)
return new Response(stream)
}
Тело запроса
Тело запроса можно читать с помощью стандартных методов:
// app/items/route.ts
export async function POST(request: Request) {
const res = await request.json()
return Response.json({ res })
}
FormData
FormData
можно читать с помощью функции request.formData
:
// app/items/route.ts
export async function POST(request: Request) {
const formData = await request.formData()
const name = formData.get('name')
const email = formData.get('email')
return Response.json({ name, email })
}
Поскольку все данные FormData
являются строками, для валидации запроса и извлечения данных в нужном формате (например, number
) можно воспользоваться zod-form-data.
CORS
Заголовки CORS можно устанавливать с помощью стандартных методов:
// app/api/route.ts
export const dynamic = 'force-dynamic' // по умолчанию `auto`
export async function GET(request: Request) {
return new Response('Привет, Next.js!', {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
Веб-хуки
Обработчики роута можно использовать для получения веб-хуков из сторонних сервисов:
// app/api/route.ts
export async function POST(request: Request) {
try {
const text = await request.text()
// Обработка полезной нагрузки веб-хука
} catch (error) {
return new Response(`Ошибка: ${error.message}`, {
status: 400,
})
}
return new Response('Успех', {
status: 200,
})
}
Граничная и Node.js среды выполнения
Обработчики роута имеют изоморфный Web API
для поддержки граничной (edge) и Node.js сред выполнения, включая поддержку стриминга. Поскольку обработчики роута имеют такую же конфигурацию сегмента роута, что и страницы и макеты, они поддерживают продвинутые возможности, такие как статическая регенерация.
Для определения среды выполнения используется настройка runtime
:
export const runtime = 'edge' // по умолчанию `nodejs`
Ответы не UI
Обработчики роута могут возвращать контент, не связанный с UI. Обратите внимание, что sitemap.xml
, robots.txt
, иконки приложения и изображения OpenGraph имеют встроенную поддержку.
// app/rss.xml/route.ts
export const dynamic = 'force-dynamic'
export async function GET() {
return new Response(
`<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>Документация Next.js</title>
<link>https://nextjs.org/docs</link>
<description>Фреймворк React для веба</description>
</channel>
</rss>`,
{
headers: {
'Content-Type': 'text/xml',
},
}
)
}
Настройки сегмента роута
Обработчики роута имеют такую же конфигурацию сегмента роута, что и страницы и макеты:
// app/items/route.ts
export const dynamic = 'auto'
export const dynamicParams = true
export const revalidate = false
export const fetchCache = 'auto'
export const runtime = 'nodejs'
export const preferredRegion = 'auto'
Посредник (middleware) позволяет запускать код перед завершением обработки запроса для модификации ответа путем перезаписи, перенаправления, изменения заголовков запроса или ответа и раннего ответа.
Посредник запускается перед возвратом кешированного контента и совпадением роутов.
Соглашение
Для определения посредника используется файл middleware.js
в корне проекта (на одном уровне с директорией app
или src
).
Пример
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// Эта функция может быть асинхронной
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
export const config = {
matcher: '/about/:path*',
}
Совпадающие пути
Посредник вызывается для каждого роута приложения. Порядок выполнения следующий:
headers
из next.config.js
.redirects
из next.config.js
.beforeFiles
(перезаписи) из next.config.js
public/
, _next/static/
, app/
и др.)afterFiles
(перезаписи) из next.config.js
./blog/[slug]
).fallback
(перезаписи) из next.config.js
.Существует два способа определить, для каких путей запускается посредник:
Матчер
matcher
позволяет определять пути для запуска посредника:
export const config = {
matcher: '/about/:path*',
}
Путей может быть несколько:
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
}
matcher
поддерживает все возможности регулярных выражений:
export const config = {
matcher: [
/*
* Совпадает со всеми путями, кроме тех, которые начинаются с:
* - api (роуты API)
* - _next/static (статические файлы)
* - _next/image (файлы оптимизированных изображений)
* - favicon.ico
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
Запросы на предварительное получение данных роутов (компонент Link
), которые не должны проходить через посредника, можно игнорировать с помощью массива missing
:
export const config = {
matcher: [
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
}
Пути для поиска совпадения должны соответствовать следующим условиям:
/
/about/:path
совпадает с /about/a
и /about/b
, но не с /about/a/c
:
): /about/:path*
совпадает с /about/a/b/c
, поскольку *
— нуль и более. ?
— это нуль или один, а +
— это один и более/about/(.*)
— это тоже самое, что /about/:path*
Условные инструкции
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/about')) {
return NextResponse.rewrite(new URL('/about-2', request.url))
}
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
}
NextResponse
NextResponse
API позволяет:
redirect
) входящий запрос на другой URLrewrite
) ответ путем отображения определенного URLgetServerSideProps
и назначений rewrite
Для генерации ответа в посреднике можно:
rewrite
в роут (страницу или обработчик роута), генерирующие ответNextResponse
напрямуюИспользование куки
Куки — это обычные заголовки. В Request
они находятся в заголовке Cookie
, в Response
— в заголовке Set-Cookie
. Next.js предоставляет удобный способ для доступа и управления куки через расширение cookies
на NextRequest
и NextResponse
.
cookies
предоставляет методы get
, getAll
, set
, delete
, has
и clear
.cookies
предоставляет методы get
, getAll
, set
и delete
.import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Предположим, что в запросе есть заголовок "Cookie:nextjs=fast"
// Получаем куки из запроса с помощью `RequestCookies` API
let cookie = request.cookies.get('nextjs')
console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
const allCookies = request.cookies.getAll()
console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]
request.cookies.has('nextjs') // => true
request.cookies.delete('nextjs')
request.cookies.has('nextjs') // => false
// Устанавливаем куки в ответ с помощью `ResponseCookies` API
const response = NextResponse.next()
response.cookies.set('vercel', 'fast')
response.cookies.set({
name: 'vercel',
value: 'fast',
path: '/',
})
cookie = response.cookies.get('vercel')
console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
// Ответ будет содержать заголовок `Set-Cookie:vercel=fast;path=/`
return response
}
Установка заголовков
Для установки заголовков запросов и ответов можно использовать API NextResponse
(доступно с Next.js@13.0.0):
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Клонируем заголовки запроса и добавляем новый заголовок `x-hello-from-middleware1`
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-hello-from-middleware1', 'hello')
// Заголовки запроса также можно устанавливать в `NextResponse.rewrite`
const response = NextResponse.next({
request: {
// Новые заголовки запроса
headers: requestHeaders,
},
})
// Устанавливаем новый заголовок ответа `x-hello-from-middleware2`
response.headers.set('x-hello-from-middleware2', 'hello')
return response
}
Генерация ответа
Посредник может возвращать Response
или NextResponse
(доступно с Next.js@13.1.0):
import { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'
// Ограничиваем посредника путями, начинающимися с `/api/`
export const config = {
matcher: '/api/:function*',
}
export function middleware(request: NextRequest) {
// Вызываем функцию аутентификации для проверки запроса
if (!isAuthenticated(request)) {
// Отвечаем JSON, содержащим сообщение об ошибке
return Response.json(
{ success: false, message: 'Провал аутентификации' },
{ status: 401 }
)
}
}
waitUntil
и NextFetchEvent
Объект NextFetchEvent
расширяет нативный объект FetchEvent и предоставляет метод waitUntil.
Метод waitUntil
принимает промис в качестве параметра и увеличивает время жизни посредника до разрешения промиса. Это может быть полезным для выполнения работы в фоновом режиме.
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'
export function middleware(req: NextRequest, event: NextFetchEvent) {
event.waitUntil(
fetch('https://my-analytics-platform.com', {
method: 'POST',
body: JSON.stringify({ pathname: req.nextUrl.pathname }),
})
)
return NextResponse.next()
}
Продвинутые флаги посредника
В Next.js@13.1 появилось два новых флага для посредника: skipMiddlewareUrlNormalize
и skipTrailingSlashRedirect
.
skipTrailingSlashRedirect
отключает перенаправления Next.js для добавления или удаления завершающих слэшей. Это позволяет кастомной обработке в посреднике поддерживать завершающие слэши для одних путей и удалять для других, что может облегчить инкрементальные миграции.
// next.config.js
module.exports = {
skipTrailingSlashRedirect: true,
}
// middleware.js
const legacyPrefixes = ['/docs', '/blog']
export default async function middleware(req) {
const { pathname } = req.nextUrl
if (legacyPrefixes.some((prefix) => pathname.startsWith(prefix))) {
return NextResponse.next()
}
// Обработка завершающих слэшей
if (
!pathname.endsWith('/') &&
!pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)
) {
req.nextUrl.pathname += '/'
return NextResponse.redirect(req.nextUrl)
}
}
skipMiddlewareUrlNormalize
позволяет отключить нормализацию URL в Next.js для того, чтобы сделать обработку прямых и клиентских переходов одинаковой. В некоторых случаях эта настройка предоставляет полный контроль использования оригинального URL.
// next.config.js
module.exports = {
skipMiddlewareUrlNormalize: true,
}
// middleware.js
export default async function middleware(req) {
const { pathname } = req.nextUrl
// GET /_next/data/build-id/hello.json
console.log(pathname)
// С флагом название пути имеет вид `/_next/data/build-id/hello.json`
// Без флага оно будет нормализовано до `/hello`
}
Среда выполнения
В настоящее время посредник поддерживает только граничную среду выполнения, Node.js не поддерживается.
Это конец первой части руководства.
Happy coding!
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩