Next.js изнутри. Часть 5. Серверный слой.

Next.js изнутри. Часть 5. Серверный слой.


Введение

В прошлых частях мы разобрали Pages и App Router, но весь этот разговор начинался уже внутри рендеринга. На самом деле до рендера запрос проходит через целый слой, который мы лишь мельком упомянули в первой части: матчинг маршрута, middleware, проверку файловой системы, отдачу статики.

В этой части мы посмотрим на этот слой. Проследим путь запроса от момента, когда он пришёл на порт, до момента, когда вызывается рендер. Главное, что стоит держать в голове с самого начала: next start поднимает два логических слоя. Один отвечает за маршрутизацию, второй — за рендеринг.

Колбэк на HTTP-сервере

В основе своей Next.js не делает ничего экзотического с сетью. Когда вы запускаете next start, фреймворк создаёт обычный Node.js HTTP-сервер и навешивает на него один обработчик запросов. Всё, что Next.js умеет — роутинг, рендеринг, кеширование — это то, что происходит внутри этого обработчика. Снаружи это просто функция (req, res), как в любом сервере на голом Node.

Из этого следует и то, как Next.js встраивается в чужую инфраструктуру: если у вас уже есть HTTP-сервер, Next.js может отдать вам свой обработчик, и вы навесите его сами.

Любопытная деталь старта: сервер начинает слушать порт раньше, чем готова вся внутренняя машинерия. Запросы, которые прилетели в этот момент, не отбрасываются, а дожидаются, пока инициализация завершится, и только потом обрабатываются. Это сделано ради того, чтобы сокет открывался как можно быстрее и ни один ранний запрос не потерялся.

Два слоя: router-server и render-server

Теперь к главному. Запустив HTTP-сервер, Next.js поднимает над ним два слоя, у которых принципиально разные задачи.

Первый слой router-server — это маршрутизатор. Он принимает сырой запрос и решает, что с ним делать: применить редирект из конфига, прогнать через middleware, переписать URL по правилу rewrite, отдать статический файл с диска или, если речь о настоящей странице, передать запрос дальше — на рендеринг. Сам router-server ничего не рендерит. Он не знает про React, RSC и гидратацию.

Второй слой — render-server. Это тот самый сервер, который мы неявно обсуждали в предыдущих частях: он берёт разрешённый маршрут и превращает его в HTML или Flight-поток. Внутри render-server живёт BaseServer и его реализация для Node.js, про которые шла речь в первой части.

Дальше мы рассмотрим подробнее router-server — именно там происходит всё интересное до рендера.

Порядок обработки запроса

Когда запрос попадает в router-server, он прогоняется через фиксированную последовательность шагов, которую можно сложить в такую лестницу:

  1. Заголовки из next.config — добавляются к ответу.
  2. Редиректы из конфига — если совпало, запрос разворачивается.
  3. Middleware — если URL подходит под матчер, запускается наш код.
  4. Rewrites группы beforeFiles из next.config — переписывание URL до проверки файлов.
  5. Проверка файловой системы — точное совпадение с реальным маршрутом или статическим файлом.
  6. Rewrites группы afterFiles из next.config — переписывание URL, если по файлам ничего не нашлось.
  7. Динамические маршруты и повторная проверка.
  8. Rewrites группы fallback из next.config — последний шанс переписать URL.

Этот порядок объясняет целый класс вопросов, например, почему middleware видит запрос раньше, чем срабатывает редирект из вашего кода внутри страницы.

В минимальном режиме — так Next.js работает на serverless-платформах, где маршрутизацию берёт на себя сама платформа — почти все эти шаги отключены: редиректы, заголовки и rewrites уже применены инфраструктурой платформы. Router-server в этом режиме занимается в основном тем, что доводит запрос до рендера.

Что сервер отдаёт сам

Важный момент: далеко не каждый запрос доходит до render-server. На шаге проверки файловой системы router-server сверяет путь с тем, что у него есть на диске, и для целого ряда случаев отвечает сам, не запуская рендеринг вообще.

Чтобы понимать, что есть на диске, на старте router-server считывает служебную информацию о сборке: идентификатор сборки, списки страниц и роут-хендлеров, содержимое папки public, правила для middleware. Для всего этого сервер использует для этого уже знакомые нам манифесты.

