Статическое построение, текущий запрос и потоковый вывод
Оглавление курса · Скачать все состояния проекта.
Учебная задача: Различить публичную фикстуру, данные запроса и границу Suspense.
Входное состояние: Разделены серверные страницы и интерактивные компоненты. Выходное состояние: Публичный учебный каталог имеет заранее заданные пути; сведения запроса выводятся отдельно. Продолжаем «Смену» — учебную диспетчерскую заявок мастерской. Нужны основы JavaScript, TypeScript, React и HTTP. Используем App Router, Next 16.4.0, React 19.3.0 и Node.js не ниже 20.9.0. Основные версии закреплены; диапазоны остальных пакетов ещё не разрешены установкой и lockfile отсутствует. Полный каталог lesson-11 в архиве соответствует окончанию этой главы. Выбирайте один снимок целиком: перенос отдельных файлов из будущих этапов меняет его зависимости и правила данных.
Исходники подготовлены редакционным чтением. Пакеты, сервер, сборка, миграции, примеры и внешнее размещение не выполнялись. Ответы страницы и изменения данных ниже являются ожидаемыми, example_execution: not_run.
Не все страницы получают данные с одинаковой частотой. Публичная учебная карточка может зависеть только от исходного массива. Рабочая область зависит от текущего запроса и позже от личности пользователя. Разделим эти примеры, чтобы не смешивать предварительное построение публичных данных с персональным ответом. Добавим отдельный /catalog/[id], который прямо обозначен как учебная фикстура, а не как запись рабочей базы.
Сначала полезно вспомнить, зачем сервер вообще строит интерфейс. При полностью клиентском подходе браузер должен получить код, выполнить его и запросить необходимые данные. Серверное построение позволяет передать начальное представление раньше, а интерактивный код подключать к нужным участкам. Server Components дополнительно задают границу исполняемого кода и источников данных. Эти идеи связаны, но SSR и RSC не являются двумя названиями одного механизма.
Пути, известные заранее
generateStaticParams() возвращает идентификаторы двух публичных фикстур. Функция описывает набор параметров, для которых можно подготовить страницы заранее. Это не список персональных заявок сотрудника и не способ угадать будущие записи базы. Значения формируются из исходного массива, который принадлежит учебному каталогу. После перехода рабочей части на PostgreSQL каталог продолжит показывать только этот публичный набор.
dynamicParams = false ограничивает неизвестные варианты. Адрес с id, которого нет в предварительном наборе, не должен автоматически стать новой динамической карточкой этого каталога. Такая политика подходит закрытому учебному перечню. Для постоянно пополняющегося публичного каталога мог бы потребоваться другой выбор. Для личных заявок предварительное перечисление пользователей вообще было бы неверной моделью доступа.
Значение revalidate = 3600 описывает интервал обновления данного маршрута в используемом договоре без Cache Components. Оно не превращает исходный массив в живую базу: если данные принадлежат версии кода, новый текст всё равно связан с изменением исходника. Также интервал не обещает, что каждый посетитель увидит новую страницу точно в определённую секунду. Механизм обновления и фактический источник данных нужно рассматривать вместе.
API текущего запроса
Компонент RequestSummary ожидает headers(). Эти сведения принадлежат конкретному запросу, поэтому рабочий ответ нельзя считать одной одинаковой персональной страницей для всех посещений. Показываем сокращённое значение языка из заголовка, обозначая его учебную роль. Не раскрываем cookie, токены или весь набор заголовков. Заголовок языка также не является доказательством личности или географического положения человека.
Корневая публичная фикстура и рабочая область теперь имеют разные основания построения. Наличие Server Component само по себе не доказывает статический режим. На решение влияют источники данных и API запроса. Аналогично Client Component не отменяет возможность первоначального серверного представления. Для чтения проекта полезнее проследить, какие значения требуются до завершения ответа и где они доступны.
Suspense определяет ожидающую часть
В компоновке сведения запроса находятся внутри Suspense с коротким fallback. Пока асинхронная часть не готова, окружающая оболочка может сохранять собственное представление. Так потоковый вывод позволяет разделить участки ответа по готовности. Однако простое наличие Suspense не гарантирует заметную задержку или измеренное ускорение. Если операция завершается быстро, запасной интерфейс может быть почти незаметен.
Не добавляем искусственный таймер ради впечатляющей анимации. Учебная задача состоит в месте границы и зависимости данных, а не в демонстрации выдуманных секунд ожидания. Если внутри этой части возникнет исключение, понадобится соответствующая граница ошибок выше неё. Suspense описывает ожидание, а не общий способ скрыть все отказы. Это продолжает различие состояний предыдущего урока.
Кэш данных, повторное использование результата в рамках запроса, кэш маршрута и состояние клиентской навигации относятся к разным уровням. Например, React cache помогает переиспользовать вызов в серверном запросе, но не означает бессрочное публичное хранение личного результата. Явный fetch с настройками определяет собственную политику внешнего запроса. Название «кэш» без указания слоя недостаточно для решения о свежести и доступе.
В серии не включён флаг cacheComponents, поэтому не переносим директиву use cache в произвольный файл. У этого современного режима собственный договор и API времени жизни. Выбранное локальное состояние использует более узкий пример публичного маршрута и явные политики получения дальше. В одном приложении нельзя без объяснения смешивать настройки разных режимов только из-за похожего назначения.
Ожидаемые различия
Для /catalog/1 ожидается публичный учебный заголовок; для неизвестного пути каталог не создаёт персональную запись. В рабочей области сведения языка принадлежат текущему запросу и могут отличаться. Ни предварительная генерация, ни Suspense здесь не выполнялись на сборке. Результаты описаны по исходникам и официальному договору.
Для самостоятельного разбора подпишите источник каждой величины: id каталога из массива, язык из запроса, тема из браузера. Затем объясните, какие из них можно безопасно разделить между посетителями. Этот вопрос подготовит подключение персональной базы: кэширование должно сохранять границы пользователя, а потоковый интерфейс должен показывать правдивое состояние ожидания.
Проверьте смысл границ
Заголовок Accept-Language может отсутствовать или содержать длинный набор предпочтений. Мы не используем его для выбора прав и ограничиваем показ длиной. Отсутствие даёт обычный текст «не указан», а не ошибку документа. Так учебный компонент показывает правильную работу с необязательным входом запроса.
Статический каталог продолжает использовать фиксированные данные даже после появления PostgreSQL. Такое разделение предотвращает случайное включение пользовательских заявок в предварительно подготовленную публичную страницу. При расширении проекта каждый новый источник должен получить своё решение о доступе и свежести, прежде чем появится оптимизация общего хранения.
Полные файлы этого шага
Следующие файлы показаны полностью. Остальные согласованные компоненты и настройки находятся в том же каталоге архива; сохраняйте их вместе при самостоятельном прохождении.
app/catalog/[id]/page.tsx
import { notFound } from "next/navigation";
import { sampleTickets } from "@/lib/tickets";
export const dynamicParams = false;
export const revalidate = 3600;
export function generateStaticParams() { return sampleTickets.map(item => ({ id: String(item.id) })); }
export default async function CatalogCard({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const item = sampleTickets.find(row => String(row.id) === id);
if (!item) notFound();
return <main><h1>Публичный учебный пример</h1><p>{item.title}</p><p>Это фикстура, а не персональная заявка из базы.</p></main>;
}
components/request-summary.tsx
import { headers } from "next/headers";
export async function RequestSummary() {
const requestHeaders = await headers();
return <aside>Язык запроса: {requestHeaders.get("accept-language")?.slice(0, 80) || "не указан"}. Это серверная часть текущего запроса.</aside>;
}
app/(work)/layout.tsx
import { Suspense } from "react";
import { RequestSummary } from "@/components/request-summary";
import Link from "next/link";
import type { ReactNode } from "react";
import { ThemeProvider, ThemeButton } from "@/components/theme-provider";
import { Nav } from "@/components/nav";
export default function WorkLayout({ children }: { children: ReactNode }) {
return <ThemeProvider><header><Link href="/">Смена</Link><Nav /><ThemeButton /></header><Suspense fallback={<p role="status">Получаем сведения запроса…</p>}><RequestSummary /></Suspense>{children}</ThemeProvider>;
}
Документация и исходники
Договор этой главы раскрыт в официальной документации. Сопоставьте её API с выбранной версией и с объяснённым источником каждого значения. Архив «Смены» содержит двадцать полных состояний и условия самостоятельной подготовки. Ожидаемый результат оценивается по текущей главе: поздние проверки владельца, база и внешние ключи не являются неявно выполненными предпосылками раннего учебного состояния.