Next.js: Современные тенденции в веб-разработке (часть 2)
Предыдущая часть про
Page роутер
App роутер
По мнению команды Vercel
, использование Next.js
представляет собой новую ступень в развитии разработки. Официальный сайт не только рекомендует использовать App роутер
для новых проектов, но и советует переводить существующие проекты с Page роутера
на этот новый подход, что позволяет объединить обе модели и упростить процесс рефакторинга. Давайте рассмотрим ключевые изменения и принципы этого подхода.
- Файловая структура роутинга
В App роутере
компонентные файлы больше не участвуют в определении маршрутов, поскольку каждый компонент-страница теперь должен иметь зарезервированное название page
. Это означает, что роль определения маршрутов целиком лежит на папках. Например, компонент, расположенный по пути app/about/page.tsx
, будет доступен в браузере по адресу /about
. Также синтаксис динамических роутов применяется теперь только к папкам, например, app/users/[id]/page.tsx
доступен как /users/123
. Если добавить символ _
в начале названия папки, вся ветка проекта исключится из маршрутизации.
- Зарезервированные имена и роль в формировании страниц
Известно, что многие веб-приложения требуют глобальных элементов пользовательского интерфейса на различных страницах, или некоторые страницы нуждаются в установке общего контекста или состояния. В стандартном React
, а также в Page роутере
, это обычно решается созданием отдельных компонентов с общими элементами пользовательского интерфейса или инициализацией контекстов, которые затем «оборачивают» другие компоненты. App роутер
существенно упрощает этот процесс, предоставляя специальные зарезервированные имена для компонентов, которые автоматически распознаются при рендеринге страниц для формирования нужной структуры страницы без дополнительного вмешательства разработчика.
Один из основных компонентов в этой связи — это layout
. Его задача заключается в предоставлении общего пользовательского интерфейса и контекста для определенного сегмента файловой системы. Рутовый layout
обычно находится в корневой папке приложения, что позволяет определять «скелет» каждой страницы.
Базовый пример:
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "ASDEV Next App",
description: "Базовый пример Root Layout",
};
export default function RootLayout({ children, }: { children: React.ReactNode; }) {
return (
<html lang="ru">
<body>{ children } </body>
</html>
);
}
Особенность layout-компонентов заключается в том, что они рендерятся всего один раз за весь жизненный цикл приложения и «запоминаются», что повышает производительность приложения. Например, рутовый layout идеально подходит для инициализации глобального состояния с использованием таких популярных решений, как Redux
, Recoil
и Zustand
. Также рутовый layout часто используется для включения в каждую страницу общих элементов пользовательского интерфейса, таких как хедер и футер.
Другие зарезервированные компоненты и их роли:
template
– схожая роль сlayout
, однако рендеринг происходит при каждом запросе страницы сегмента;loading
– загрузочныйUI
, который будет показываться до завершения рендеринга запрашиваемой страницы сегмента;error
–UI
на случай ошибки при обработке запрашиваемой страницы текущего сегмента, использует специальный React-компонентErrorBoundary
;not-found
– схожая роль сerror
специально на случай не распознавания запрашиваемого адреса, аналогично используетErrorBoundary
компонент;page
– уникальныйUI
запрашиваемой страницы.
Для большей наглядности позаимствуем схемы иерархии с официальной Next.js документации:
Иерархия страниц на нижних уровнях сегмента файловой системы начинает формироваться внутри всех компонентов высших уровней (кроме уникальных page
):
- Разделение компонентов на роли. Рендеринг компонентов
Одной из проблем Page роутера
было отсутствие четкой границы между тем, что можно выполнять на сервере, и исключительно клиентской логикой. App роутер
четко разделяет компоненты на серверные и клиентские. По умолчанию все компоненты считаются серверными для стимулирования разработки в стиле, при котором максимальное количество логики выполняется в соответствии с принципами SSG
или SSR
. Для указания Next.js
, что компонент должен обрабатываться на стороне клиента, используется специальная директива в начале файла "use client"
.
Вот простой пример клиентского компонента-счетчика:
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p> Вы нажали {count} раз</p>
<button onClick={() => setCount(count + 1)}>Нажмите меня</button>
</div>
)
}
Как видно, этот компонент ничем не отличается от обычного React-компонента, использующего хук useState
. Однако хуки являются частью API React
и не существуют на серверной стороне, что требует превращения компонента в клиентский. Примерные требования к использованию различных типов компонентов можно выразить следующей таблицей:
Клиентские компоненты могут внедрятся в серверные, например, в разрезе зарезервированных компонентов, layout
обычно является серверным компонентом, тогда как page
по необходимости может быть клиентским интерактивным компонентом.
Формирование результирующей страницы сначала происходит в 2 шага на стороне сервера:
- React рендерит серверные компоненты в специальный формат данных, который называется
React Server Component Payload (RSC Payload)
. Next.js
используетRSC Payload
и JavaScript-инструкции клиентского компонента для рендерингаHTML
на сервере.
Затем на клиенте:
HTML
используется для немедленного отображения быстрого неинтерактивного предварительного варианта запрашиваемого роута – только для начальной загрузки страницы.RSC Payload
используется для согласования структур клиентских и серверных компонентов и обновленияDOM
.- JavaScript-инструкции используются для «гидрации» клиентских компонентов и придания приложению интерактивности.
- Серверные компоненты и стратегии рендеринга
Использование серверных компонентов на вершине структуры страниц обеспечивает удобный способ получения данных и рендеринга компонентов во время сборки приложения или запроса клиентом без необходимости применения специальных методов, в отличие от Page роутера. Рассмотрим применение серверных компонентов в различных стратегиях:
— Статическая генерация (SSG):
Изначально серверные компоненты настроены на статическую генерацию. Любые запросы на бэкенд кэшируются, а их результаты – запоминаются. Пример:
type Product = {
id: number;
name: string;
price: number;
isAvailable: boolean;
};
async function getProducts() {
const res = await fetch(`https://...`);
const products = await res.json();
return products;
}
export default async function Page() {
const products = await getProducts();
return products.map((product: Product) => <div>{product.name}</div>)
}
Next.js
расширяет возможности стандартного fetch()
, и его использование в серверных компонентах поддерживает специальный параметр cache
. Поскольку SSG
являет стратегией по умолчанию, все запросы отправляются с cache: 'force-cache'
, если не указывать этот параметр вручную. По сути, это соответствует getStaticProps()
методу из Page роутера
.
— Рендеринг на сервере (SSR):
SSG
стратегия легко переходит в SSR
при дополнительной настройке fetch()
. Передавая параметр cache: 'no-store'
, мы сигнализируем, что компонент должен проходить рендинг на сервере во время каждого запроса страницы, включающей компонент. Аналогично это соответствует методу getServerSideProps()
. Немного поправив предыдущий пример, получаем:
type Product = {
id: number;
name: string;
price: number;
isAvailable: boolean;
};
async function getProducts() {
const res = await fetch(`https://...`, { cache: "no-store" });
const products = await res.json();
return products;
}
export default async function Page() {
const products = await getProducts();
return products.map((product: Product) => <div>{product.name}</div>);
}
— Стриминг
Стриминг можно рассматривать как специализированный тип SSR
, который используется для рендеринга серверного компонента, содержащего несколько независимых асинхронных блоков или других серверных компонентов. В этом подходе каждый такой блок сначала оборачивается в специальный компонент React Suspense
, который предназначен для отображения временного пользовательского интерфейса во время выполнения внутреннего JSX-кода
. Таким образом, стратегия стриминга позволяет достичь постепенного рендеринга HTML на сервере и гидратации компонентов по мере их готовности, постепенно заменяя Suspense-компоненты. Этот подход существенно улучшает производительность комплексных страниц, таких как веб-дэшборды. Рассмотрим условный пример подобной страницы:
import { Suspense } from 'react'
import { NewsFeed, Weather } from './Components'
export default function Dashboard() {
return (
<section>
<Suspense fallback={<p>Загружаем новости. Пожалуйста подождите...</p>}>
<NewsFeed />
</Suspense>
<Suspense fallback={<p>Загружаем погоду. Пожалуйста подождите...</p>}>
<Weather />
</Suspense>
</section>
)
}
- API
Аналогично разделению компонентов на серверные и клиентские, API-маршруты
из структуры Page
имеют свои аналоги в зависимости от контекста использования. Если необходимо выполнить API-запрос
из серверного компонента, то вместо этого серверный компонент может напрямую взаимодействовать с базой данных и действовать как API-эндпоинт
. В случае необходимости отправить запрос из клиентского компонента в результате взаимодействия пользователя с приложением, App роутер
предоставляет специальные файлы — обработчики маршрутов API (Route Handlers)
. Структура эндпоинта формируется файловой системой. Папка на верхнем уровне не обязательно должна иметь имя api
, но сегмент должен содержать файл с зарезервированным именем route.js/ts
, что позволяет Next.js
понимать, что это исключительно серверный функционал, который не следует включать в само клиентское приложение. Сам обработчик должен содержать одну или несколько экспортируемых функций с названием, соответствующим типу запроса из списка доступных для App роутера
. На данный момент поддерживаются GET
, POST
, PUT
, PATCH
, DELETE
, HEAD
и OPTIONS
. Рассмотрим простой пример с условным получением данных из MongoDB
из обработчика по пути app/productItems/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 })
}
Запрос через fetch(“/productItems”)
клиентского компонента вызовет обработчик на стороне сервера с доступом ко всем необходимым элементам авторизации.
Несколько советов для разработки
- Сохраняйте лаконичность роутов:
Хотя Next.js
позволяет гибко подходить к их наименованию, очень важно сохранять их краткость и осмысленность. Четкие названия и файловая структура улучшают читаемость кодовой базы и облегчают понимание приложения другими разработчиками.
- Разделяйте компоненты от логики:
Если вы не используете Typescript
, старайтесь давать компонентам расширение .jsx
, хоть и просто .js
также поддерживается. Это поможет вам и другим разработчикам быстро различать компоненты, составляющие интерфейс, от API роутов/обработчиков, функционала глобального состояния, кастомных хуков и другой абстрактной логики. В случае использования TypeScript
только .tsx
файлы будут восприниматься как компоненты.
- Оптимизация получения данных и рендеринга:
Выберите подходящий метод получения данных исходя из требований вашего приложения. Анализируйте, когда нужен getStaticProps()
для статического содержимого, а когда getServerSideProps()
для обновлений в Page роутере
. Отключайте кэширование запросов только при необходимости в App роутере
. Переводите компоненты в клиентские только при четкой необходимости взаимодействия с пользователем или состоянием клиентской части.
- Определяйте релевантность API в Next.js
Создавайте API роуты
или обработчики только при необходимости. Если у вас уже есть существующий бэкенд, создание дополнительных эндпоинтов в Next.js
сервере, которые будут вызывать эндпоинты внешнего сервера, зачастую может ухудшить производительность, не предоставляя ничего взамен. Одной из причин создания API
в Next.js
приложении при существующем бекенде может быть необходимость проводить сложные манипуляции с данными перед их использованием клиентской частью или отправкой в запросе от клиента. Такое происходит, если ваш внешний бэкенд предоставляется третьей стороной, и запрос изменений под цели фронтенда не всегда возможен. Но если внешний бэкенд разрабатывается вашей командой, то обычно все сложные манипуляции с данными будут выполняться там.
Будущее веб-разработки с Next.js
По мере роста спроса на высокопроизводительные веб-приложения, Next.js
играет ключевую роль в формировании будущего веб-разработки. Его способность легко интегрироваться с другими технологиями, а также возможность создания как собственного бэкенда, так и использования внешнего, укрепляют его позиции как универсального и перспективного решения. Новый рекомендуемый App роутер
продолжает упрощать разработку, предлагая шаблоны структуры проекта и компонентов для автоматической поддержки частых сценариев. Некоторые критики считают, что новое решение все еще нестабильно и не всегда совместимо с некоторыми популярными технологиями. Однако благодаря активному сообществу и регулярным обновлениям, Next.js
имеет все шансы продолжать развиваться и занимать лидирующие позиции в мире современной веб-разработки.
Заключение
Next.js
стал надежным, гибким и удобным для разработчиков фреймворком для создания высококлассных веб-приложений. Его уникальные особенности, включая легкую оптимизацию производительности и простую интеграцию с современными технологиями, делают его превосходным выбором для разработчиков, которые стремятся обеспечить исключительный пользовательский опыт, соблюдая высокие стандарты производительности. Независимо от того, начинаете ли вы новый проект или стремитесь улучшить уже существующий, Next.js
является мощным инструментом, который заслуживает вашего внимания в области веб-разработки.