Демистификация каталога приложений, потоковой передачи, приостановки и гибридных серверных и клиентских компонентов — с небольшой помощью GraphQL + WunderGraph.

Next.js 13 полностью меняет правила игры.

Это сделало создание Интернета следующего поколения намного, намного более интуитивным для разработчиков, реализовав передовые функции React 18 через новый каталог приложений, такие как Серверные компоненты, встроенную поддержку async/await (наконец-то!) и потоковую передачу HTML — с помощью упрощенные вложенные маршруты/макеты с помощью маршрутизации на основе папок и улучшенный DX (и бесконечно лучшая безопасность типов!) при выполнении SSR/SSG/ISR с его расширенным API выборки.

Раскрытие каждой блестящей, передовой новой функции в Next.js 13 заняло бы больше времени, чем у нас есть, поэтому сегодня давайте быстро поговорим о самой важной части буквально любого приложения, которое вы когда-либо разрабатывали, — история получения данных. — создание этого браузерного приложения каталога Record Store поверх базы данных Postgres в качестве учебного упражнения. О, и мы будем использовать GraphQL (через WunderGraph) для получения данных из нашей базы данных, потому что мы не ставим под угрозу опыт разработчиков в этих частях.

Что не так с ванильным SSR?

Чтобы понять проблему получения данных с использованием SSR в Next.js 12 и ниже, давайте сначала поговорим о последовательности событий, которые должны произойти для передачи данных с сервера клиенту.

  • Во-первых, при получении запроса от клиента на определенную страницу сервер извлекает необходимые данные (из базы данных, API, откуда угодно).
  • Затем сервер отображает HTML для страницы.
  • Визуализированный пакет HTML и JavaScript для страницы отправляется клиенту.
  • Наконец, React увлажняет страницу, чтобы сделать ее интерактивной.

Проблема в том, что эти шаги являются последовательными и блокирующими.

Сервер не может отобразить HTML-код страницы, пока все данные не будут получены, а браузер пользователя не сможет заполнить страницу с помощью JavaScript до тех пор, пока код для каждого компонента на страница была загружена. Таким образом, всегда будет заметная задержка между тем, когда клиент запрашивает страницу, и тем, когда она поступает, полностью визуализированная и интерактивная.

Это (не)известная проблема водопада данных. Теперь вы можете смягчить часть этой проблемы с помощью разделения кода (динамический импорт) или предварительной выборки данных для определенных маршрутов (с помощью предварительной выборки компонента <Link>)... но есть более прямое решение.

Что, если бы проход рендеринга можно было прервать? Что, если бы вы могли приостановить/возобновить/отказаться от рендеринга в процессе, сохраняя при этом согласованный и производительный пользовательский интерфейс?

Это именно то, что теперь позволяет вам делать Next.js 13 (основанный на бета-функциях React 18, а именно Concurrent Rendering).

Раньше вы не могли распараллелить конвейер рендеринга, но теперь вы можете вместо этого постепенно отправлять фрагменты HTML с сервера на клиент — сервер поддерживает соединение открытым, отправляя поток пользовательского интерфейса по мере его рендеринга на внешний интерфейс — мгновенный рендеринг частей страницы, которым не требуются данные, а затем постепенная потоковая передача в разделах, которые требуют данных, по мере разрешения их зависимостей данных. Это позволяет отображать части страницы раньше, не дожидаясь загрузки всех данных, прежде чем можно будет отобразить какой-либо пользовательский интерфейс.

Сокращение времени до первого байта (TTFB) и первой отрисовки содержимого (FCP) таким образом обеспечивает лучшее взаимодействие с пользователем, особенно при более медленных соединениях и устройствах с низким энергопотреблением.

Потоковое SSR — с практическими рекомендациями.

Next.js 13 предлагает два способа реализации Streaming SSR. Давайте посмотрим на оба, в коде.

В качестве примера я буду использовать источник данных Postgres (знаменитая база данных Chinook), используя WunderGraph, чтобы сделать его доступным через JSON-RPC для моего внешнего интерфейса Next.js.

