Как мы использовали 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, с которыми мы встретились при разработке проекта:

    • Генерация типов. Генерация типов graphql позволяет экономить время при написании запросов, идеально подходит для использования TypeScript и помогает отлавливать неприятные ошибки на раннем этапе разработки.
    • Быстрота обновления данных. С GraphQL в нашем случае можно было быстро обновить данные в фронтэнд-части приложения. У разработчиков была возможность вносить изменения на стороне клиента, не вмешиваясь в работу сервера (при наличии необходимых полей на бекэнде, конечно). При этом у graphql есть так называемая «песочница» для запросов, в которой можно протестировать запросы с разными параметрами
    • Обработка только нужных данных. В graphql-запросах можно точно указать, какие данные следует получать. При отправке большого количества запросов это позволяет избежать обработки излишних данных.
    • Масштабируемость. GraphQL делает удобнее работу с масштабируемыми системами, поскольку облегчает объединение данных из нескольких сервисов в один.

    Спасибо за внимание! Надеемся, что этот пример был вам полезен.
    SimbirSoft
    Лидер в разработке современных ИТ-решений на заказ

    Комментарии 6

      +1

      Что такого особенного в проекте, чтобы в относительно небольшом интернет-магазине запросы по 20 секунд работали? Да даже по 2?


      Пока из тех кейсов с graphql, что видел, оправданным выглядит только один — объединение нескольких разрозненных api за единым фасадом. В остальных случаях, если поспрашивать, аргументов кроме "модно, молодежно" особо и не было

        0

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

        0
        Привет! А вы использовали для управления состоянием apollo или взяли что-то стороннее? И ещё интересно, использовали ли фичу общего кеша сущностей?
          0
          Для управления состоянием мы использовали непосредственно Apollo и не применяли сторонних решений. Что касается кэша: нужно понимать, что имеется в виду под фичей общего кэша сущностей? Вообще в этом проекте для управления кэшированием на клиенте мы использовали Apollo Cache.
          0
          Ну со стороны фронта выглядит как идеальный вариант получения данных. Не узнавали ли Вы у разработчиков который работали со стороны бека?
          Даже при правильно построенной архитектуре бека, бавают ситуации когда на беке это реализовывать крайне неудобно.
            0
            Добрый день. Не совсем понятен вопрос по беку, уточните, пожалуйста.

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

          Самое читаемое