Получайте правильные данные с помощью Next.js и React SSR

Получайте правильные данные с помощью Next.js и React SSR

8 мая 2022 г.

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


Это то, что мой друг недавно сказал в дискуссии. Почему в NextJS нет простого способа универсальной выборки данных? Чтобы ответить на этот вопрос, давайте рассмотрим проблемы, связанные с универсальной выборкой данных в NextJS.


Но сначала, что на самом деле представляет собой универсальная выборка данных?


***Отказ от ответственности: это будет длинная и подробная статья.


Он будет охватывать много вопросов и довольно глубоко вдаваться в детали.***


Если вы ожидаете легкий маркетинговый блог, эта статья не для вас.


Универсальная выборка данных NextJS


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


Этот хук для выборки данных должен работать везде в вашем приложении без какой-либо дополнительной настройки.


Вот пример, наверное, самый сложный, но я слишком взволнован, чтобы не поделиться им с вами.


Это хук «универсальная подписка».


```машинопись


const PriceUpdates = () => {


константные данные = useSubscription.PriceUpdates();


возврат (


<дел>


Универсальная подписка


{JSON.stringify(данные)}



Хук «PriceUpdates» генерируется нашей структурой, поскольку мы определили файл «PriceUpdates.graphql» в нашем проекте. Что особенного в этом крючке? Вы можете свободно размещать React Component в любом месте своего приложения. По умолчанию сервер отображает первый элемент из подписки. Отображенный сервером HTML-код будет отправлен клиенту вместе с данными.


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


Определите необходимые данные, написав операцию GraphQL, а платформа позаботится обо всем остальном. Имейте в виду, что мы не пытаемся скрыть факт осуществления сетевых вызовов.


Что мы здесь делаем, так это возвращаем фронтенд-разработчикам их продуктивность. Вам не следует беспокоиться о том, как извлекаются данные, как защитить уровень API, какой транспорт использовать и т. д. Это должно просто работать.


Почему выборка данных в NextJS такая сложная?


Если вы какое-то время использовали NextJS, у вас может возникнуть вопрос, что именно должно быть сложным в извлечении данных?


В NextJS вы можете просто определить конечную точку в каталоге «/api», которую затем можно вызвать с помощью «swr» или просто «fetch».


Правильно, что "Hello, world!" Пример извлечения данных из «/api» действительно прост, но масштабирование приложения за пределы первой страницы может быстро перегрузить разработчика. Давайте рассмотрим основные проблемы выборки данных в NextJS.


getServerSideProps работает только с корневыми страницами


По умолчанию единственным местом, где вы можете использовать асинхронные функции для загрузки данных, необходимых для рендеринга на стороне сервера, является корень каждой страницы. Вот пример из документации NextJS:


```машинопись