Теперь для запросов к этой базе данных вы можете использовать любой удобный для вас метод — я использую WunderGraph. Это сумасшедшая огромная победа DX при использовании в качестве шлюза API или Backend-for-Frontend — самоанализ ваших источников данных (независимо от того, являются ли они федерациями Apollo, API-интерфейсами OpenAPI-REST, API-интерфейсами GraphQL, реляционными или основанными на документах базами данных; вот полный список) и консолидация их в слой виртуального графа, для которого вы можете определить типобезопасные операции и доступ через JSON-RPC — используя GraphQL только как инструмент разработки, а не общедоступную конечную точку.

Давайте начнем.

Шаг 0A: Настройка базы данных

Во-первых, проще всего запустить базу данных Postgres с помощью Docker, и именно этим я здесь и занимаюсь. Но поскольку эти базы данных используют строки подключения TCP, вы можете использовать буквально любой хост Postgres, какой захотите, включая DBaaS, например, Railway.

docker run --name mypg -e POSTGRES_USER=myusername -e POSTGRES_PASSWORD=mypassword -p 5432:5432 -d postgres

Это создаст для вас контейнер Docker с именем mypg, с указанными вами именем пользователя и паролем, на порту 5432 (localhost), используя официальный postgres Docker Image (он загрузит его для вас, если у вас его нет локально)

Я предполагаю, что у вас будут свои собственные данные, чтобы согласиться с этим, но если у вас их нет, возьмите файл Chinook_PostgreSql.sql из официального репозитория Chinook здесь и запустите его (возможно, через клиент Postgres, такой как pgAdmin ) заполнить базу данных Chinook (это каталог музыкального магазина; исполнители, их альбомы, его песни, с некоторыми образцами транзакций и счетов-фактур)

Шаг 0B: Настройка WunderGraph + Next.js

Во-вторых, мы можем настроить как сервер WunderGraph, так и Next.js, используя интерфейс командной строки WunderGraph create-wundergraph-app, так что давайте сделаем именно это.

npx create-wundergraph-app my-project -E nextjs

Когда это будет сделано, перейдите в только что созданный каталог (вы же не оставили my-project как есть, не так ли?) и npm i && npm start. Перейдите к localhost:3000, и вы должны увидеть всплывающую стартовую заставку WunderGraph + Next.js с результатами примера запроса, что означает, что все прошло хорошо.

Шаг 0C: (необязательно) Настройка Tailwind CSS для стилей

Мне нравится CSS, ориентированный прежде всего на полезность, поэтому я использую Tailwind для стилизации в этом уроке. Инструкция по установке здесь.

Вы можете использовать любой другой подход к стилю, но помните, что решения по стилю, включая библиотеки компонентов, зависят от движков CSS-in-JS, таких как emotion, в настоящее время не работают с новой структурой каталогов приложений.

Шаг 1: Определение наших данных

Проверьте wundergraph.config.ts в каталоге .wundergraph в корневом каталоге.

const spaceX = introspect.graphql({
	apiNamespace: 'spacex',
	url: 'https://spacex-api.fly.dev/graphql/',
});

// configureWunderGraph emits the configuration
configureWunderGraphApplication({
 apis: [spaceX],
    //...
})

Видеть это? Вот как легко добавить свои источники данных в качестве зависимостей при использовании WunderGraph. Определите свои источники данных как переменные JavaScript/TypeScript, попросите WunderGraph изучить их, а затем добавьте их в конфигурацию вашего проекта в виде массива зависимостей. Подход «Конфигурация как код» означает, что ваш интерфейс может оставаться полностью независимым, что значительно упрощает адаптацию, обслуживание и итерацию для вас как разработчика.

Следуя этому шаблону, давайте добавим нашу базу данных Postgres.

const db = introspect.postgresql({
  apiNamespace: "db",
  databaseURL: `postgresql://${process.env.DB_PG_USER}:${process.env.DB_PG_PWD}@${process.env.DB_PG_HOST}:5432/chinook`,
});

// configureWunderGraph emits the configuration
configureWunderGraphApplication({
  apis: [db],
  //...
})