Существует развилка по типу совпадения:

  • Запрос к /_next/static/... — это собранные при билде чанки. Они отдаются как статические файлы напрямую, без какого-либо участия React.
  • Запрос к файлу из папки public — тоже отдаётся как статика.
  • Запрос к оптимизируемой картинке уходит в оптимизатор изображений.
  • И только запрос, который совпал с настоящей страницей или роут-хендлером, передаётся дальше — в рендеринг.

API-эндпоинты

До сих пор мы говорили про то, что Next.js делает сам. Но часть маршрутов разработчик описывает руками — это API-эндпоинты. Тут полезно понять, как наш код подключается к слою, который мы только что разобрали. В Next.js есть две модели написания эндпоинтов, и они довольно разные.

Route Handlers — современная модель App Router. Мы создаём файл route.ts и экспортируем из него функции, названные по HTTP-методам: GET, POST, DELETE и так далее. На сборке Next.js оборачивает этот файл в служебный модуль, который собирает из наших экспортов таблицу «метод → функция». Когда приходит запрос, модуль смотрит на его HTTP-метод и вызывает соответствующий обработчик.

API Routes — старая модель Pages Router. Здесь мы экспортируем один обработчик по умолчанию — функцию (req, res). Next.js прогоняет её через свою обвязку, которая досыпает в req и res удобные хелперы: разбор тела запроса, чтение кук, методы вроде res.json(). Это классический Node-стиль: мы получаем запрос и ответ Node.js, слегка обогащённые, и сами пишем в ответ.

С точки зрения router-server API-эндпоинт и страница — это один и тот же тип «выхода»: динамический маршрут, который надо передать в render-server. Просто эндпоинт рендерится не в HTML, а в ответ, который мы сформировали руками.

Middleware (proxy)

Вторая точка, где код разработчика врезается в серверный слой — это middleware (в свежих версиях его постепенно переименовывают в proxy). Мы описываем в нём правило — для каких путей он должен срабатывать. На сборке это правило превращается в набор регулярных выражений, которые ложатся в служебную карту (middleware-manifest). В рантайме на третьем шаге router-server сверяет путь запроса с этими выражениями, и если совпало — запускает наш код. Правило может быть и сложнее простого совпадения по пути: можно требовать наличия определённого заголовка или куки, либо наоборот их отсутствия.

Дальше наша функция получает запрос и возвращает ответ: «пропустить дальше», «переписать URL», «сделать редирект», «добавить заголовок». Но middleware не вызывает маршрутизатор напрямую. Вместо этого его решение кодируется в специальные служебные заголовки ответа.

Router-server читает эти служебные заголовки обратно и поступает согласно им: разворачивает редирект, подменяет путь, прокидывает изменённые заголовки в следующий шаг.

У middleware есть и важное ограничение: он всегда исполняется на Edge, а не в полноценном Node.js. Причина в его роли: middleware стоит на пути всех запросов, подходящих под его правило, ещё до того, как стало понятно, что вообще с запросом делать. На таком горячем участке тяжёлая среда исполнения бы сильно резала производительность.

Передача в рендер

Допустим, запрос прошёл всю лестницу: его не развернул редирект, middleware пропустил дальше, по файловой системе он совпал с настоящей страницей. Теперь router-server передаёт его в render-server.

Router-server уже проделал всю работу по разрешению маршрута: он знает финальный путь и параметры запроса. Эту разрешённую информацию он добавляет как служебные метаданные и зовёт обработчик render-server. Тот берёт готовый разрешённый маршрут и сразу идёт в пайплайн рендера, тот самый, что мы разбирали в предыдущих частях.

При этом render-server — это не финальная инстанция. Router-server вызывает его так же, как вызывал любой другой шаг лестницы. И если render-server возвращает «этот путь отрендерить не удалось» (например, для динамического маршрута без предрендера и без fallback), router-server не считает это концом: последнее слово остаётся за ним, и он просто продолжает лестницу со следующего шага, пробуя подобрать другой выход.

Итого

Серверный слой Next.js — это два слоя с разделением труда. Router-server занимается маршрутизацией: прогоняет запрос через фиксированную лестницу из заголовков, редиректов, middleware, rewrites и проверки файловой системы; статику, ассеты и картинки отдаёт сам; а до рендера доводит только то, что действительно нужно рендерить. Render-server — отдельный слой, который берёт уже разрешённый маршрут и превращает его в ответ. Код разработчика встраивается в эту трубу в двух местах: эндпоинты и middleware.

Report Page