функция Страница({данные}) {


// Отрисовка данных...


// Это вызывается при каждом запросе


экспортировать асинхронную функцию getServerSideProps() {


// Получаем данные из внешнего API


const res = ожидание выборки(https://.../data)


константные данные = ожидание res.json()


// Передаем данные на страницу через пропсы


вернуть {реквизит: {данные}}


экспорт страницы по умолчанию


Представьте себе веб-сайт с сотнями страниц и компонентов. Если вам нужно определить все зависимости данных в корне каждой страницы, как вы узнаете, какие данные действительно необходимы, до рендеринга дерева компонентов?


В зависимости от данных, которые вы загрузили для корневых компонентов, некоторая логика может решить полностью изменить дочерние компоненты. Я разговаривал с разработчиками, которым приходится поддерживать большие приложения NextJS.


Они четко заявили, что выборка данных в «getServerSideProps» плохо масштабируется при большом количестве страниц и компонентов.


Аутентификация усложняет выборку данных


Большинство приложений имеют какой-либо механизм аутентификации. Некоторый контент может быть общедоступным, но что, если вы хотите персонализировать веб-сайт?


Будет необходимость отображать разный контент для разных пользователей. Когда вы визуализируете пользовательский контент только на клиенте, вы замечали этот уродливый эффект «мерцания» при поступлении данных?


Если вы визуализируете только пользовательский контент на клиенте, вы всегда получите эффект, что страница будет повторно отображаться несколько раз, пока она не будет готова. В идеале наши хуки для извлечения данных должны изначально поддерживать аутентификацию.


Типовая безопасность необходима, чтобы избежать ошибок и сделать разработчиков продуктивнее


Как мы видели в приведенном выше примере с использованием «getServerSideProps», нам нужно предпринять дополнительные действия, чтобы сделать наш уровень API типобезопасным. Не лучше ли было бы, чтобы обработчики выборки данных были типобезопасными по умолчанию?


Подписки не могут быть отображены на сервере, могут ли они?


До сих пор я никогда не видел никого, кто применил бы рендеринг на стороне сервера в NextJS к подпискам. Но что, если вы хотите отображать цену акций на сервере для SEO и повышения производительности, но также хотите иметь подписку на стороне клиента для получения обновлений?


Конечно, вы можете использовать запрос Query/GET на сервере, а затем добавить подписку на клиенте, но это сильно усложняет работу. Должен быть более простой способ!


Что должно произойти, если пользователь покинет окно и снова войдет в него?


Возникает еще один вопрос: что должно произойти, если пользователь покинет окно и снова войдет в него. Должны ли подписки быть остановлены или продолжать передавать данные?


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


Должны ли мутации влиять на другие обработчики выборки данных?


Довольно часто мутации оказывают побочные эффекты на другие ловушки для получения данных. Например. у вас может быть список задач. Когда вы добавляете новую задачу, вы также хотите обновить список задач. Следовательно, ловушки для выборки данных должны иметь возможность обрабатывать такие ситуации.


Что насчет ленивой загрузки?


Другой распространенный шаблон — ленивая загрузка. Вы можете захотеть загружать данные только при определенных условиях, например. когда пользователь прокручивает страницу вниз или когда пользователь нажимает кнопку. В таких случаях наши обработчики выборки данных должны иметь возможность отложить выполнение выборки до тех пор, пока данные действительно не понадобятся.


Как мы можем предотвратить выполнение запроса, когда пользователь вводит поисковый запрос?


Еще одним важным требованием к хукам для выборки данных является предотвращение дребезга при выполнении запроса. Это сделано для того, чтобы избежать ненужных запросов к серверу. Представьте себе ситуацию, когда пользователь вводит поисковый запрос в поле поиска.


Вы действительно должны делать запрос к серверу каждый раз, когда пользователь набирает письмо? Мы увидим, как мы можем использовать устранение дребезга, чтобы избежать этого и сделать наши ловушки для выборки данных более производительными.


Краткий обзор самых больших проблем при создании хуков для выборки данных для NextJS


  1. getServerSideProps работает только на корневых страницах

  1. ловушки для получения данных с аутентификацией

  1. безопасность типов

  1. подписки и SSR

  1. фокус окна и размытие

  1. побочные эффекты мутаций

  1. ленивая загрузка

  1. устранение дребезга

Это подводит нас к 8 основным проблемам, которые нам нужно решить.


Давайте теперь обсудим 21 шаблон и лучшие практики решения этих проблем.


==21 шаблон и рекомендации по решению основных 8 основных проблем хуков выборки данных для NextJS==


Если вы хотите следовать этим паттернам и испытать их на себе, вы можете [клонировать этот репозиторий и поэкспериментировать] (https://github.com/wundergraph/wundergraph-demo). Для каждого шаблона есть [выделенная страница в демонстрации] (https://github.com/wundergraph/wundergraph-demo/tree/main/nextjs-frontend/pages/patterns).


Запустив демоверсию, вы можете открыть браузер и найти обзор паттернов на


http://localhost:3000/шаблоны.


Вы заметите, что мы используем GraphQL для определения наших ловушек для выборки данных, но реализация на самом деле не специфична для GraphQL. Вы можете применять те же шаблоны с другими стилями API, такими как REST, или даже с пользовательским API.


1. Клиентский пользователь


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


Вот хук для получения текущего пользователя:


```машинопись


использоватьЭффект(() => {


если (отключитьFetchUserClientSide) {


возврат;


const abort = новый AbortController();


если (пользователь === ноль) {


(асинхронный () => {


пытаться {


const nextUser = await ctx.client.fetchUser(abort.signal);


если (JSON.stringify(следующий пользователь) === JSON.stringify(пользователь)) {


возврат;


установить пользователя (следующий пользователь);


} поймать (е) {


возврат () => {


прервать.прервать();


}, [disableFetchUserClientSide]);


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


Важно всегда передавать контроллер прерывания клиенту, иначе мы можем столкнуться с утечкой памяти. Возвращающая функция стрелки вызывается, когда компонент, содержащий хук, размонтирован.


Вы заметите, что мы используем этот шаблон во всем нашем приложении для правильной обработки потенциальных утечек памяти. Давайте теперь рассмотрим реализацию «client.fetchUser».


```машинопись


public fetchUser = async (abortSignal?: AbortSignal, перепроверить?: логическое значение): Promise | ноль> => {


пытаться {


const revalidateTrailer = revalidate === undefined ? "" : "?revalidate=true";


const response = await fetch(this.baseURL + "/" + this.applicationPath + "/auth/cookie/user" + revalidateTrailer, {


заголовки: {


...эти.дополнительные заголовки,


"Тип контента": "приложение/json",


"WG-SDK-Версия": this.sdkVersion,


метод: "ПОЛУЧИТЬ",


учетные данные: "включить",


режим: "корс",


сигнал: прерывание сигнала,


если (ответ.статус === 200) {


вернуть ответ.json();


} ловить {


вернуть ноль;


Вы заметите, что мы не отправляем никаких учетных данных клиента, токена или чего-либо еще. Мы неявно отправляем защищенный, зашифрованный файл cookie только для http, который был установлен сервером, к которому у нашего клиента нет доступа.


Для тех, кто не знает, файлы cookie только http автоматически прикрепляются к каждому запросу, если вы находитесь в том же домене. Если вы используете HTTP/2, клиент и сервер также могут применять сжатие заголовков, что означает, что файл cookie не нужно отправлять в каждом запросе, поскольку и клиент, и сервер могут согласовывать карту известного значения ключа заголовка. пары на уровне соединения.


Шаблон, который мы используем за кулисами, чтобы упростить аутентификацию, называется «Шаблон обработчика маркеров». Шаблон обработчика токенов — это наиболее безопасный способ проверки подлинности в современных приложениях JavaScript.


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


Это потому, что наш «бэкенд» действует как проверяющая сторона OpenID Connect Connect. Что такое доверяющая сторона, спросите вы? Это приложение с клиентом OpenID Connect, которое передает аутентификацию третьей стороне.


Поскольку мы говорим в контексте OpenID Connect, наш «бэкэнд» совместим с любым сервисом, реализующим протокол OpenID Connect. Таким образом, наш бэкенд может обеспечить бесперебойную аутентификацию, в то время как разработчики могут выбирать между различными поставщиками удостоверений, такими как Keycloak, Auth0, Okta, Ping Identity и т. д. Как выглядит процесс аутентификации с точки зрения пользователей?


  1. Пользователь нажимает «Войти».

  1. Фронтенд перенаправляет пользователя на бэкенд (проверяющую сторону).

  1. Серверная часть перенаправляет пользователя к поставщику удостоверений.

  1. Пользователь аутентифицируется у поставщика удостоверений.

  1. Если аутентификация прошла успешно, поставщик удостоверений перенаправляет пользователя обратно на серверную часть.

  1. Затем серверная часть обменивает код авторизации на токен доступа и идентификации.

  1. Токен доступа и удостоверения используются для установки на клиенте безопасного зашифрованного файла cookie только по протоколу HTTP.

  1. С установленным файлом cookie пользователь перенаправляется обратно во внешний интерфейс.

Отныне, когда клиент вызывает метод fetchUser, он автоматически отправляет файл cookie серверу. Таким образом, внешний интерфейс всегда имеет доступ к информации о пользователе, когда он вошел в систему. Если пользователь нажимает кнопку «Выйти», мы вызываем функцию на внутреннем интерфейсе, которая сделает файл cookie недействительным.


Все это может быть очень сложно переварить, поэтому давайте подытожим основные моменты. Во-первых, вы должны сообщить бэкенду, с какими поставщиками удостоверений работать, чтобы он мог действовать как сторона рейлинга.


Как только это будет сделано, вы сможете инициировать поток аутентификации из внешнего интерфейса, получить текущего пользователя из внутреннего интерфейса и выйти из системы. Если мы поместим этот вызов «fetchUser» в хук «useEffect», который мы поместим в корень каждой страницы, мы всегда будем знать, кто является текущим пользователем.


Однако есть одна загвоздка. Если вы откроете демоверсию и перейдете к клиентскому-пользователю страница,


вы заметите эффект мерцания после загрузки страницы, потому что на клиенте происходит вызов fetchUser. Если вы посмотрите на Chrome DevTools и откроете предварительный просмотр страницы, вы заметите, что страница отображается с объектом пользователя, для которого установлено значение «null».


Вы можете нажать кнопку входа, чтобы начать процесс входа. После завершения обновите страницу, и вы увидите мерцающий эффект. Теперь, когда вы понимаете механику шаблона обработчика токенов, давайте посмотрим, как можно убрать мерцание при загрузке первой страницы.


2. Серверный пользователь


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


Итак, как нам получить доступ к объекту пользователя на стороне сервера? Помните, что все, что у нас есть, это файл cookie, прикрепленный к домену. Допустим, наш бэкэнд работает на api.example.com, а внешний интерфейс работает на www.example.com или example.com. Если есть одна важная вещь, которую вы должны знать о файлах cookie, так это то, что вам разрешено устанавливать файлы cookie на родительских доменах, если вы находитесь на поддомене.


Это означает, что после завершения процесса аутентификации серверная часть НЕ ДОЛЖНА устанавливать файл cookie в домене api.example.com. Вместо этого он должен установить для файла cookie домен «example.com».


При этом файл cookie становится видимым для всех поддоменов «example.com», включая «www.example.com», «api.example.com» и сам «example.com».


Между прочим, это отличный шаблон для реализации единого входа.


У вас есть пользователи, которые входят в систему один раз, и они аутентифицируются на всех поддоменах.


WunderGraph автоматически устанавливает файлы cookie для родительского домена, если серверная часть находится на поддомене, поэтому вам не нужно об этом беспокоиться. Теперь вернемся к тому, чтобы получить пользователя на стороне сервера. Чтобы получить пользователя на стороне сервера, мы должны реализовать некоторую логику в методе getInitialProps наших страниц.


```машинопись


WunderGraphPage.getInitialProps = асинхронный (ctx: NextPageContext) => {


// ... опущено для краткости


const cookieHeader = ctx.req?.headers.cookie;


если (typeof cookieHeader === "строка") {


defaultContextProperties.client.setExtraHeaders({


Куки: cookieHeader,


пусть ssrUser: User | ноль = ноль;


если (параметры?.disableFetchUserServerSide !== true) {


пытаться {


ssrUser = await defaultContextProperties.client.fetchUser();


} поймать (е) {


// ... опущено для краткости


return {...pageProps, ssrCache, пользователь: ssrUser};


Объект ctx функции getInitialProps содержит запрос клиента, включая заголовки.


Мы можем сделать «волшебный трюк», чтобы «API-клиент», который мы создаем на стороне сервера, мог действовать от имени пользователя. Поскольку и интерфейс, и серверная часть используют один и тот же родительский домен, у нас есть доступ к файлу cookie, установленному серверной частью.


Итак, если мы возьмем заголовок cookie и установим его в качестве заголовка «Cookie» клиента API, клиент API сможет действовать в контексте пользователя даже на стороне сервера! Теперь мы можем получить пользователя на стороне сервера и передать объект пользователя вместе с pageProps в функцию рендеринга страницы.


Не пропустите этот последний шаг, иначе повторная гидратация клиента не удастся. Хорошо, мы решили проблему мерцания, по крайней мере, когда вы нажимаете кнопку «Обновить». Но что, если мы начали с другой страницы и использовали навигацию на стороне клиента, чтобы перейти на эту страницу?


Откройте демо и попробуйте сами. Вы увидите, что объект пользователя будет установлен на null, если пользователь не был загружен на другой странице. Чтобы решить и эту проблему, мы должны сделать еще один шаг и применить шаблон «универсальный пользователь».


3. Универсальный пользователь


Универсальный пользовательский шаблон представляет собой комбинацию двух предыдущих шаблонов.


Если мы открываем страницу в первый раз, загрузите пользователя на стороне сервера, если это возможно, и отобразите страницу. На стороне клиента мы повторно гидратируем страницу с объектом пользователя и не извлекаем ее повторно, поэтому мерцания нет.


Во втором сценарии мы используем навигацию на стороне клиента, чтобы попасть на нашу страницу. В этом случае мы проверяем, загружен ли уже пользователь. Если пользовательский объект имеет значение null, мы попытаемся его получить.


Отлично, у нас есть универсальный пользовательский шаблон! Но есть еще одна проблема, с которой мы можем столкнуться. Что произойдет, если пользователь откроет вторую вкладку или окно и нажмет кнопку выхода?


Откройте [страницу универсального пользователя в демоверсии] (https://github.com/wundergraph/wundergraph-demo/blob/main/nextjs-frontend/pages/patterns/universal-user.tsx) в двух вкладках или окнах и попробуйте сами.


Если вы нажмете «Выйти» на одной вкладке, а затем вернетесь на другую вкладку, вы увидите, что объект пользователя все еще там.


Паттерн «обновить пользователя в фокусе окна» является решением этой проблемы.


4. Обновить пользователя в фокусе окна


К счастью, мы можем использовать метод window.addEventListener для прослушивания события focus. Таким образом, мы получаем уведомление всякий раз, когда пользователь активирует вкладку или окно.


Давайте добавим на нашу страницу хук для обработки оконных событий.


```машинопись


const windowHooks = (setIsWindowFocused: Dispatch<SetStateAction<"первоначальный" | "сфокусированный" | "размытый">>) => {


использоватьЭффект(() => {


const onFocus = () => {


setIsWindowFocused("в фокусе");


const onBlur = () => {


setIsWindowFocused("размыто");


window.addEventListener('фокус', onFocus);


window.addEventListener('размытие', onBlur);


возврат () => {


window.removeEventListener('фокус', onFocus);


window.removeEventListener('размытие', onBlur);


Вы заметите, что мы представляем три возможных состояния для действия «isWindowFocused»: нетронутое, сфокусированное и размытое. Почему три государства? Представьте, если бы у нас было только два состояния: сфокусированное и размытое. В этом случае нам всегда приходилось запускать событие focus, даже если окно уже было сфокусировано. Введя третье состояние (первоначальное), мы можем этого избежать.


Еще одно важное замечание, которое вы можете сделать, заключается в том, что мы удаляем прослушиватели событий при размонтировании компонента. Это очень важно, чтобы избежать утечек памяти.


Хорошо, мы ввели глобальное состояние для фокуса окна.


Давайте воспользуемся этим состоянием, чтобы повторно получить пользователя в фокусе окна, добавив еще один хук:


```машинопись


использоватьЭффект(() => {


если (отключитьFetchUserClientSide) {


возврат;


если (отключитьFetchUserOnWindowFocus) {


возврат;


if (isWindowFocused !== "сфокусировано") {


возврат


const abort = новый AbortController();


(асинхронный () => {


пытаться {


const nextUser = await ctx.client.fetchUser(abort.signal);


если (JSON.stringify(следующий пользователь) === JSON.stringify(пользователь)) {


возврат;


установить пользователя (следующий пользователь);


} поймать (е) {


возврат () => {


прервать.прервать();


}, [isWindowFocused, disableFetchUserClientSide, disableFetchUserOnWindowFocus]);


При добавлении состояния isWindowFocused в список зависимостей этот эффект будет срабатывать при изменении фокуса окна. Мы отклоняем события «нетронутые» и «размытые» и запускаем пользовательскую выборку только в том случае, если окно находится в фокусе.


Кроме того, мы удостоверяемся, что запускаем setState для пользователя только в том случае, если он действительно изменился. В противном случае мы можем вызвать ненужные повторные рендеринг или повторную выборку.


Превосходно! Наше приложение теперь может обрабатывать аутентификацию в различных сценариях. Это отличная основа для перехода к фактическим ловушкам для выборки данных.


5. Клиентский запрос


Первый хук для получения данных, который мы рассмотрим, — это [запрос на стороне клиента] (https://github.com/wundergraph/wundergraph-demo/blob/main/nextjs-frontend/pages/patterns/client-side- запрос.tsx).


Вы можете открыть демонстрационную страницу (http://localhost:3000/patterns/client-side-query) в своем браузере, чтобы почувствовать ее.


```машинопись


константные данные = useQuery.CountryWeather({


вход: {


код: "DE",


Итак, что стоит за useQuery.CountryWeather?


Давайте посмотрим!


```машинопись


function useQueryContextWrapper(wunderGraphContext: Context>, запрос: QueryProps, args?: InternalQueryArgsWithInput): {


результат: QueryResult<Данные>;


const {клиент} = useContext (wunderGraphContext);


const cacheKey = client.cacheKey (запрос, аргументы);


const [statefulArgs, setStatefulArgs] = useState | не определен>(аргументы);


const [queryResult, setQueryResult] = useState | не определен>({статус: "нет"});


использоватьЭффект(() => {


если (lastCacheKey === "") {


установитьпоследнийкэшкей(кешКей);


возврат;


если (lastCacheKey === cacheKey) {


возврат;


установитьпоследнийкэшкей(кешКей);


установитьStatefulArgs (аргументы);


установить недействительным (недействительным + 1);


}, [cacheKey]);


использоватьЭффект(() => {


const abort = новый AbortController();


setQueryResult({статус: "загрузка"});


(асинхронный () => {


константный результат = ожидание client.query(запрос, {


... аргументы состояния,


abortSignal: прерывание.сигнал,


setQueryResult (результат как QueryResult);


возврат () => {


прервать.прервать();


setQueryResult({статус: "отменено"});


}, [недействителен]);


возврат {


результат: queryResult как QueryResult,


Давайте объясним, что здесь происходит. Во-первых, мы берем клиента, который внедряется через React.Context.


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


Начальное состояние операции установлено на {status: "none"}. Когда запускается первая выборка, статус устанавливается на «загрузка». Когда выборка завершена, статус устанавливается на «успешно» или «ошибка».


Если компонент, обертывающий этот хук, размонтируется, статус устанавливается на «отменен». Кроме этого, ничего необычного здесь не происходит. Выборка происходит только при срабатывании useEffect.


Это означает, что мы не можем выполнить выборку на сервере. React.Hooks не выполняются на сервере. Если вы посмотрите на демо, вы заметите, что снова мерцание. Это потому, что мы не рендерим компонент на сервере. Давайте улучшим это!


6. Серверный запрос


Чтобы выполнять запросы не только на клиенте, но и на сервере, нам нужно внести некоторые изменения в наши хуки.


Давайте сначала обновим хук useQuery.


```машинопись


function useQueryContextWrapper(wunderGraphContext: Context>, запрос: QueryProps, args?: InternalQueryArgsWithInput): {


результат: QueryResult<Данные>;


const {ssrCache, клиент, isWindowFocused, refetchMountedOperations, пользователь} = useContext (wunderGraphContext);


const isServer = тип окна === 'undefined';


const ssrEnabled = args?.disableSSR !== true && args?.lazy !== true;


const cacheKey = client.cacheKey (запрос, аргументы);


если (сервер) {


если (ssrEnabled) {


если (ssrCache[cacheKey]) {


возврат {


результат: ssrCache[cacheKey] как QueryResult,


константное обещание = client.query (запрос, аргументы);


ssrCache[cacheKey] = обещание;


бросить обещание;


} еще {


ssrCache[cacheKey] = {


статус: "нет",


возврат {


результат: ssrCache[cacheKey] как QueryResult,


const [invalidate, setInvalidate] = useState<число>(0);


const [statefulArgs, setStatefulArgs] = useState | не определен>(аргументы);


const [lastCacheKey, setLastCacheKey] = useState("");


const [queryResult, setQueryResult] = useState | undefined>(ssrCache[cacheKey] as QueryResult || {статус: "нет"});


использоватьЭффект(() => {


если (lastCacheKey === "") {


установитьпоследнийкэшкей(кешКей);


возврат;


если (lastCacheKey === cacheKey) {


возврат;


установитьпоследнийкэшкей(кешКей);


установитьStatefulArgs (аргументы);


if (args?.debounceMillis !== undefined) {


setDebounce (предыдущая => предыдущая + 1);


возврат;


установить недействительным (недействительным + 1);


}, [cacheKey]);


использоватьЭффект(() => {


setQueryResult({статус: "загрузка"});


(асинхронный () => {


константный результат = ожидание client.query(запрос, {


... аргументы состояния,


abortSignal: прерывание.сигнал,


setQueryResult (результат как QueryResult);


возврат () => {


прервать.прервать();


setQueryResult({статус: "отменено"});


}, [недействителен]);


возврат {


результат: queryResult как QueryResult,


Теперь мы обновили хук useQuery, чтобы проверить, находимся ли мы на сервере или нет. Если мы находимся на сервере, мы проверим, были ли уже разрешены данные для сгенерированного ключа кэша. Если данные были разрешены, мы вернем их. В противном случае мы будем использовать клиент для выполнения запроса с помощью Promise. Но есть проблема. Нам не разрешено выполнять асинхронный код во время рендеринга на сервере. Таким образом, теоретически мы не можем «ждать» разрешения обещания.


Вместо этого мы должны использовать трюк. Нам нужно «приостановить» рендеринг. Мы можем сделать это, «отбросив» обещание, которое мы только что создали. Представьте, что мы рендерим окружающий компонент на сервере. Что мы могли бы сделать, так это обернуть процесс рендеринга каждого компонента в блок try/catch. Если один из таких компонентов выдает промис, мы можем перехватить его, дождаться разрешения промиса, а затем повторно отрендерить компонент.


Как только обещание разрешено, мы можем заполнить ключ кеша результатом. Таким образом, мы можем сразу вернуть данные, когда мы «попытаемся» отрендерить компонент во второй раз. Используя этот метод, мы можем перемещаться по дереву компонентов и выполнять все запросы, которые разрешены для рендеринга на стороне сервера. Вам может быть интересно, как реализовать этот метод try/catch. К счастью, нам не нужно начинать с нуля. Есть библиотека под названием [react-ssr-prepass (https://github.com/FormidableLabs/react-ssr-prepass), которую мы можем использовать для этого.


Давайте применим это к нашей функции getInitialProps:


```машинопись


WithWunderGraph.getInitialProps = async (ctx: NextPageContext) => {


const pageProps = (Страница как NextPage).getInitialProps? await (Page as NextPage).getInitialProps!(ctx как любой): {};


const ssrCache: { [ключ: строка]: любой} = {};


если (тип окна !== 'undefined') {


// мы на клиенте


// нет необходимости делать все, что связано с SSR


вернуть {...pageProps, ssrCache};


const cookieHeader = ctx.req?.headers.cookie;


если (typeof cookieHeader === "строка") {


defaultContextProperties.client.setExtraHeaders({


Куки: cookieHeader,


пусть ssrUser: User | ноль = ноль;


если (параметры?.disableFetchUserServerSide !== true) {


пытаться {


ssrUser = await defaultContextProperties.client.fetchUser();


} поймать (е) {


const AppTree = ctx.AppTree;


const App = createElement(wunderGraphContext.Provider, {


ценность: {


... свойства контекста по умолчанию,


пользователь: ssrUser,


}, createElement(AppTree, {


страницаПропс: {


...страницы,


ssrCache,


пользователь: ssrUser


ждите ssrPrepass (приложение);


const keys = Object.keys(ssrCache).filter(key => typeof ssrCache[key].then === 'function').map(key => ({


ключ,


значение: ssrCache[ключ]


})) as {ключ: строка, значение: Promise<любой> }[];


если (keys.length !== 0) {


const promises = keys.map(key => key.value);


const results = await Promise.all(обещания);


for (пусть я = 0; я < keys.length; я ++) {


константный ключ = keys[i].key;


ssrCache[ключ] = результаты[i];


return {...pageProps, ssrCache, пользователь: ssrUser};


Объект ctx содержит не только объект req, но и объекты AppTree. Используя объект AppTree, мы можем построить все дерево компонентов и внедрить наш провайдер контекста, объект ssrCache и объект user.


Затем мы можем использовать функцию ssrPrepass для обхода дерева компонентов и выполнения всех запросов, разрешенных для рендеринга на стороне сервера. После этого мы извлекаем результаты из всех промисов и заполняем объект ssrCache.


Наконец, мы возвращаем объект pageProps и объект ssrCache, а также объект пользователя.


Фантастический! Теперь мы можем применить рендеринг на стороне сервера к нашему хуку useQuery! Стоит отметить, что мы полностью отделили рендеринг на стороне сервера от необходимости реализовывать getServerSideProps в нашем компоненте Page. Это имеет несколько эффектов, которые важно обсудить.


Во-первых, мы решили проблему, связанную с объявлением наших зависимостей данных в getServerSideProps. Мы можем размещать хуки useQuery в любом месте дерева компонентов, они всегда будут выполняться. С другой стороны, этот подход имеет тот недостаток, что эта страница не будет статически оптимизирована. Вместо этого страница всегда будет отображаться на сервере, а это означает, что для обслуживания страницы должен быть запущен сервер.


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


Это здорово, что мы достигли до сих пор. Но что должно произойти, если пользователь ненадолго покидает окно и возвращается? Могут ли данные, которые мы получили в прошлом, устареть? Если да, то как нам быть в этой ситуации? На следующий узор!


7. Обновить запрос в фокусе окна


К счастью, мы уже реализовали объект глобального контекста для распространения трех различных состояний фокуса окна: нетронутого, размытого и сфокусированного.


Давайте воспользуемся «сфокусированным» состоянием, чтобы инициировать повторную выборку запроса. Помните, что мы использовали счетчик «invalidate», чтобы инициировать повторную выборку запроса. Мы можем добавить новый эффект для увеличения этого счетчика всякий раз, когда окно находится в фокусе.


```машинопись


использоватьЭффект(() => {


если (!refetchOnWindowFocus) {


возврат;


if (isWindowFocused !== "сфокусировано") {


возврат;


setInvalidate (предыдущая => предыдущая + 1);


}, [refetchOnWindowFocus, isWindowFocused]);


Вот и все! Мы отклоняем все события, если для refetchOnWindowFocus установлено значение false или окно не сфокусировано. В противном случае мы увеличиваем счетчик недействительности и инициируем повторную выборку запроса.


Если вы следите за демонстрацией, загляните на страницу refetch-query-on-window-focus.


Хук, включая конфигурацию, выглядит так:


```машинопись


константные данные = useQuery.CountryWeather({


вход: {


код: "DE",


отключитьSSR: правда,


refetchOnWindowFocus: правда,


Это было быстро! Давайте перейдем к следующему шаблону, ленивой загрузке.


8. Ленивый запрос


Как обсуждалось в постановке задачи, некоторые из наших операций должны выполняться только после определенного события. До тех пор исполнение должно быть отложено.


Давайте посмотрим на страницу lazy-query.


```машинопись


const [args,setArgs] = useState>({


вход: {


код: "DE",


ленивый: правда,


Установка для lazy значения true настраивает хук как «ленивый».


Теперь давайте посмотрим на реализацию:


```машинопись


использоватьЭффект(() => {


если (ленивый && аннулировать === 0) {


setQueryResult({


статус: "ленивый",


возврат;


const abort = новый AbortController();


setQueryResult({статус: "загрузка"});


(асинхронный () => {


константный результат = ожидание client.query(запрос, {


... аргументы состояния,


abortSignal: прерывание.сигнал,


setQueryResult (результат как QueryResult);


возврат () => {


прервать.прервать();


setQueryResult({статус: "отменено"});


}, [недействителен]);


const refetch = useCallback((args?: InternalQueryArgsWithInput) => {


если (аргументы !== не определено) {


установитьStatefulArgs (аргументы);


setInvalidate (предыдущая => предыдущая + 1);


Когда этот хук выполняется в первый раз, для ленивого будет установлено значение true, а для недействительного будет установлено значение 0. Это означает, что хук эффекта вернется раньше и установит результат запроса как «ленивый».


В этом сценарии выборка не выполняется.


Если мы хотим выполнить запрос, мы должны увеличить недействительность на 1. Мы можем сделать это, вызвав refetch в хуке useQuery. Вот и все! Теперь реализована ленивая загрузка.


Давайте перейдем к следующей проблеме: отмена дребезга пользовательских входов, чтобы не слишком часто получать запрос.


9. Отклонить запрос


Допустим, пользователь хочет получить прогноз погоды для определенного города. Мой родной город Франкфурт-на-Майне, прямо в центре Германии.


Этот поисковый запрос состоит из 17 символов. Как часто мы должны получать запрос, пока пользователь печатает? 17 раз? Один раз? Может дважды?


Ответ будет где-то посередине, но точно не 17 раз. Итак, как мы можем реализовать это поведение? Давайте посмотрим на реализацию хука useQuery.


```машинопись


использоватьЭффект(() => {


если (отклонить === 0) {


возврат;


константа отмены = setTimeout(() => {


setInvalidate (предыдущая => предыдущая + 1);


}, аргументы?.debounceMillis || 0);


return () => clearTimeout (отмена);


}, [устранение дребезга]);


использоватьЭффект(() => {


если (lastCacheKey === "") {


установитьпоследнийкэшкей(кешКей);


возврат;


если (lastCacheKey === cacheKey) {


возврат;


установитьпоследнийкэшкей(кешКей);


установитьStatefulArgs (аргументы);


if (args?.debounceMillis !== undefined) {


setDebounce (предыдущая => предыдущая + 1);


возврат;


установить недействительным (недействительным + 1);


}, [cacheKey]);


Давайте сначала посмотрим на второй useEffect, который имеет cacheKey в качестве зависимости. Вы можете видеть, что перед увеличением счетчика недействительности мы проверяем, содержат ли аргументы операции свойство debounceMillis.


Если это так, мы не сразу увеличиваем счетчик недействительности. Вместо этого мы увеличиваем счетчик debounce.


Увеличение счетчика debounce вызовет первый useEffect, так как счетчик debounce является зависимостью. Если счетчик debounce равен 0, что является начальным значением, мы немедленно возвращаемся, так как делать нечего. В противном случае мы запускаем таймер с помощью setTimeout.


Как только тайм-аут срабатывает, мы увеличиваем счетчик недействительности. Что особенного в эффекте с использованием setTimeout, так это то, что мы используем функцию возврата хука эффекта для очистки тайм-аута.


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


Я часто вижу, что разработчики используют setTimeout, но забывают обработать возвращаемый объект. Отсутствие обработки возвращаемого значения setTimeout может привести к утечке памяти, так как также возможно, что закрывающий компонент React размонтируется до того, как истечет время ожидания. Если вам интересно поиграть, перейдите к демоверсии и попробуйте ввести разные условия поиска, используя разное время устранения отказов.


Большой! У нас есть хорошее решение для устранения дребезга пользовательских входов.


Давайте теперь рассмотрим операции, требующие аутентификации пользователя. Начнем с защищенного на стороне сервера запроса.


10. Защищенный запрос на стороне сервера


Допустим, мы визуализируем панель инструментов, которая требует аутентификации пользователя. Панель инструментов также будет отображать пользовательские данные. Как мы можем реализовать это?


Опять же, нам нужно изменить хук useQuery.


```машинопись


const {ssrCache, клиент, isWindowFocused, refetchMountedOperations, пользователь} = useContext (wunderGraphContext);


const isServer = тип окна === 'undefined';


const ssrEnabled = args?.disableSSR !== true && args?.lazy !== true;


const cacheKey = client.cacheKey (запрос, аргументы);


если (сервер) {


если (query.requiresAuthentication && user === null) {


ssrCache[cacheKey] = {


статус: "требуется_аутентификация"


возврат {


результат: ssrCache[cacheKey] как QueryResult,


обновить: () => {


если (ssrEnabled) {


если (ssrCache[cacheKey]) {


возврат {


результат: ssrCache[cacheKey] как QueryResult,


refetch: () => Promise.resolve(ssrCache[cacheKey] as QueryResult),


константное обещание = client.query (запрос, аргументы);


ssrCache[cacheKey] = обещание;


бросить обещание;


} еще {


ssrCache[cacheKey] = {


статус: "нет",


возврат {


результат: ssrCache[cacheKey] как QueryResult,


обновить: () => ({}),


Как мы обсуждали в шаблоне 2, пользователь на стороне сервера,


мы уже реализовали некоторую логику для получения объекта пользователя в getInitialProps и внедрения его в контекст.


Мы также внедрили пользовательский файл cookie в клиент, который также вводится в контекст. Вместе мы готовы реализовать защищенный запрос на стороне сервера.


Если мы на сервере, мы проверяем, требует ли запрос аутентификации. Это статическая информация, определенная в метаданных запроса. Если пользовательский объект имеет значение null, что означает, что пользователь не аутентифицирован, мы возвращаем результат со статусом «requires_authentication».


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


Вот и все, никакой магии. Это было не слишком сложно, не так ли? Ну, сервер запрещает хуки, что значительно упрощает логику. Давайте теперь посмотрим, что требуется для реализации той же логики на клиенте.


11. Защищенный запрос на стороне клиента


Чтобы реализовать ту же логику для клиента, нам нужно еще раз изменить хук useQuery.


```машинопись


использоватьЭффект(() => {


если (query.requiresAuthentication && user === null) {


setQueryResult({


статус: "требуется_аутентификация",


возврат;


если (ленивый && аннулировать === 0) {


setQueryResult({


статус: "ленивый",


возврат;


const abort = новый AbortController();


если (queryResult?.status === "ok") {


setQueryResult({...queryResult, повторная выборка: true});


} еще {


setQueryResult({статус: "загрузка"});


(асинхронный () => {


константный результат = ожидание client.query(запрос, {


... аргументы состояния,


abortSignal: прерывание.сигнал,


setQueryResult (результат как QueryResult);


возврат () => {


прервать.прервать();


setQueryResult({статус: "отменено"});


}, [недействителен, пользователь]);


Как видите, теперь мы добавили пользовательский объект в зависимости эффекта. Если запрос требует аутентификации, но пользовательский объект имеет значение null, мы устанавливаем результат запроса на «requires_authentication» и возвращаемся раньше, выборка не происходит. Если мы проходим эту проверку, запрос запускается как обычно. Создание пользовательского объекта зависимостью эффекта выборки также имеет два приятных побочных эффекта.


Допустим, запрос требует аутентификации пользователя, но в настоящее время это не так. Первоначальный результат запроса — «requires_authentication». Если пользователь теперь входит в систему, объект пользователя обновляется через объект контекста.


Поскольку пользовательский объект является зависимостью эффекта выборки, все запросы теперь запускаются снова, а результат запроса обновляется.


С другой стороны, если запрос требует аутентификации пользователя, а пользователь только что вышел из системы, мы автоматически аннулируем все запросы и установим результаты «requires_authentication». Превосходно! Теперь мы реализовали защищенный шаблон запроса на стороне клиента. Но это еще не идеальный результат.


Если вы используете защищенные запросы на стороне сервера, навигация на стороне клиента не обрабатывается должным образом. С другой стороны, если мы используем только защищенные запросы на стороне клиента, у нас всегда будет снова неприятное мерцание. Чтобы решить эти проблемы, мы должны объединить оба эти шаблона, что приводит нас к шаблону запроса с универсальной защитой.


12. Универсальный защищенный запрос


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


Вот код со страницы universal-protected query:


```машинопись


const UniversalProtectedQuery = () => {


const {пользователь,логин,выход} = useWunderGraph();


константные данные = useQuery.ProtectedWeather({


вход: {


город: "Берлин",


возврат (


<дел>


Универсальный защищенный запрос


{JSON.stringify(пользователь)}


{JSON.stringify(данные)}





экспорт по умолчанию с WunderGraph (UniversalProtectedQuery);


Поиграйте с демо-версией и посмотрите, как она ведет себя при входе в систему и выходе из нее. Также попробуйте обновить страницу или использовать клиентскую навигацию. Что хорошо в этом шаблоне, так это то, насколько проста фактическая реализация страницы. Крючок запроса «ProtectedWeather» абстрагирует всю сложность обработки аутентификации как на стороне клиента, так и на стороне сервера.


13. Незащищенная мутация


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


```машинопись


function useMutationContextWrapper(wunderGraphContext: Context>,mutation: MutationProps): {


результат: MutationResult<Данные>;


mutate: (args?: InternalMutationArgsWithInput) => Promise>;


const {клиент, пользователь} = useContext (wunderGraphContext);


const [результат, setResult] = useState>(mutation.requiresAuthentication && user === null? {status: "requires_authentication"}: {status: "none"});


const mutate = useCallback(async (args?: InternalMutationArgsWithInput): Promise> => {


setResult({статус: "загрузка"});


константный результат = ожидание client.mutate (мутация, аргументы);


setResult (результат любой);


вернуть результат как любой;


возврат {


результат,


мутировать


Мутации не запускаются автоматически. Это означает, что мы не используем


useEffect для запуска мутации. Вместо этого мы используем хук useCallback для создания функции «мутации», которую можно вызывать. После вызова мы устанавливаем состояние результата на «загрузка», а затем вызываем мутацию.


Когда мутация завершена, мы устанавливаем состояние результата на результат мутации. Это может быть успех или неудача. Наконец, мы возвращаем и результат, и функцию mutate.


Посмотрите на страницу незащищенная мутация, если вы хотите поиграть с этим шаблоном.


Это было довольно прямолинейно.


Давайте добавим сложности, добавив аутентификацию.


14. Защищенная мутация


```машинопись


function useMutationContextWrapper(wunderGraphContext: Context>,mutation: MutationProps): {


результат: MutationResult<Данные>;


mutate: (args?: InternalMutationArgsWithInput) => Promise>;


const {клиент, пользователь} = useContext (wunderGraphContext);


const [результат, setResult] = useState>(mutation.requiresAuthentication && user === null? {status: "requires_authentication"}: {status: "none"});


const mutate = useCallback(async (аргументы?: InternalMutationArgsWithInput): Promise> => {


если (mutation.requiresAuthentication && user === null) {


вернуть {статус: "requires_authentication"}


setResult({статус: "загрузка"});


константный результат = ожидание client.mutate (мутация, аргументы);


setResult (результат любой);


вернуть результат как любой;


}, [пользователь]);


использоватьЭффект(() => {


если (!mutation.requiresAuthentication) {


возврат


если (пользователь === ноль) {


если (result.status !== "requires_authentication") {


setResult({статус: "requires_authentication"});


возврат;


если (result.status !== "нет") {


setResult({статус: "нет"});


}, [пользователь]);


возврат {


результат,


мутировать


Подобно шаблону защищенного запроса, мы внедряем пользовательский объект из контекста в обратный вызов. Если мутация требует аутентификации, мы проверяем, является ли пользователь нулевым. Если пользователь нулевой, мы устанавливаем результат «requires_authentication» и возвращаемся раньше.


Кроме того, мы добавляем эффект для проверки того, является ли пользователь нулевым. Если пользователь нулевой, мы устанавливаем результат «requires_authentication». Мы сделали это для того, чтобы мутации автоматически переходили в состояние «requires_authentication» или «none», в зависимости от того, аутентифицирован пользователь или нет.


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


Итак, защищенные мутации реализованы. Вам может быть интересно, почему нет раздела о мутациях на стороне сервера, защищенных или нет.


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


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


15. Обновить смонтированные операции при успешной мутации


Чтобы это работало, нам нужно изменить как обратный вызов мутации, так и обработчик запроса. Начнем с обратного вызова мутации.


```машинопись


const {клиент, setRefetchMountedOperations, пользователь} = useContext (wunderGraphContext);


const mutate = useCallback(async (аргументы?: InternalMutationArgsWithInput): Promise> => {


если (mutation.requiresAuthentication && user === null) {


вернуть {статус: "requires_authentication"}


setResult({статус: "загрузка"});


константный результат = ожидание client.mutate (мутация, аргументы);


setResult (результат любой);


if (result.status === "ok" && args?.refetchMountedOperationsOnSuccess === true) {


setRefetchMountedOperations (предыдущая => предыдущая + 1);


вернуть результат как любой;


}, [пользователь]);


Наша цель — аннулировать все в настоящее время смонтированные запросы, когда мутация прошла успешно. Мы можем сделать это, представив еще один объект глобального состояния, который хранится и распространяется через контекст React.


Мы называем этот объект состояния «refetchMountedOperationsOnSuccess», который представляет собой простой счетчик. В случае, если наш обратный вызов мутации был успешным, мы хотим увеличить счетчик. Этого должно быть достаточно, чтобы сделать недействительными все текущие смонтированные запросы.


Второй шаг — изменить хук запроса.


```машинопись


const {ssrCache, клиент, isWindowFocused, refetchMountedOperations, пользователь} = useContext (wunderGraphContext);


использоватьЭффект(() => {


if (queryResult?.status === "ленивый" || queryResult?.status === "none") {


возврат;


setInvalidate (предыдущая => предыдущая + 1);


}, [refetchMountedOperations]);


Вы уже должны быть знакомы со счетчиком «invalidate». Теперь мы добавляем еще один эффект для обработки приращения счетчика «refetchMountedOperations», введенного из контекста. Вы можете спросить, почему мы возвращаемся раньше, если статус «ленивый» или «нет»?


В случае «ленивого» мы знаем, что этот запрос еще не был выполнен, и разработчик намерен выполнять его только при ручном запуске. Итак, мы пропускаем ленивые запросы и ждем, пока они не будут запущены вручную. В случае «нет» применяется то же правило.


Это может произойти, например. если запрос обрабатывается только на стороне сервера, но мы перешли на текущую страницу с помощью навигации на стороне клиента. В таком случае мы ничего не можем «аннулировать», так как запрос еще не был выполнен. Мы также не хотим случайно запускать запросы, которые еще не были выполнены, из-за побочного эффекта мутации.


Хотите испытать это в действии? Перейдите на страницу [Refetch Mounted Operations on Mutation Success] (http://localhost:3000/patterns/refetch-mounted-operations-on-mutation-success). Прохладно! Мы закончили с запросами и мутациями. Далее мы рассмотрим реализацию хуков для подписок.


16. Подписка на стороне клиента


Чтобы реализовать подписки, нам нужно создать новый выделенный хук:


```машинопись


function useSubscriptionContextWrapper(wunderGraphContext: Context>, подписка: SubscriptionProps, args?: InternalSubscriptionArgsWithInput): {


результат: SubscriptionResult<Данные>;


const {ssrCache, клиент} = useContext (wunderGraphContext);


const cacheKey = client.cacheKey (подписка, аргументы);


const [invalidate, setInvalidate] = useState<число>(0);


const [subscriptionResult, setSubscriptionResult] = useState | undefined>(ssrCache[cacheKey] as SubscriptionResult || {статус: "нет"});


использоватьЭффект(() => {


if (subscriptionResult?.status === "ok") {


setSubscriptionResult({...subscriptionResult, streamState: "перезапуск"});


} еще {


setSubscriptionResult({статус: "загрузка"});


const abort = новый AbortController();


client.subscribe(подписка, (ответ: SubscriptionResult) => {


setSubscriptionResult (ответ как любой);


... аргументы,


abortSignal: прерывание.сигнал


возврат () => {


прервать.прервать();


}, [недействителен]);


возврат {


результат: subscribeResult as SubscriptionResult


Реализация этого хука аналогична хуку запроса. Он автоматически запускается при монтировании включающего компонента, поэтому мы снова используем хук «useEffect». Важно передать сигнал прерывания клиенту, чтобы гарантировать прерывание подписки при размонтировании компонента.


Кроме того, мы хотим отменить и перезапустить подписку, когда счетчик недействительности, аналогичный ловушке запроса, увеличивается. Мы опустили аутентификацию для краткости на этом этапе, но вы можете предположить, что это очень похоже на перехватчик запроса.


Хотите поиграть с примером? Перейдите на страницу Подписка на стороне клиента.


Однако следует отметить, что подписки ведут себя иначе, чем запросы. Подписки — это поток данных, который постоянно обновляется. Это означает, что мы должны подумать о том, как долго мы хотим держать подписку открытой. Должен ли он оставаться открытым навсегда?


Или может быть случай, когда мы хотим остановить и возобновить подписку? Один из таких случаев — когда пользователь размывает окно, что означает, что он больше не использует приложение активно.


17. Остановить подписку на размытие окна


Чтобы остановить подписку, когда пользователь размывает окно, нам нужно расширить хук подписки:


```машинопись


function useSubscriptionContextWrapper(wunderGraphContext: Context>, подписка: SubscriptionProps, args?: InternalSubscriptionArgsWithInput): {


результат: SubscriptionResult<Данные>;


const {ssrCache, клиент, isWindowFocused, refetchMountedOperations, пользователь} = useContext (wunderGraphContext);


const isServer = тип окна === 'undefined';


const ssrEnabled = args?.disableSSR !== true;


const cacheKey = client.cacheKey (подписка, аргументы);


const [stop, setStop] = useState (false);


const [invalidate, setInvalidate] = useState<число>(0);


const [stopOnWindowBlur] = useState(args?.stopOnWindowBlur === true);


const [subscriptionResult, setSubscriptionResult] = useState | undefined>(ssrCache[cacheKey] as SubscriptionResult || {статус: "нет"});


использоватьЭффект(() => {


если (стоп) {


if (subscriptionResult?.status === "ok") {


setSubscriptionResult({...subscriptionResult, streamState: "остановлен"});


} еще {


setSubscriptionResult({статус: "нет"});


возврат;


if (subscriptionResult?.status === "ok") {


setSubscriptionResult({...subscriptionResult, streamState: "перезапуск"});


} еще {


setSubscriptionResult({статус: "загрузка"});


const abort = новый AbortController();


client.subscribe(подписка, (ответ: SubscriptionResult) => {


setSubscriptionResult (ответ как любой);


... аргументы,


abortSignal: прерывание.сигнал


возврат () => {


прервать.прервать();


}, [stop, refetchMountedOperations, invalidate, user]);


использоватьЭффект(() => {


если (! StopOnWindowBlur) {


возврат


если (isWindowFocused === "сфокусировано") {


установитьСтоп (ложь);


если (isWindowFocused === "размыто") {


установитьСтоп (истина);


}, [stopOnWindowBlur, isWindowFocused]);


возврат {


результат: subscribeResult as SubscriptionResult


Чтобы это работало, мы вводим новую переменную с состоянием под названием «стоп». Состояние по умолчанию будет ложным, но когда пользователь размоет окно, мы установим состояние в истинное. Если они снова войдут в окно (в фокусе), мы установим состояние обратно в false. Если разработчик установил для «stopOnWindowBlur» значение false, мы проигнорируем это, что можно настроить в объекте «args» подписки.


Кроме того, мы должны добавить переменную остановки к зависимостям подписки. Вот и все! Очень удобно, что мы обрабатывали события окна глобально, это значительно упрощает реализацию всех остальных хуков.


Лучший способ испытать реализацию — открыть страницу [Подписка на стороне клиента (http://localhost:3000/patterns/client-side-subscription) и внимательно посмотреть на вкладку сети в консоли Chrome DevTools (или аналогично, если вы используете другой браузер).


Возвращаясь к одной из проблем, которые мы описали вначале, нам еще предстоит дать ответ на вопрос, как мы можем реализовать серверный рендеринг для подписок, сделав хук подписок «универсальным».


18. Универсальная подписка


Вы можете подумать, что рендеринг на стороне сервера для подписок невозможен. Я имею в виду, как вы должны отображать поток данных на сервере?


Если вы регулярно читаете этот блог, возможно, вы знаете о нашей реализации подписки. [Как мы описали в другом блоге (/blog/deprecate_graphql_subscriptions_over_websockets), мы реализовали подписки GraphQL таким образом, чтобы они были совместимы с EventSource (SSE), а также с Fetch API.


Мы также добавили в реализацию один специальный флаг.


Клиент может установить для параметра запроса "wg_subscribe_once" значение true. Это означает, что подписка с установленным флагом по существу является запросом.


Вот реализация клиента для получения запроса:


```машинопись


константные параметры = this.queryString({


wg_variables: args?.input,


wg_api_hash: this.applicationHash,


wg_subscribe_once: аргументы?.subscribeOnce,


константные заголовки: Заголовки = {


...эти.дополнительные заголовки,


Принять: "приложение/json",


"WG-SDK-Версия": this.sdkVersion,


const defaultOrCustomFetch = this.customFetch || globalThis.fetch;


const url = this.baseURL + "/" + this.applicationPath + "/operations/" + query.operationName + params;


const response = await defaultOrCustomFetch(url,


заголовки,


метод: «ПОЛУЧИТЬ»,


учетные данные: "включить",


режим: "корс",


Мы берем переменные, хэш конфигурации и флаг subscribeOnce и кодируем их в строку запроса.


Если установлена ​​подписка один раз, серверу будет ясно, что нам нужен только первый результат подписки.


Чтобы дать вам полную картину, давайте также рассмотрим реализацию клиентских подписок:


```машинопись


private subscribeWithSSE = (subscription: S, cb: (response: SubscriptionResult) => void, args?: InternalSubscriptionArgs) => {


(асинхронный () => {


пытаться {


константные параметры = this.queryString({


wg_variables: args?.input,


wg_live: подписка.isLiveQuery ? правда : не определено,


wg_sse: правда,


wg_sdk_version: this.sdkVersion,


const url = this.baseURL + "/" + this.applicationPath + "/operations/" + subscribe.operationName + params;


const eventSource = новый источник событий (url, {


withCredentials: правда,


eventSource.addEventListener('сообщение', ev => {


const responseJSON = JSON.parse(ev.data);


// опущено для краткости


если (ответJSON.данные) {


КБ({


статус: "ок",


streamState: "потоковое",


данные: ответJSON.data,


если (args?.abortSignal) {


args.abortSignal.addEventListener("abort", () => eventSource.close());


} поймать (е: любой) {


// опущено для краткости


Реализация клиента подписки похожа на клиент запроса, за исключением того, что мы используем API EventSource с обратным вызовом. Если EventSource недоступен, мы вернемся к Fetch API, но я не буду рассказывать о реализации в блоге, так как она не добавляет особой ценности.


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


Имейте в виду, если мы выполняем асинхронную работу любого рода, нам всегда нужно убедиться, что мы правильно обрабатываем отмену, иначе мы можем получить утечку памяти. Хорошо, теперь вы знаете о реализации клиента подписки. Давайте добавим в клиент простые в использовании хуки подписки, которые можно использовать как на клиенте, так и на сервере.


```машинопись


const {ssrCache, клиент, isWindowFocused, refetchMountedOperations, пользователь} = useContext (wunderGraphContext);


const isServer = тип окна === 'undefined';


const ssrEnabled = args?.disableSSR !== true;


const cacheKey = client.cacheKey (подписка, аргументы);


если (сервер) {


если (ssrEnabled) {


если (ssrCache[cacheKey]) {


возврат {


результат: ssrCache[cacheKey] как SubscriptionResult


const promise = client.query(subscription, {...args, subscribeOnce: true});


ssrCache[cacheKey] = обещание;


бросить обещание;


} еще {


ssrCache[cacheKey] = {


статус: "нет",


возврат {


результат: ssrCache[cacheKey] как SubscriptionResult


Аналогично хуку useQuery мы добавляем ветку кода для рендеринга на стороне сервера. Если мы находимся на сервере и у нас еще нет данных, мы делаем «запрос» с флагом subscribeOnce, установленным в true.


Как описано выше, подписка с флагом subscribeOnce, установленным в true, вернет только первый результат, поэтому она ведет себя как запрос. Вот почему мы используем client.query() вместо client.subscribe(). Некоторые комментарии к сообщению в блоге о нашей реализации подписки указывали на то, что не так важно делать подписки без состояния.


Я надеюсь, что теперь понятно, почему мы пошли по этому пути. Поддержка Fetch только что появилась в NodeJS, и даже до этого у нас была node-fetch в качестве полифилла. Определенно было бы возможно инициировать подписки на сервере с помощью WebSockets, но в конечном итоге я думаю, что гораздо проще просто использовать Fetch API и не беспокоиться о соединениях WebSocket на сервере.


Лучший способ поиграть с этой реализацией — перейти на страницу универсальная подписка. Когда вы обновите страницу, посмотрите на «превью» первого запроса. Вы увидите, что страница будет отображаться на сервере по сравнению с подпиской на стороне клиента. После повторной гидратации клиента он сам запустит подписку, чтобы обновлять пользовательский интерфейс.


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


19. Защищенная подписка


Вы заметите, что это очень похоже на обычную ловушку запроса.


```машинопись


const {ssrCache, клиент, isWindowFocused, refetchMountedOperations, пользователь} = useContext (wunderGraphContext);


const [subscriptionResult, setSubscriptionResult] = useState | undefined>(ssrCache[cacheKey] as SubscriptionResult || {статус: "нет"});


использоватьЭффект(() => {


если (subscription.requiresAuthentication && user === null) {


setSubscriptionResult({


статус: "требуется_аутентификация",


возврат;


если (стоп) {


if (subscriptionResult?.status === "ok") {


setSubscriptionResult({...subscriptionResult, streamState: "остановлен"});


} еще {


setSubscriptionResult({статус: "нет"});


возврат;


if (subscriptionResult?.status === "ok") {


setSubscriptionResult({...subscriptionResult, streamState: "перезапуск"});


} еще {


setSubscriptionResult({статус: "загрузка"});


const abort = новый AbortController();


client.subscribe(подписка, (ответ: SubscriptionResult) => {


setSubscriptionResult (ответ как любой);


... аргументы,


abortSignal: прерывание.сигнал


возврат () => {


прервать.прервать();


}, [stop, refetchMountedOperations, invalidate, user]);


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


Если это так, мы проверяем, вошел ли пользователь в систему. Если пользователь вошел в систему, мы продолжаем подписку. Если пользователь не вошел в систему, мы устанавливаем результат подписки на «requires_authentication».


Вот и все! Универсальные подписки с поддержкой аутентификации готовы! Давайте посмотрим на наш конечный результат:


```машинопись


const ProtectedSubscription = () => {


const {логин,выход,пользователь} = useWunderGraph();


константные данные = useSubscription.ProtectedPriceUpdates();


возврат (


<дел>


{JSON.stringify(пользователь)}


{JSON.stringify(данные)}





экспорт по умолчанию с помощью WunderGraph(ProtectedSubscription);


Разве не здорово, что мы можем скрыть такую ​​сложность за простым API? Все эти вещи, такие как аутентификация, фокус окна и размытие, рендеринг на стороне сервера, рендеринг на стороне клиента, передача данных с сервера на клиент, правильная регидратация клиента, все это обрабатывается за нас.


Кроме того, клиент в основном использует дженерики и обернут небольшим слоем сгенерированного кода, что делает весь клиент полностью типобезопасным. Типобезопасность была одним из наших требований, если вы помните.


Некоторые клиенты API "могут" быть типобезопасными. Другие позволяют вам добавить дополнительный код, чтобы сделать их типобезопасными. С нашим подходом, универсальным клиентом и автоматически сгенерированными типами, клиент всегда является типобезопасным.


Для нас очевидно, что до сих пор никто не просил нас добавить «чистый» клиент JavaScript. Наши пользователи, кажется, принимают и ценят то, что все является типобезопасным из коробки. Мы считаем, что безопасность типов помогает разработчикам делать меньше ошибок и лучше понимать свой код.


Хотите сами играть с защищенными универсальными подписками? Ознакомьтесь со страницей защищенной-подписки демонстрации. Не забудьте проверить Chrome DevTools и вкладку сети, чтобы получить наилучшую информацию. Наконец, мы закончили с подписками. Осталось еще два шаблона, и мы полностью закончили.


20. Live-Query на стороне клиента


Последний шаблон, который мы собираемся рассмотреть, — это Live Queries. Динамические запросы похожи на подписки тем, как они ведут себя на стороне клиента. Где они отличаются, так это на стороне сервера. Давайте сначала обсудим, как живые запросы работают на сервере и чем они полезны. Если клиент «подписывается» на оперативный запрос, сервер начнет опрашивать исходный сервер на наличие изменений.


Это будет делаться с настраиваемым интервалом, например. каждую секунду. Когда сервер получает изменение, он хэширует данные и сравнивает их с хешем последнего изменения. Если хэши отличаются, сервер отправит клиенту новые данные. Если хэши совпадают, мы знаем, что ничего не изменилось, поэтому ничего не отправляем клиенту.


Почему и когда живые запросы полезны?


Во-первых, большая часть существующей инфраструктуры не поддерживает подписки. Добавление оперативных запросов на уровне шлюза означает, что вы можете добавить возможности «в реальном времени» к существующей инфраструктуре. У вас может быть устаревший PHP-бэкэнд, который вы больше не хотите трогать. Добавьте к нему оперативные запросы, и ваш интерфейс сможет получать обновления в режиме реального времени.


Вы можете спросить, почему бы просто не сделать опрос со стороны клиента? Опрос на стороне клиента может привести к большому количеству запросов к серверу. Представьте, если 10 000 клиентов делают один запрос в секунду. Это 10 000 запросов в секунду. Как вы думаете, может ли ваш устаревший PHP-бэкэнд справиться с такой нагрузкой?


Чем могут помочь оперативные запросы?


10.000 клиентов подключаются к шлюзу API и подписываются на оперативный запрос. Затем шлюз может объединить все запросы вместе, поскольку они, по сути, запрашивают одни и те же данные, и сделать один запрос к источнику.


Используя live-запросы, мы можем уменьшить количество запросов к исходному серверу в зависимости от того, сколько «потоков» используется.


Итак, как мы можем реализовать live-запросы на клиенте?


Взгляните на «сгенерированную» оболочку для универсального клиента для одной из наших операций:


```машинопись


CountryWeather: (args: SubscriptionArgsWithInput) =>


hooks.useSubscriptionWithInput(WunderGraphContext, {


имя_операции: "CountryWeather",


isLiveQuery: правда,


требует аутентификации: ложь,


})(аргументы)


Глядя на этот пример, вы можете заметить несколько вещей.


Во-первых, мы используем хук useSubscriptionWithInput.


Это указывает на то, что на самом деле нам не нужно различать подписку и активный запрос, по крайней мере, с точки зрения клиентской стороны.


Единственное отличие состоит в том, что мы устанавливаем флаг isLiveQuery в значение true. Для подписок мы используем тот же хук, но устанавливаем для флага isLiveQuery значение false.


Поскольку мы уже реализовали хук подписки выше, для работы живых запросов не требуется дополнительный код. Проверьте live-query страницу демонстрации. Одна вещь, которую вы можете заметить, это то, что в этом примере снова появилось неприятное мерцание, потому что мы не рендерим его на стороне сервера.


21. Универсальный живой запрос


Последний и последний шаблон, который мы собираемся рассмотреть, — это универсальные живые запросы. Универсальные интерактивные запросы похожи на подписки, но проще с точки зрения сервера.


Для сервера, чтобы инициировать подписку, он должен открыть соединение WebSocket с исходным сервером, выполнить рукопожатие, подписаться и т. д. Если нам нужно подписаться один раз с помощью живого запроса, мы просто «опрашиваем» один раз. , что означает, что мы просто делаем один запрос.


Таким образом, оперативные запросы на самом деле инициируются немного быстрее по сравнению с подписками, по крайней мере, при первоначальном запросе. Как мы можем их использовать? Давайте посмотрим на пример из демо:


```машинопись


const UniversalLiveQuery = () => {


константные данные = useLiveQuery.CountryWeather({


вход: {


код: "DE",


возврат (


{JSON.stringify(данные)}


экспорт по умолчанию с помощью WunderGraph (UniversalLiveQuery);


Вот и все, это ваш поток данных о погоде для столицы Германии, Берлина, который обновляется каждую секунду.


Вам может быть интересно, как мы получили данные в первую очередь. Давайте посмотрим на определение операции CountryWeather:


```график


query ($capital: String! @internal $code: ID!) {


country_country(код: $код){


код


название


капитал @экспорт (как: «капитал»)


погода: _join @transform(получить: "weather_getCityByName.weather") {


Weather_getCityByName(имя: $capital){


погода {


температура {


действительный


резюме {


заглавие


описание


На самом деле мы объединяем данные из двух разных сервисов. Во-первых, мы используем API стран, чтобы получить столицу страны. Мы экспортируем поле capital во внутреннюю переменную $capital.


Затем мы используем поле _join, чтобы объединить данные о стране с API погоды. Наконец, мы применяем директиву @transform, чтобы немного сгладить ответ.


Это обычный действительный запрос GraphQL. В сочетании с шаблоном запросов в реальном времени мы теперь можем транслировать погоду в любой столице любой страны в прямом эфире. Круто, не так ли?


Подобно всем другим шаблонам, этот также можно опробовать и протестировать в демо-версии. Перейдите на страницу universal-live-query и поиграйте!


Вот и все! Были сделаны!


Надеюсь, вы узнали, как создавать универсальные перехватчики для выборки данных с поддержкой аутентификации. Прежде чем мы подойдем к концу этого поста, я хотел бы рассмотреть альтернативные подходы и инструменты для реализации перехватчиков выборки данных.


Альтернативные подходы к выборке данных в NextJS


SSG (генерация статического сайта)


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


Одним из решений этой проблемы является статическое создание страницы на сервере. NextJS позволяет реализовать асинхронную функцию getStaticProps поверх каждой страницы.


Эта функция вызывается во время сборки и отвечает за получение всех данных, необходимых для страницы. Если в то же время вы не прикрепите к странице функцию getInitialProps или getServerSideProps, NextJS считает эту страницу статической, а это означает, что для рендеринга страницы не потребуется никакого процесса NodeJS. В этом сценарии страница будет предварительно визуализирована во время компиляции, что позволит кэшировать ее CDN.


Этот способ рендеринга делает приложение чрезвычайно быстрым и простым в размещении, но есть и недостатки. Во-первых, статическая страница не зависит от пользователя. Это потому, что во время сборки нет контекста пользователя. Однако для общедоступных страниц это не проблема. Просто вы не можете использовать таким образом пользовательские страницы, такие как информационные панели.


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


Итак, если вы создаете приложение, которое требует аутентификации пользователя, вы можете вместо этого использовать рендеринг на стороне сервера.


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


ISR (инкрементная статическая регенерация)


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


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


Фрагменты GraphQL


Одна проблема, с которой вы можете столкнуться при рендеринге на стороне сервера (но также и на стороне клиента), заключается в том, что при обходе дерева компонентов серверу может потребоваться создать огромный водопад запросов, которые зависят друг от друга.


Если дочерние компоненты зависят от данных своих родителей, вы легко можете столкнуться с проблемой N+1. N+1 в этом случае означает, что вы получаете массив данных в корневом компоненте, а затем для каждого из элементов массива вам нужно будет запустить дополнительный запрос в дочернем компоненте.


Имейте в виду, что эта проблема не относится к использованию GraphQL. У GraphQL действительно есть решение для решения этой проблемы, в то время как REST API страдают от той же проблемы. Решение состоит в том, чтобы использовать фрагменты GraphQL с клиентом, который их должным образом поддерживает.


Создатели GraphQL, Facebook/Meta, создали решение этой проблемы, оно называется Relay Client.


Relay Client — это библиотека, которая позволяет вам указывать ваши «Требования к данным» вместе с компонентами через фрагменты GraphQL. Вот пример того, как это может выглядеть:


```машинопись


тип импорта {UserComponent_user$key} из 'UserComponent_user.graphql';


const Реагировать = требовать('Реагировать');


const {graphql, useFragment} = require('react-relay');


введите реквизиты = {


пользователь: UserComponent_user$key,


функция UserComponent (реквизит: Реквизит) {


константные данные = useFragment (


graphql`


фрагмент UserComponent_user для пользователя {


название


profile_picture (масштаб: 2) {


ури


реквизит.пользователь,


возврат (


{данные.имя


<дел>




Если это был вложенный компонент, то фрагмент позволяет нам поднять наши требования к данным до корневого компонента. Это означает, что корневой компонент сможет извлекать данные для своих дочерних компонентов, сохраняя при этом определение требований к данным в дочерних компонентах.


Фрагменты обеспечивают слабую связь между родительскими и дочерними компонентами, обеспечивая при этом более эффективный процесс выборки данных. Для многих разработчиков это настоящая причина, по которой они используют GraphQL. Дело не в том, что они используют GraphQL, потому что хотят использовать язык запросов, а потому, что хотят использовать возможности Relay Client.


Для нас Relay Client — отличный источник вдохновения.


Я действительно думаю, что использование Relay слишком сложно. В нашей следующей итерации мы рассматриваем возможность использования подхода «Поднятие фрагментов», но наша цель — сделать его более простым в использовании, чем Relay Client.


Приостановка реакции


Еще одна разработка, которая происходит в мире React, — это создание React Suspense. Как вы видели выше, мы уже используем Suspense на сервере. «Отбрасывая» промис, мы можем приостановить рендеринг компонента до тех пор, пока промис не будет разрешен. Это отличный способ справиться с асинхронной выборкой данных на сервере.


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


Такой подход делает код внутри компонента более читабельным, поскольку он обрабатывает только «счастливый путь». Поскольку мы уже поддерживаем Suspense на сервере, вы можете быть уверены, что в будущем мы также добавим клиентскую поддержку. Мы просто хотим найти наиболее идиоматический способ поддержки клиентов с приостановкой и без приостановки.


Таким образом, пользователи получают свободу выбора стиля программирования, который они предпочитают.


Альтернативные технологии для выборки данных и аутентификации в NextJS


Мы не единственные, кто пытается улучшить получение данных в NextJS. Поэтому давайте кратко рассмотрим другие технологии и их сравнение с предлагаемым нами подходом.


свр


На самом деле мы черпали вдохновение из swr. Если вы посмотрите на шаблоны, которые мы реализовали, вы увидите, что swr действительно помог нам определить отличный API для выборки данных.


Есть несколько вещей, в которых наш подход отличается от swr, о которых стоит упомянуть.


SWR гораздо более гибкий и простой в использовании, потому что вы можете использовать его с любым бэкендом. Подход, который мы выбрали, особенно то, как мы обрабатываем аутентификацию, требует, чтобы вы также запускали серверную часть WunderGraph, которая предоставляет ожидаемый API.


Например. если вы используете клиент WunderGraph, мы ожидаем, что серверная часть является проверяющей стороной OpenID Connect. С другой стороны, клиент swr не делает таких предположений.


Я лично считаю, что с такой библиотекой, как swr, вы в конечном итоге получите такой же результат, как если бы вы использовали клиент WunderGraph в первую очередь. Просто теперь вы поддерживаете больше кода, так как вам пришлось добавить логику аутентификации.


Другим большим отличием является рендеринг на стороне сервера. WunderGraph тщательно разработан для устранения ненужного мерцания при загрузке приложения, требующего аутентификации.


Документы от swr объясняют, что это не проблема, и пользователи могут загружать счетчики на панели инструментов.


Я думаю, что мы можем сделать лучше, чем это. Я знаю панели мониторинга SaaS, которым требуется 15 и более секунд для загрузки всех компонентов, включая контент. В течение этого периода времени пользовательский интерфейс вообще непригоден для использования, потому что он продолжает «покачивать» весь контент в нужное место.


Почему мы не можем предварительно отрендерить всю приборную панель, а затем перезагрузить клиент? Если HTML отображается правильно, ссылки должны быть кликабельны еще до загрузки клиента JavaScript. Если весь ваш «бэкенд» помещается в каталог «/api» вашего приложения NextJS, вероятно, лучшим выбором будет использование библиотеки «swr». В сочетании с NextAuthJS это может составить очень хорошую комбинацию.


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


NextAuthJS


Говоря о NextAuthJS, почему бы просто не добавить аутентификацию непосредственно в ваше приложение NextJS?


Библиотека предназначена для решения именно этой проблемы, добавляя аутентификацию в ваше приложение NextJS с минимальными усилиями. С технической точки зрения NextAuthJS следует тем же шаблонам, что и WunderGraph. Есть лишь несколько отличий с точки зрения общей архитектуры.


Если вы создаете приложение, которое никогда не будет масштабироваться за пределы одного веб-сайта, вы, вероятно, можете использовать NextAuthJS. Однако, если вы планируете использовать несколько веб-сайтов, инструментов cli, собственных приложений или даже подключить серверную часть, вам лучше использовать другой подход.


Позвольте мне объяснить, почему.


Способ реализации NextAuthJS заключается в том, что он фактически становится «издателем» потока аутентификации. Тем не менее, это не эмитент, совместимый с OpenID Connect, это пользовательская реализация. Таким образом, несмотря на то, что начать работу легко, в самом начале вы фактически добавляете много технического долга.


Допустим, вы хотите добавить еще одну панель инструментов или инструмент cli или подключить серверную часть к своим API. Если вы использовали эмитента, совместимого с OpenID Connect, уже существует поток, реализованный для различных сценариев.


Кроме того, этот провайдер OpenID Connect лишь слабо связан с вашим приложением NextJS. Создание самого приложения эмитентом означает, что вам придется повторно развертывать и модифицировать ваше «интерфейсное» приложение всякий раз, когда вы хотите изменить поток аутентификации.


Вы также не сможете использовать стандартные потоки проверки подлинности, такие как поток кода с pkce или поток устройства. Аутентификация должна выполняться вне самого приложения.


Недавно мы объявили о партнерстве с Cloud IAM, благодаря которому настройка поставщика OpenID Connect с WunderGraph в качестве проверяющей стороны занимает считанные минуты. Я надеюсь, что мы сделали это достаточно простым для вас, чтобы вам не пришлось создавать собственные потоки аутентификации.


трпк


Уровень извлечения данных и хуки на самом деле очень похожи на WunderGraph. Я думаю, что мы даже используем тот же подход для рендеринга на стороне сервера в NextJS.


Очевидно, что trpc имеет очень мало общего с GraphQL по сравнению с WunderGraph. История с аутентификацией тоже не такая полная, как у WunderGraph.


Тем не менее, я думаю, что [Alex] (https://github.com/KATT) проделал большую работу по созданию trpc. Он менее самоуверен, чем WunderGraph, что делает его отличным выбором для различных сценариев.


Насколько я понимаю, trpc лучше всего работает, когда и бэкенд, и внешний интерфейс используют TypeScript. WunderGraph идет другим путем. Общей промежуточной точкой для определения контракта между клиентом и сервером является JSON-RPC, определенный с использованием схемы JSON. Вместо того, чтобы просто импортировать типы серверов в клиент, вы должны пройти процесс генерации кода с помощью WunderGraph.


Это означает, что настройка немного сложнее, но мы можем поддерживать не только TypeScript в качестве целевой среды, но и любой другой язык или среду выполнения, поддерживающие JSON через HTTP.


Другие клиенты GraphQL


Есть много других клиентов GraphQL, таких как Apollo Client, urql и graphql-request. Их всех объединяет то, что они обычно не используют JSON-RPC в качестве транспорта.


Я, вероятно, писал об этом в нескольких сообщениях в блоге раньше, но отправка запросов на чтение через HTTP POST просто ломает Интернет. Если вы не меняете операции GraphQL, как 99% всех приложений, использующих этап компиляции/транспиляции, зачем использовать клиент GraphQL, который делает это?


Клиенты, браузеры, кэш-серверы, прокси и CDN — все они понимают заголовки Cache-Control и ETag.


Популярный клиент извлечения данных NextJS «swr» получил свое название не просто так, потому что swr расшифровывается как «stale while revalidate», что является не чем иным, как шаблоном, использующим ETag для эффективной инвалидации кеша.


GraphQL — отличная абстракция для определения зависимостей данных. Но когда дело доходит до развертывания веб-приложений, мы должны использовать существующую инфраструктуру Интернета.


Это означает следующее: GraphQL великолепен во время разработки, но в производстве мы должны максимально использовать принципы REST.


Резюме


Создание хороших хуков для выборки данных для NextJS и React в целом — непростая задача. Мы также обсудили, что приходим к несколько иным решениям, если с самого начала принимаем во внимание аутентификацию.


Я лично считаю, что добавление аутентификации прямо на уровень API на обоих концах, бэкенде и внешнем интерфейсе, делает подход более чистым. Еще один аспект, о котором следует подумать, — это где разместить логику аутентификации. В идеале вы не реализуете его самостоятельно, но можете положиться на правильную реализацию.


Объединение OpenID Connect в качестве эмитента с проверяющей стороной в вашем backend-for-frontend (BFF) — отличный способ сохранить разъединенность, но при этом очень контролируемый.


Наш лучший друг все еще создает и проверяет файлы cookie, но это не источник правды. Мы всегда делегируем полномочия Keycloak.


Что хорошо в этой настройке, так это то, что вы можете легко поменять Keycloak на другую реализацию, в этом прелесть использования интерфейсов вместо конкретных реализаций.


Наконец, я надеюсь, что смогу убедить вас в том, что больше (SaaS) информационных панелей должны использовать рендеринг на стороне сервера. NextJS и WunderGraph настолько упрощают реализацию, что стоит попробовать.


Еще раз, если вам интересно поиграть с демо, вот репозиторий:


https://github.com/wundergraph/wundergraph-demo


Что дальше?


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


В будущем мы планируем еще больше расширить поддержку NextJS. Мы хотели бы создать отличную поддержку создания статических сайтов (SSG), а также добавочной статической регенерации (ISR).


Что касается GraphQL, мы хотим добавить поддержку федераций способом, очень похожим на клиент Relay. Я считаю, что зависимости данных должны быть объявлены близко к тому месту, где данные фактически используются. Фрагменты GraphQL также допускают все виды оптимизации, например. применение различных правил выборки или кэширования, таких как отсрочка и потоковая передача, для каждого фрагмента.


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


Присоединяйся к нам!


Если вы так же увлечены этой темой, как и мы, рассмотрите возможность присоединиться к нам и помочь нам улучшить опыт разработчиков API.


Подача заявки на работу в WunderGraph немного отличается от того, что вы могли ожидать. Вы не можете напрямую подать заявку на работу в WunderGraph, мы свяжемся с вами напрямую, если сочтем, что вы подходите.


Как вы привлекаете наше внимание?






Мы осознаем, что мы всего лишь люди и не знаем всего.


Мы также должны быть очень осторожны, где и как тратить наши ресурсы. Вы, наверное, в чем-то намного умнее нас. Мы ценим отличные коммуникативные навыки и скромное отношение.


Покажите нам, что мы можем улучшить, и мы обязательно свяжемся с вами.




Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE