На текущий момент GraphQL все больше распространяется в энтерпрайзе. И это не удивительно - изящный синтаксис запросов, типизация, ускорение разработки и это далеко не все его плюсы использования.
Наша небольшая команда уже больше года использует его во всех проектах, и скажу вам: мы испробовали большинство популярных библиотек на клиентской стороне и с ними не все так гладко как хотелось бы.
> Думаю, что стоит сделать небольшую ремарку относительно того, кому подойдет эта статья. Если для вас критично держать размер конечного бандла добро пожаловать под кат.
Но обо всем по порядку.
Типичный стек
Apollo Client - это не просто библиотека запросов, но и библиотека управления состоянием, с встроенным кэшированием. По заявлению разработчиков помогает структурировать код экономичным, предсказуемым и декларативным способом, который соответствует современной практике разработки. Основная библиотека @apollo/client обеспечивает встроенную интеграцию с React.
GraphQL Code Generator - утилита для генерации готового для использования кода. Она имеет плагин для @apollo/client, что позволяет без лишних затрат времени сгенирировать хуки для ваших запросов.
Для крупных приложений дополнительно используются стейт-менеджеры, такие как Mobx, Redux, Recoil и т.п.
Что мы получаем по итогу: сгенирированные хуки для запроса данных и дополнительное хранилище для данных приложения. Казалось бы, что могло пойти не так?
Что под капотом
Допустим у нас есть какой-нибудь запрос:
query getMe {
me {
id
balance
}
}
Соответственно, для него генерируется:
const defaultOptions = {};
export const GetMeDocument = gql`
query getMe {
me {
id
balance
}
}
`;
export function useGetMeQuery(baseOptions?: Apollo.QueryHookOptions<GetMeQuery, GetMeQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetMeQuery, GetMeQueryVariables>(GetMeDocument, options);
}
В запросе на сервер отправится такая структура:
{
"operationName": "getMe",
"query": "query getMe { me { id balance } }",
"variables": {}
}
Фактически наш запрос сформированный на языке GraphQL в исходном виде помещается в JSON в поле query... Уже чувствуете подвох?
Что такое gql
gql в сгенерированном коде, это функция из библиотеки graphql-tag от Apollo, которая строит синтаксическое дерево из вашего запроса в рантайме и отдает ее Apollo Client, который в свою очередь обратно собирает её в строку. Проще говоря, это нужно для синтаксической валидации запроса.
Только вот при генерации кода graphql-codegen уже проверил наш запрос по схеме с сервера, соответственно мы выполняем двойную работу, да к тому же с оверхеадом в рантайме.
Дополнительно предоставляется поле `operationName` из нашего запроса. В спецификации GraphQL сказано, что оно нужно для кэширования и логирования на сервере, но загвостка в том, что на сервере мы разбираем `query`, чтобы понять что клиенту нужно вернуть, соответственно мы можем получить его непосредственно из самого запроса. В реализации кэширования предполагается, что все поля запроса будут хэшированы, и им будет назначен соответствующий ответ. Из этого делаем вывод, что `operationName` не такое уж и важное поле, благо по спецификации мы можем передавать в него `null`.
Получается в общем-то нам не нужен graphql-tag, и мы можем передавать запрос as-is.
Роль Apollo Client
Apollo Client несомненно предоставляет широкие возможности для запросов, включая кэширование, но польза этого самого кэширования нивелируется тем, что полученные данные мы так или иначе кладем в каком-нибудь виде в стейт-менеджер. Соответственно, все для чего он нужен - это отправить сформированную JSON на сервер, получить данные и обновить компонент.
Какой ценой
По данным bundlephobia.com:
Библиотека |
min+gzip |
graphql |
8.9kB |
graphql-tag |
1.1kB |
@apollo/client |
32kB |
итого |
42kb |
Подумайте только, мы добавили 42kB рантайма для того, чтобы получить с сервера данные. С другими популярными библиотеками, такими как urql или react-query ситуация схожая.
Пища для размышления
GraphQL — отличный инструмент, но нам надо переосмыслить его использование, например, по максиму использовать возможности кодогенерации и минимизировать рантайм на клиенте. Но об этом как-нибудь в другой раз.
Комментарии (21)
Fi1osof
18.09.2021 18:21+3Нет, аполло-клиент - это не только система состояний, но и довольно хитрая система кеширования.
1.. Не будет лишних повторных запросов на сервер, если параметры не менялись.
2.. Многоуровневый ответ разбивается в кеш на отдельные объекты, и получив новые данные объектов от сервера, вы получитет апдейт их данных во всех результатах запросов в кеше.
3.. Есть возможность указать перечень объектов-документв в параметрах запроса, чтобы по результату выполнения перезапросились все запросы с такими документами запросов.
4.. Методы типы client.resetStore() и подобные, чтобы сбросить кеш и перезапросить все активные запросы.
5.. Правила замены или "склеивания" результатов.
6.. Маппинг типов данных. К примеру, указать жиэсовский Date для графкульного DateTime. Ведь с сервера в ответах у нас всегда только строки/числа в скалярах прилетают, там объекты сложные отсутствуют. А в тайпскрипте у нас прописано, что такое-то поле - это Объект, а у нас тут строка прилетает... В аполло есть возможность задать так, что уже из кеша объект будет с типизированными полями, а не просто строки.
И еще много-много всего. Просто за год вы не успели проработать достаточно кейсов, чтобы это осознать.
Я ни в коем случае не смеюсь над вами, но со временем вы узнаете больше.dbrr
19.09.2021 13:50Что-то мне подсказывает - лень не позволила автору внимательно изучить документацию apollo client. Иначе рядом не упомянались бы всякие redux'ы и т.п. Apollo client большая бибилиотека с кучей фишек и особенностей, что бы грамотно использовать нужно потратить на много больше времени в сравненнии с тем же редаксом. Мой личный опыт говорит, что не зря.
AndyPike
18.09.2021 19:04+1Интересно мнение коллег, не кажется ли вам, что Front End, в руки которого попал GraphQL (с query и mutation), по-сути становится fullstack'ом? Бэк особо не нужен, ну, сущности для GQL подготовить, и остальные мелочи. Основная логика работы с БД в этом случае (в отличие от REST) становится полной ответственностью фронтов.
dark_ruby
18.09.2021 19:37+1отчасти согласен, немного смещается стэк, и большАя часть логики приложения переезжает на клиента, а gql становится своеобразной "базой данных"
Fi1osof
18.09.2021 19:57+2Скажу от себя, как fullstack, который в свое время довольно много работал с базами данных: ваша мысль совершенно верная. Более того, сейчас вполне есть средства для организации работы с базами данных и настройки всего так, что фронт будет очень сильно связан с бэком вплоть до типизации запросов к БД. Вот для примера моя сборка: https://github.com/prisma-cms/nextjs-nexus
1. prisma-2 обеспечивает взаимодействие с БД (MySql, Postrges и другие), в том числе управление структурой базы, миграции и т.п. При этом генерируется API вместе с типизацией. То есть прописал схему, выполнил деплой, получил типизированное бэк-АПИ.
2. nexus-js + nexus-plugin-prisma дают механизмы написания GraphQL-API в связке с типами призмы, то есть с полным соответствием бэк-АПИ.
3. Упомянутый в статье codegen позволяет стянуть Graphql-API-схему, найти все граф-запросы во фронте, провалидировать их на соответствие этой схеме и сгенерировать нужные методы и типы, и весь фронт в итоге так же покрыт типами.
4. react-hook-form + yup позволяют создавать типо-защищенные формы.
5. Все написано на TypeScript
И там еще некоторые мелочи, которые позволяют в итоге вести весь проект в комплексе, как будто это один сплошной фронт. И прям совсем мало есть вещей, которые можно было бы сказать "Вот это точно чистый бэк" (хотя они там конечно же есть, но все же тоже написаны на TypeScript).
Как это все работает можно посмотреть в этом видео: https://www.youtube.com/watch?v=lO29HOY3wNw
bugy
19.09.2021 08:36+5Если у вас простое круд приложение, без интеграции и функционала, то да.
Помимо БД есть, например:
Аутентификация/авторизация
Валидация
Интеграция с другими сервисами
Отправка сообщений пользователям (сис, имейл и т.п.)
Регулярно повторяемые события
Экспорт данных
И многое многое другое
К тому же, зачастую привязывать графкл к БД это плохая затея, т.к. уровень связности кода очень высок.
SPAHI4
18.09.2021 21:17Никто не заставляет использовать в простейших приложениях, где нужна быстрота и лёгкость, аполло вместо обычного клиента в несколько кб типо такого https://github.com/prisma-labs/graphql-request
derikn_mike
19.09.2021 00:35сколько пытался понять так и не понял зачем графкл нужен если есть рест
bugy
19.09.2021 08:47Представим ситуацию: нам на фронте нужно отобразить имя пользователя и список его заказов. Есть 2 способа:
Сделать апи для загрузки юзера по ИД, и отдельно списка заказов по ИД
Сделать неуниверсальный метод на бэке, который будет отдавать толстый ДТО, содержащий и имя пользователя и список заказов
Первый способ хорош тем, что на бэке не требуется лишней работы. Но дважды дергать сервер и потом всё это склеивать не очень удобно
Второй способ упрощает работу на фронте. Но тогда бэкендеры должны писать кучу скучных маппингов. А фронты ждать нового деплоя. И весь этот код нужно мейнтейнить
Графкл соответственно решает эти проблемы: фронтенд сам решает, что им нужно. И одним запросом на сервер клиент получает необходимые данные. А бэкендеры в этом даже не участвуют (только на изначальном этапе, когда проектируется схема и реализуется загрузка данных для неё)
Кроме того, графкл это ещё и удобная спецификация из коробки, где каждый может видеть, какие поля и типы существуют. И если добавить сверху typescript, получается ещё и с проверкой на компиляции.
У нас, например, фронтенд был мега-счастлив, когда мы им добавили графкл. При том что раньше мы им отдавали толстые ДТО, т.е. у них работы не сильно убавилось. Главное это независимость и контроль над тем, что они делают.
rudinandrey
19.09.2021 17:45в чем разница между
"проектируется схема и реализуется загрузка данных для неё " и
"Сделать неуниверсальный метод на бэке, который будет отдавать толстый ДТО, содержащий и имя пользователя и список заказов" ?
сместился уровень схемы чуть дальше в бек, теперь мы понимаем что нам нужно не из названия метода API через REST а из схемы GraphQL дальше то все равно ничего не меняется. Кто-то должен понять что фронту надо, дальше должен кто-то написать SQL запрос для получения этих данных в базу данных.
bugy
20.09.2021 17:15+1Если рассматривать гибкую, но непроизводительную реализацию графкл:
Для загрузки пользователей с заказами делаются 2 независимых загрузчика:
Загрузка пользователей (по ИД например)
Загрузка заказов, зная пользователей
Когда клиент запрашивает данные, !графкл движок сам! может вызывать или не вызывать эти загрузчики, в зависимости от того что запросил клиент. Например, если запросили только пользователей, то заказы никак не загружаются.
А в РЕСТе пришлось бы реализовывать новый метод.
А теперь представьте, что в некоторых случаях нужна ещё информация по продуктам. Тогда для графкл нужно добавить один загрузчик (продукта по заказам) и всё, дальше умный движок сам будет использовать то что нужно.
А для реста придется добавлять продукты в каждый эндпоинт отдельно.
Как я отметил выше, это может быть неоптимально, т.к. делается несколько запросов в БД (по одному запросу на каждый уровень). Но это не сильно критично в плане производительности и в больших приложениях писать кастомный СКЛ на каждый эндпоинт такое себе удовольствие. Если это так хочется, то есть трансформеры из графкл в скл. Тогда у вас будет ещё меньше работы.
НО у этого обсуждения производительности есть один большой нюанс: графкл это ни разу ни про БД, а про запросы к данным. В общем случае у вас для заказов и продуктов могут быть свои сервисы. БД может быть ноуСКЛ. А какие то данные могут вычисляться на лету. И тогда у вас будет выбор: для РЕСТа каждый раз это загружать вручную и склеивать. Или для графкл написать пару загрузчиков данных (по одному на каждый тип), которые покроют 10-20 разных сценариев.
ПС извините если криво объяснил. Для меня графкл тоже был непонятно зачем, пока не появилась необходимость склеивать данные из разных сервисов.
rudinandrey
20.09.2021 20:33да я понял, просто Вы говорите, для GraphQL пару обработчиков, типа ну тут ничего трудного. а для REST это новый эндпоинт, как будто это прям проблема проблем.
ну ок. как Вам такой вариант?
site.ru/api/getUser?id=123&orders=true
в этом же endpoint'е мы смотрим, нужен ли клиенту список заказов? или нет, если нужен добавляем его в результирующий JSON.
Я к тому что, не то же ли это самое? только в профиль. поменяли описание запроса, все остальное то никуда не девается. тот же запрос только описанный в схеме.
bugy
20.09.2021 21:59+1а для REST это новый эндпоинт, как будто это прям проблема проблем
В худшем случае это не один эндпоинт, а набор всевозможных комбинаций под разные экраны на фронте.
У нас количество эндпоинтов, для среднего проекта, около сотни. И большинство из них очень похожи, но отличаются парой лишних связанных полей. Соответственно этот зоопарк постепенно заменим на графкл. Где будет максимум 20 загрузчиков
И добавить не проблема, но меинтейнить (если во внутренних сущностях меняются поля), становится гораздо сложнее
site.ru/api/getUser?id=123&orders=true
Да, это сильно упрощенный вариант графкл, и если этого достаточно, то почему бы и нет. И если таких эндпоинтов несколько штук, то не проблема склеивать всё вручную в каждом эндпоинте.
Но графкл сверху добавляет ещё:
возможность загружать только необходимые поля (тем самым уменьшая трафик и время загрузки)
шареную спецификацию из коробки
удобное версионирование: не обязательно вводить новую версию, можно просто добавлять новые поля когда нужно, а старые удалять, если они не используются
неограниченный уровень вложенности данных (orders=true&order.products=true&order.products.availability=true)
графкл это стандартизированный язык запросов. В то время как предложенный рест вариант это личный велосипед, который сложнее изучить для сложных случаев
В общем, ИМХО, если проект небольшой и у команды с десяток эндпоинтов, при 1-2 связанных сервисах, то скорее всего графкл не нужен и рест будет быстрее.
Если проект покрупнее и вы постоянно занимаетесь склейкой данных для фронта (или других сервисов), то вполне вероятно, что графкл облегчит вам жизнь. Зачастую, кстати, сами фронты и делают промежуточный API gateway сервис, который собирает данные с разных бэкендов и отдаёт их в виде графкл на фронт.
bogolt
19.09.2021 19:03+3а зачем нужен рест если есть хттп?
и зачем нужен хттп если есть ти-си-пи?
ответ во всех случаях будет один: стандартизация и упрощение кода.
По сути когда бэк предоставляет graphql то фронт напрямую работает с логическими объектами бэка.
Благодаря универсальным методам доступа приходится намного меньше модифицировать бэк для нужд фронта. Для бэка и фронта появляется универсальный понятный метод того как будут формироваться апи, не нужны эти бесконечные обсуждения как и какой апи делать.
Ну в общем на в команде очень понравилось.AndyPike
20.09.2021 20:07Да, по сути, в отличие от REST значительно ускоряется скорость разработки (не надо ждать бэков). Но и требование к фронтам становится другое. И сейчас в такой схеме очень ценится, если ты фронт, но у тебя есть опыт fullstack за плечами. То есть не надо учить тому, что такое БД, и как оно там вообще работает.
GraphQL так же необычен после SQL, как Vue после jQuery.
Сначала дико и непонятно.
Потом проникаешься, за полгода, а там другой мир.
apapacy
20.09.2021 13:39Очень характерный вопрос. Многие этого не понимают. Можете размещать в любом порядке по важности
1) Самодокументируемость — Ваша документация никогда неразойдется с кодом. Сейчас для RESTAPI похожую функциональность дает Nest.js и Fastify.js. Также естьсведения что на Pythonпоявился самодокументирвоанный фреймворк
2) Фронтенд запрашивает только нужные ему поя объектов и с нужным уровнем вложенности без необзодимости согласовывать это с бэкендом
3) Фронтенд может одним запросом запросить нескоолько разнородных объектов и не вызывать 10 API ожидая их завершения
4) На самом деле можно назвать первым. Graphql — это строго регламентирвоанная система, в то время когда RESTAPI это свод банальных правил, намприер таких https://restapitutorial.ru/lessons/httpmethods.html, которые ничего не говорят о том что делат в лучаях более слродных чем todoapp
noodles
19.09.2021 21:38Подумайте только, мы добавили 42kB рантайма
Если смотреть не min+gzip, а просто min - цифры будут ещё больше)
Gzip как я понимаю тоже не панацея, где-то неправильно настроено, где-то не поддерживается, плюс создаёт свой собственный оверхед.
apapacy
20.09.2021 13:30польза этого самого кэширования нивелируется тем, что полученные данные мы так или иначе кладем в каком-нибудь виде в стейт-менеджер
Именно этого и не нужно делать. Любой стейт-менеджмент вносит элемет императивного подхода в приложение. Хотя если приложение разрабатывается на React, особенно после появления React-router v4, оно по своему подходу декларативно.
Испольщование Apollo позволяет уйти от стейт-менеджмента и сделать полностью декларативное приложение например на React. См. сообщение наа Хабре:
https://habr.com/ru/post/358292/ Apollo graphql client — разработка приложений на react.js без redux
vpedak
20.09.2021 15:54Ну если не нравится вам использовать клиентские библиотеки то дергайте graphql просто через fetch. В чем проблема то? Мы у себя в проекте так и делаем. graphql это же просто строка посланная через HTTP POST.
vuNemesis
21.09.2021 19:35Всегда удивляет то, что практически никто не говорит про одну из крутейших фишек графа - Subscriptions (подписки). "Попробовав раз - ем и сейчас!", как говорится.
Ну и вопрос, "зачем граф, когда есть рест?" отпадет сразу, как только попробуешь граф в более-менее сложной системе.
Zibx
Две картинки в этой статье занимают 68кб, а загрузка всей страницы хабра со статьёй потребовала 4700кб (или 1.9мб gzip).
Я тоже старался сэкономить и память и процессор, но этот случай не похож на серьёзный оверхэд, 42кб с учётом токенайзера, парсера, лексера, рабочего основного функционала +всё это явно покрыто тестами и оттестировано со всех сторон. GraphQL — это сейчас сердце многих проектов и очень хорошо что оно стабильно.