Я использую экземпляр Postgres по адресу localhost, но вы можете не использовать его, поэтому использование переменных ENV для моей строки подключения к БД обеспечивает динамичность этого руководства. Добавьте свое имя пользователя, пароль и значения хоста в .env.local соответственно — TL;DR: добавьте строку подключения к PostgreSQL как databaseURL здесь.

Сохраните этот файл конфигурации, и WunderGraph объединит эти данные в виртуальный график. Теперь вам нужно определить операции, которые вы хотите получить из него. WunderGraph позволяет получить точные отношения, которые вы хотите, за один раз (а также объединение данных из разных источников!) с легкой прогулкой с GraphQL.

Создайте файл AlbumById.graphql в .wundergraph/operations.

query ($albumId: Int!) {
  Album: db_findUniqueAlbum(where: { AlbumId: $albumId }) {
    Title
    Artist {
      Name
    }
    Track {
      TrackId
      Name
      Composer
      Milliseconds
    }
  }
}

Теперь, когда вы нажмете «Сохранить» в своей среде IDE, сервер WunderGraph создаст типы, необходимые для запросов и мутаций, которые вы определили для всех ваших источников данных, и сгенерирует собственный клиент Next.js с хуками typesafe, которые вы можете использовать в своем интерфейсе. для тех операций. Как это круто?!

Шаг 2. Сначала создайте его в Next.js 12

Next.js 13 создан для постепенного внедрения. У вас все еще могут быть части вашего приложения в каталоге старых страниц, но все, что вы поместите в каталог нового приложения, будет включено в бета-функции — вам просто нужно убедиться, что ни один из ваших маршрутов не конфликтует между ними.

Это дает нам уникальную возможность для этого руководства — создать наше приложение старомодным способом — с помощью Next.js 12 (и каталога pages)… а затем обновить его до Next.js 13 (используя каталог приложения), используя все преимущества вложенные макеты, гибридная архитектура (серверные компоненты и клиентские компоненты вместе) и потоковая передача + приостановка. Веселье!

Итак, давайте сначала избавимся от основной части работы.

1. _app.tsx

2. index.tsx

useQuery — это сгенерированный WunderGraph хук для выборки данных с безопасностью типов, который мы будем использовать для получения наших данных.

По сути, вы вызываете useQuery с объектом опций, указав –

  1. операция по имени (имя файла операции GraphQL, которую вы создали на предыдущем шаге),
  2. передать AlbumID как ввод (и наш пользовательский интерфейс формы позволяет вам увеличить это вместо того, чтобы вводить его вручную),

А в data получите обратно его вывод — список треков для указанного альбома с композитором и временем выполнения (в миллисекундах; вы можете использовать служебную функцию, чтобы преобразовать его в более читаемый Hh:Mm:Ss строковый формат)

3. DataTable.tsx

Здесь не на что смотреть, это просто отображает данные в причудливой таблице. Для полноты картины вот используемая служебная функция secondsToTime.

export default function secondsToTime(e: number) {
  const h = Math.floor(e / 3600)
      .toString()
      .padStart(2, "0"),
    m = Math.floor((e % 3600) / 60)
      .toString()
      .padStart(2, "0"),
    s = Math.floor(e % 60)
      .toString()
      .padStart(2, "0");

  return `${h}:${m}:${s}`;
}

Получил все это? Фух. Сделай передышку. Теперь пришло время заставить это работать для Next.js 13.

Шаг 3. Обновление до Next.js 13

Часть 1. Конфигурация

Во-первых, убедитесь, что у вас установлен Node.js версии 16.8 или выше.

Затем обновитесь до Next.js v13 (и React 18). Не забывайте ESLint, если вы его используете!

npm install next@latest react@latest react-dom@latest
# If using ESLint…
npm install -D eslint-config-next@latest

Наконец, выберите парадигму каталога приложений Next.js 13, явно заявив об этом в next.config.js.

/** @type  {import('next').NextConfig} */
const  nextConfig  = {
 experimental: {
  appDir:  true,
 },
};
module.exports  = nextConfig;

Часть 2. Макеты — ваш новый лучший друг

