Получайте правильные данные с помощью 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
- getServerSideProps работает только на корневых страницах
- ловушки для получения данных с аутентификацией
- безопасность типов
- подписки и SSR
- фокус окна и размытие
- побочные эффекты мутаций
- ленивая загрузка
- устранение дребезга
Это подводит нас к 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 и т. д. Как выглядит процесс аутентификации с точки зрения пользователей?
- Пользователь нажимает «Войти».
- Фронтенд перенаправляет пользователя на бэкенд (проверяющую сторону).
- Серверная часть перенаправляет пользователя к поставщику удостоверений.
- Пользователь аутентифицируется у поставщика удостоверений.
- Если аутентификация прошла успешно, поставщик удостоверений перенаправляет пользователя обратно на серверную часть.
- Затем серверная часть обменивает код авторизации на токен доступа и идентификации.
- Токен доступа и удостоверения используются для установки на клиенте безопасного зашифрованного файла cookie только по протоколу HTTP.
- С установленным файлом 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
результат: 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
результат: 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
использоватьЭффект(() => {
если (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
результат: MutationResult<Данные>;
mutate: (args?: InternalMutationArgsWithInput) => Promise
const {клиент, пользователь} = useContext (wunderGraphContext);
const [результат, setResult] = useState
const mutate = useCallback(async (args?: InternalMutationArgsWithInput): Promise
setResult({статус: "загрузка"});
константный результат = ожидание client.mutate (мутация, аргументы);
setResult (результат любой);
вернуть результат как любой;
возврат {
результат,
мутировать
Мутации не запускаются автоматически. Это означает, что мы не используем
useEffect для запуска мутации. Вместо этого мы используем хук useCallback для создания функции «мутации», которую можно вызывать. После вызова мы устанавливаем состояние результата на «загрузка», а затем вызываем мутацию.
Когда мутация завершена, мы устанавливаем состояние результата на результат мутации. Это может быть успех или неудача. Наконец, мы возвращаем и результат, и функцию mutate.
Посмотрите на страницу незащищенная мутация, если вы хотите поиграть с этим шаблоном.
Это было довольно прямолинейно.
Давайте добавим сложности, добавив аутентификацию.
14. Защищенная мутация
```машинопись
function useMutationContextWrapper
результат: MutationResult<Данные>;
mutate: (args?: InternalMutationArgsWithInput) => Promise
const {клиент, пользователь} = useContext (wunderGraphContext);
const [результат, setResult] = useState
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
результат: SubscriptionResult<Данные>;
const {ssrCache, клиент} = useContext (wunderGraphContext);
const cacheKey = client.cacheKey (подписка, аргументы);
const [invalidate, setInvalidate] = useState<число>(0);
const [subscriptionResult, setSubscriptionResult] = useState
использоватьЭффект(() => {
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
результат: 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
использоватьЭффект(() => {
если (стоп) {
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
использоватьЭффект(() => {
если (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
имя_операции: "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) {
ури
реквизит.пользователь,
возврат (
{данные.имя
<дел>