Клиент учебного каталога
Завершим Академию простой браузерной страницей каталога. Она запросит courseConnection, покажет displayTitle и имя автора, затем позволит загрузить продолжение. Основная задача клиента — правильно прочитать HTTP-результат и GraphQL-конверт. Успешный сетевой статус не гарантирует отсутствие errors, а отдельная ошибка автора не требует скрывать весь список. Мы используем те же три курса и специально недоступную метаинформацию c03, чтобы интерфейс отражал реальную форму учебного API.
Страница client.html отдаётся GET-запросом к корню локального Node-сервера. Запросы к /graphql используют тот же origin, поэтому не нужен отдельный CORS-договор. Клиент не выполняет запись на курс и не имитирует настоящий вход. Его результат — чтение каталога с частичной выдачей и курсором. Mutation остаётся доступной для самостоятельного разбора документов курса, но её демонстрационный субъект не превращается в производственный механизм авторизации.
Документ и переменная продолжения
Операция Page запрашивает две карточки за раз. Внутри edges выбираются ID, displayTitle и author.name, а pageInfo содержит hasNextPage и endCursor. Переменная after сначала равна null. После полезного ответа клиент сохраняет endCursor и отправляет его при следующем нажатии. Он не декодирует значение и не вычисляет новый ID: непрозрачный курсор является договором сервера, уже рассмотренным в восьмой главе.
Кнопка выключается на время обращения. Это предотвращает два одновременных клика с одной и той же границей в простом последовательном интерфейсе. Если запрос завершился сетевой ошибкой, кнопка становится доступной снова и курсор не продвигается. Для полноценного интерфейса с поиском понадобились бы отмена устаревших обращений и идентификация актуального состояния. Здесь действие одно, поэтому механизм не скрывает сложную клиентскую модель за минимальным примером.
try{const response=await fetch('/graphql',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({query:'query Page($after:String){courseConnection(first:2,after:$after){edges{node{id displayTitle author{name}} cursor}pageInfo{hasNextPage endCursor}}}',variables:{after}})});
if(!response.ok)throw Error(`HTTP ${response.status}`);const result=await response.json();
if(result.errors?.length)status.textContent=result.errors.map(e=>e.message).join('; ');
const page=result.data?.courseConnection;if(!page){button.disabled=false;return}
for(const edge of page.edges){const item=document.createElement('li');item.textContent=edge.node.displayTitle+' — '+(edge.node.author?.name??'автор не указан');list.append(item)}
after=page.pageInfo.endCursor;button.disabled=!page.pageInfo.hasNextPage;if(!result.errors?.length)status.textContent=page.pageInfo.hasNextPage?'Можно загрузить продолжение':'Все курсы показаны';
Листинг показывает часть клиентского обработчика из полного client.html состояния lesson-12. Документ целиком находится в архиве вместе с сервером и схемой. Сначала проверяется HTTP-статус, затем читается JSON, затем рассматриваются errors и data. Такой порядок отделяет транспортный отказ от результата языка. Не следует сразу обращаться к result.data.courseConnection без проверки: некоторые ветви ответа содержат только errors.
Частичная выдача в интерфейсе
Если page существует, клиент добавляет узлы даже при наличии errors. Отсутствующий author получает подпись «автор не указан», а сообщение ошибки остаётся видимым в status. Так пользователь видит сам курс и причину неполной метаинформации. Пример не скрывает ошибку как полностью успешную загрузку, но и не уничтожает соседние данные. Это прямое применение nullable границы схемы, а не произвольное клиентское угадывание.
Если data.courseConnection отсутствует, курсор не изменяется и кнопка возвращается в доступное состояние. Клиент может повторить чтение или исправить документ, в зависимости от причины. Однако бесконечная автоматическая повторная попытка при QUERY_LIMIT была бы бесполезна: тот же документ получает тот же отказ политики. Код не добавляет таймер повтора, чтобы оставить решение человеку и не создавать лишние запросы при исправимой ошибке.
В текущем наборе первая страница должна показать c01 и c02, вторая — c03 с отсутствующим автором. После второй кнопка выключается, потому что hasNextPage:false. Самостоятельное чтение без поля author не получило бы эту ошибку, но наша страница специально запрашивает его. Выбор интерфейса определяет, какие части источника участвуют в исполнении. Отсутствие errors в другой проекции не является доказательством восстановления всех данных.
Безопасное отображение строки
Для каждой карточки создаётся li, а подпись присваивается через textContent. Название курса не интерпретируется как HTML. Даже если собственный вымышленный набор изменится на строку с угловыми скобками, интерфейс должен показать текст. Не следует вставлять название через innerHTML ради краткости, если продукт не обещает доверенную разметку. Серверная схема проверяет тип String, но сама по себе не гарантирует безопасную интерпретацию этой строки браузером.
Сообщение status также использует textContent и role="status". Это создаёт смысловой канал состояния чтения. Кнопка остаётся обычным button, а список — ul. Такая базовая структура полезна для доступности, но не означает выполненного аудита всех режимов, экранных читателей и клавиатурных сценариев. Пользователь попросил не запускать проверки; статья описывает выбранные свойства разметки без отчёта о несуществующем испытании интерфейса.
Что осталось на стороне сервера
Клиент не проверяет право mutation вместо сервера, не исправляет курсор и не вычисляет внутренний authorId. Он отправляет документ и variables, а сервер отвечает по схеме. Ресурсный бюджет находится до предметного выполнения. Загрузчик авторов живёт в context и не создаёт общий долгоживущий кэш. Эти границы помогают сохранить небольшой клиент: интерфейсу не требуется повторять все правила, которые уже определены в источнике истины.
Однако данные и записи студентов остаются в памяти. После перезапуска каталог возвращает исходное состояние, а Enrollment исчезают. Настоящий вход, постоянная база, квоты одновременных запросов и строгий промышленный транспортный профиль не реализованы. Также запросная Map показывает пакетирование без измеренного SQL. Ограничения важны для выбора следующего развития, потому что завершённость учебной последовательности не равна готовности публичной образовательной платформы.
Если развивать проект предметно, сначала стоит подключить постоянное хранилище Enrollment с уникальностью пары, затем заменить демонстрационный заголовок проверенной сессией. После этого определить разрешённые чтения записей и пределы источников. Схему можно сохранять совместимой через уже изученный переход полей. Такой порядок следует из текущих возможностей Академии и даёт каждый раз проверяемый результат, вместо случайного добавления новой библиотеки на каждый шаг.
При подготовке страницы сервер, браузерный клиент и проверки не запускались. Установка зависимостей и команды приведены для самостоятельного прохождения читателем. Архив содержит двенадцать полных независимых состояний, чтобы можно было открыть любой шаг и изучить соответствующие файлы. Материалы сайта остаются локальными черновиками. Общий результат курса — понятный путь от схемы до клиента, где проекция, ошибка, право, курсор и стоимость запроса имеют разные, явно обозначенные роли.
При потере сети fetch может отклонить Promise до появления HTTP-ответа. Это отличается от 403 или GraphQL errors, которые пришли от сервера. Наш catch возвращает кнопку в доступное состояние и показывает короткую причину, сохраняя прежний список. Для чтения такой повтор допустим, а для mutation пришлось бы отдельно выяснить, дошло ли действие до источника. Одинаковая клиентская ошибка не задаёт универсальное правило повторной отправки.
Исходники урока
Оглавление серии · Скачать исходники. Полное самостоятельное состояние находится в lesson-12. Учебные приложения, зависимости, база и проверки при подготовке не запускались; описанный результат ожидается по исходникам. Материалы пока находятся в локальной редакционной версии.
Первичные источники: документация 1.