В Next.js 13 маршрутизация следует иерархии на основе папок. Вы используете папки для определения маршрутов и специальные файлы с зарезервированными именами — layout.js/tsx, page.js/tsx и loading.js/tsx — для определения пользовательского интерфейса, при этом page.js/tsx является минимально необходимым.

Как это работает? Во-первых, ваше содержимое _app.tsx (включая глобальные стили) будет перемещено в корень layout.tsx, который экспортирует функциональный компонент по умолчанию, скажем, RootLayout(), который служит макетом для всего сегмента маршрута. Если layout.tsx находится на корневом уровне, все страницы вашего приложения наследуют его.

Каждый другой сегмент маршрута, вложенный в него, может иметь свои собственные layout.tsx и page.tsx (это означает, что теперь у вас гораздо больше гибкости в создании сложных вложенных макетов без необходимости использования оболочек или жесткого глобального макета), и они по-прежнему будут наследовать макет своего родительского маршрута. сегментировать автоматически.

Каждый экспортированный компонент компоновки разумно избегает ненужных повторных визуализаций при навигации между одноуровневыми сегментами маршрута.

Кроме того, вы можете разместить все остальное, что нужно этому сегменту маршрута (модульные тесты, документы MDX, файлы сборника рассказов) в его каталоге. Нет больше грязного импорта.

Лучшая часть? Все в каталоге приложения по умолчанию является компонентом React Server. Это означает, что вы можете получать данные на уровне макета для каждого сегмента маршрута и передавать их для обработки. Вы не можете использовать хуки и API-интерфейсы браузера в этих серверных компонентах, но вам это и не нужно — теперь вы можете изначально асинхронизировать/ожидать данные непосредственно в серверных компонентах, не нуждаясь в очень небезопасном для типов getServerSideProps pattern — и явно задействуйте фрагменты вашего пользовательского интерфейса в клиентских компонентах везде, где вам действительно нужны интерактивные функции/перехватчики.

Итак, что это значит для приложения, которое мы создаем?

Это означает, что наш каталог app в конечном итоге будет выглядеть так.

Давайте рассмотрим это один за другим, повышая корневой уровень.

1. layout.tsx

2. head.tsx

Вы также можете разместить здесь любые внешние скрипты, которые вам нужны (как <Script>, импортированные из next/script).

3. app/albums/layout.tsx

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

4. app/albums/page.tsx

Не пугайтесь, если сегменты вашего маршрута page.tsx окажутся минимальными!

Часть 3. Потоковая служба SSR

Для фактической реализации Streaming HTML Next.js 13 предлагает два варианта.

Вариант А. Мгновенная загрузка состояний с помощью loading.tsx

Используйте специальный файл с именем loading.js/tsx, который позволяет указать каноническое состояние «Загрузка» для всех ваших данных, получаемых на уровне сегмента маршрута.

Идея проста: если один из ваших компонентов в сегменте маршрута еще не готов к отображению (нет данных, плохие данные, медленное соединение и т. д.), Next.js покажет пользовательский интерфейс, отображаемый этим файлом, вместо вашего данные, а затем автоматически замените их фактическим пользовательским интерфейсом + данными вашего компонента после завершения рендеринга последнего. Вся навигация является немедленной и прерываемой. Вы не блокируете ожидание полной загрузки содержимого маршрута перед переходом на другой маршрут.

Использование loading.tsx таким образом позволяет обозначить важные и некритические части страницы. Вы можете предварительно визуализировать важные фрагменты пользовательского интерфейса (индикаторы загрузки, такие как скелеты и счетчики, или изображение продукта, имя и т. д.), в то время как некритические фрагменты пользовательского интерфейса (комментарии, обзоры и т. д.) будут загружаться.

Это хороший UX-дизайн, потому что он позволяет пользователям понять, что что-то — что угодно — происходит, и что ваше приложение не зависает и/или не падает.

4. app/albums/[id]/loading.tsx

И ваш [id]/page.tsx настолько прост, насколько это возможно. Нет необходимости вручную выполнять условный рендеринг, проверять, не определены ли данные, и отображать загрузочные сообщения/спиннеры/скелеты, если это так.

