Как мы использовали GraphQL в разработке на примере интернет-каталога
В этой статье мы делимся опытом реального применения GraphQL при создании интернет-каталога. Рассказываем о том, какие достоинства и недостатки этого языка запросов мы нашли при использовании стека GraphQL + Apollo Client, Next JS (SSR) + TypeScript.

Дисклеймер: мы не описываем подробно «магическую суть» и историю возникновения инструмента (об этом уже рассказано немало). Надеемся, что тем, кто уже знаком с GraphQL, окажется полезен наш практический опыт.



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

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

Нам нужно было выводить свойства товаров в древовидной структуре и обеспечить достаточно высокую скорость обработки запросов.

Мы выбрали следующий порядок работы с запросами:

  1. Запросить все товарные группы для определенной категории (как правило, около 50 групп).
  2. Запросить список товаров для каждой группы.
  3. Запросить список свойств для каждого товара.

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

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

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

Далее расскажем подробнее непосредственно о работе с GraphQL.

Особенности работы с GraphQL


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

Мы учитывали, что GraphQL был разработан для упрощения разработки и поддержки API, прежде всего, за счет наличия единственной конечной точки.

GET	/news
GET	/posts
POST	/news
POST	/post


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

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

const FETCH_USER_DATA = gql`
 query FetchUserData {
   user {
     firstName
     lastName
     date
   }
 }
`;


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

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

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

Также на фронте мы использовали фреймворк NextJS, выбранный с учетом следующих факторов: пре-рендеринг (NextJS предоставляет очень простой механизм реализации статической генерации и SSR “из коробки”), поддержка всех существующих css-in-js решений, динамический роутинг, поддержка статических файлов (например, изображений) в React-компонентах.

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

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

`getInitialProps` (SSR) – при первой загрузке запускается на сервере, запрашивает данные и затем передает их в компонент в качестве props.

function Component ({data}) {
 ...
}
 
Component.getInitialProps = async (ctx) => {
 const res = await fetch('https://...')
 const json = await res.json()
 return { data: json.data }
}


`getStaticProps` (Static Generation) – запускается на build-этапе. NextJS производит пре-рендер страницы с использованием props, вернувшихся из данной функции.

export async function getStaticProps(context) {
 return {
   props: {}, // передает данные в компонент в качестве props
 }
}


`getServerSideProps` (Server Side Rendering) – NextJS производит пре-рендер страницы при каждом запросе, используя данные, вернувшиеся из данной функции в качестве props.

export async function getServerSideProps(context) {
 return {
   props: {}, // передает данные в компонент в качестве props
 }
}


Таким образом, все перечисленные функции объявляются вне компонента, получают некоторые данные и передают в компонент. Отсюда вытекает одна из проблем взаимодействия с Apollo Client. Библиотека предоставляет такие механизмы запросов, как хук `useQuery` и компонент `Query`, при этом ни один из предложенных способов невозможно использовать вне компонента. С учетом этого в нашем проекте мы решили использовать библиотеку next-with-apollo и в итоге остались довольны результатом.

Еще одна известная проблема Apollo Client, с которой нам также довелось столкнуться – это возможность появления бесконечного цикла запросов из хука `useQuery`. Суть проблемы заключается в том, что Apollo Client продолжает бесконечно отправлять запросы до тех пор, пока успешно не получит данные. Это может привести к ситуации, когда компонент “повиснет” в бесконечном запросе, если сервер по какой-либо причине не может вернуть данные.

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

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

Стоит отметить, что одним из преимуществ GraphQL считается удобство разработки и поддержки API, но это свойство более значимо для backend-разработки и, по нашим наблюдениям, не оказывает значительного влияния на фронт. Из-за генерации типов при каждом изменении схемы на бэке мы переписывали все затронутые запросы. Бэку также приходилось дорабатывать схему, если нам на фронте нужно было получить что-то, что еще не было реализовано. При этом генерация типов при использовании TypeScript позволяла отлавливать многие ошибки еще на стадии написания кода.

Подводя итоги


По нашим наблюдениям, GraphQL достаточно широко востребован в различных типах IT-решений и обеспечивает определенные преимущества для команды разработки. Суммируем основные особенности GraphQL, с которыми мы встретились при разработке проекта:



Спасибо за внимание! Надеемся, что этот пример был вам полезен.