Если вы хотите использовать обработчики извлечения данных с безопасным типом (которые используют SWR Vercel под капотом — useQuery, useMutation и т. д.), сгенерированные WunderGraph, вам нужно явно сделать это клиентским компонентом — с “use client” верхнего уровня. оператор, подобный прагме модуля.

5. app/albums/[id]/page.tsx

Хотите воспользоваться преимуществами React Server Component (намного более быстрая выборка данных, простая нативная асинхронность/ожидание, отсутствие отправки JS клиенту)? Без проблем! Используя конфигурации WunderGraph по умолчанию, каждая ваша операция (файл .graphql) отображается как JSON-RPC (HTTP) по адресу:

http://localhost:9991/app/main/operations/[operation_name]

Итак, теперь ваш [id]/page.tsx будет выглядеть так:

Вы можете напрямую импортировать типы, сгенерированные WunderGraph (здесь AlbumByIdResponseData; и вы даже можете импортировать AlbumByIdResponse, если хотите ввести сам ответ API!) даже если вы не используете клиентские хуки WunderGraph.

💡Это то, что делает WunderGraph полностью совместимым с платформами, которые не являются React или Next.js.

Кроме того… обратите внимание на это — первоклассная поддержка async/await на уровне компонентов. Спасибо, что наконец-то признали, что операции выборки данных в webdev по своей сути являются асинхронными операциями, React!

Вариант Б. Ручное определение границ приостановки с помощью <Suspense>

Думаете, что знаете лучше, чем Next.js, и хотите водить вручную, переключать передачи и все такое? Затем самостоятельно определите границы приостановки для детализированной потоковой передачи на уровне компонентов.

Нет больше loading.tsx. При таком подходе вашему [id].page/tsx нужно будет импортировать <Suspense> и установить границы приостановки вручную для каждой части пользовательского интерфейса с учетом данных, который необходимо отобразить на этой странице (в нашем случае это только для <DataTable> ).

💡Помните, однако, что, в отличие от loading.tsx, определение границ приостановки вручную задержит навигацию до тех пор, пока не загрузится новый сегмент.

Я просто использую здесь простое <p> Loading…</p>, но вы, конечно, можете определить более сложные резервные пользовательские интерфейсы — скелетоны, счетчики, пользовательские изображения. Импортируйте и используйте их в резерве по своему усмотрению!

Наконец, наш Form component практически не изменился — за исключением одного факта. Наша форма нуждается в интерактивности пользователя, используя хуки и API-интерфейсы браузера. Итак, если вы извлекаете его в отдельный компонент, явно сделайте его клиентским компонентом с “use client”

Наш DataTable можно использовать как есть. Однако обратите внимание, что при использовании сегментов динамического маршрута вы можете передавать параметры URL-адреса через реквизиты, эффективно передавая состояние через URL-адрес, даже если они являются серверными компонентами. Хороший шаблон для запоминания!

Давайте завершим это панелью навигации, которую мы будем использовать для переключения между версиями Next.js 12 и 13 того, что мы только что создали.

Обратите внимание на новый компонент Next 13 <Link>! Гораздо более интуитивно понятный, больше не нужен вложенный тег <a>.

Вот и все, Народ!

Запустите браузер, перейдите на localhost:3000 и попробуйте две версии вашего приложения, которые можно переключать через панель навигации: одна использует старый добрый Next.js 12 и каталог страниц, а другая использует Next.js 13. с Streaming SSR, Suspense и гибридной архитектурой серверных и клиентских компонентов — оба со сквозной безопасностью типов, включенной WunderGraph в качестве нашего шлюза API / BFF.

Next.js 13 еще не готов к промышленному использованию — черт возьми, даже Vercel говорит об этом в своей официальной документации Beta Next.js! Но прогресс Vercel в Next.js 13 был быстрым, его функции невероятно полезны для создания потрясающих, удобных и доступных пользовательских интерфейсов… и если вы используете WunderGraph, вы можете сделать все это без компрометация безопасности типов или опыт разработчика в придачу!

Получить раскрутил. У нас впереди несколько веселых, веселых дней как разработчиков полного стека.