Баннер мобильный (1) Пройти тест

Как новичку начать пользоваться GraphQL и зачем это нужно

Подробная инструкция с примерами и лайфхаками

Инструкция

1 марта 2024

Поделиться

Скопировано
Как новичку начать пользоваться GraphQL и зачем это нужно

Содержание

    Популярный способ организации клиент-серверного взаимодействия — REST API. Но у инструмента есть альтернативы. Одна из них — GraphQL. Разберемся в неочевидных преимуществах, посмотрим на недостатки, развенчаем несколько мифов и попытаемся ответить на основной вопрос — стоит ли переходить на GraphQL.

    Немного теории

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

    GraphQL — язык, в отличие от REST. С его помощью разработчик описывает взаимодействие клиента и сервера.

    В отличие от REST, в GraphQL всего одна точка взаимодействия (endpoint) с бэкендом. Т. е. неважно, что вы хотите запросить или изменить: клиент будет взаимодействовать с одной конечной точкой, одним URL.

    Основная часть GraphQL — схема (schema), которая описывает все типы, запросы и их взаимодействие, которые вы можете использовать в рамках текущего endpoint. Схема — не удобное дополнение, как, например, Swagger, а обязательная начальная точка проектирования любого GraphQL API. 

    Есть два типа операций: запросы (query) и мутации (mutation). Первые отвечают за получение данных, вторые — за их изменение. Еще есть подписки (subscriptions), но в этой статье мы не будем их рассматривать.

    Доступны базовые типы: 

    • ID: уникальный идентификатор объекта, реализован как String;
    • String: строки, например, имя пользователя;
    • Boolean: булевое значение (true / false);
    • Int: целочисленное (1, 2, 3, …);
    • Float: число с плавающей точкой (1.5);
    • Enum: перечисления.

    Также можно описать собственные типы на основе базовых, пометить значения как обязательные или нет (знак «!» после указания типа), а также указать, что ожидается список, а не единичное значение (помечается скобками «[]», как в примере ниже). 

    Рассмотрим пример схемы:

    type Query {
    
      user(id: ID!): User
    
      users: [User]
    
    }
    
    type Mutation {
    
      updateUser(id: ID!, name: String, age: Int): User
    
    }
    
    type User {
    
      id: ID!
    
      name: String!
    
      age: Int!
    
    }

    Здесь говорится, что существует запрос (query) user, который принимает id типа ID и возвращает объект типа User, состоящий из трех полей: id типа ID, name типа String и age типа Int.  

    Запрос users вернет список всех User. 

    А мутация updateUser обновляет поля существующего пользователя по его id.

    Клиент может запросить данные, например, по пользователю с ID 42, следующим запросом на языке GraphQL:

    query {
    
      user(id: 42) {
    
        id
    
        name
    
      }
    
    }

    В запросе мы не указали age, а следовательно, эти данные не будут возвращены на клиент, т.е. если они нам не нужны, мы сохраним немного байт и времени на пересылке данных между клиентом и сервером. 

    Базовый пример работы

    Я подготовил тестовое приложение, которое можно скачать и собрать из исходников: https://github.com/NevRA/skillfactory-graphql.

    Приложение состоит из демо-сервера, песочницы и нескольких примеров на React, которые мы рассмотрим далее.

    Запустите в корне проекта npm install && npm start и откройте песочницу по адресу http://localhost:4000/.

    Вы сможете посмотреть описание схемы (по факту — документация нашего API):

    GraphQL. Описание схемы, документация нашего API в нашей песочнице

    Важно поэкспериментировать с самими запросами, например, получить всех пользователей, доступных в системе:

    Получение всех пользователей GraphQL

    Или конкретного:

    Получение конкретного пользователя, GraphQL

    Или обновить пользовательские данные:

    GraphQL, обновление пользовательских данных юзера

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

    На языке GraphQL мы описали контракт, а так как все типы известны заранее, то неважно, кто именно взаимодействует с бэкендом (наш клиент на react/vue/angular/…, песочница, другой бэкенд или любое другое приложение).  Важно, чтобы клиент понимал язык. С GraphQL endpoint мы можем отлаживать API и экспериментировать с запросами при наличии любого клиента, понимающего GraphQL. 

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

    Генерация данных в приложении

    Реализуем это же в React-приложении. Будем использовать TypeScript и библиотеку Apollo Client — продвинутое решение для работы с GraphQL и управления состоянием.

    Пример можно посмотреть в папке examples/1-SimpleReact. В нем мы хотим загрузить список всех пользователей. Для этого используем информацию о схеме и сгенерируем необходимые данные и методы.

    В файле codegen.yml указываем endpoint (schema). Т. е. мы подсказываем генератору, откуда брать схему (наш сервер, который мы запустили в предыдущем разделе):

    overwrite: true
    
    generates:    
    
     src/generated/graphql.d.ts:
    
       documents: './src/**/*.graphql'
    
       schema:
    
         - http://localhost:4000
    
       plugins:
    
         - typescript
    
         - typescript-operations
    
         - typescript-react-apollo

    Создаем файл users.graphql с нашим запросом, который мы использовали в песочнице:

    query Users {
    
     users {
    
       id
    
       age
    
       name
    
     }
    
    }

    После того как мы запустим npm run compile, будут сгенерированы не только все нужные типы, но и хуки (React Hooks) для нашего приложения. Поэтому весь код приложения может выглядеть лаконично:

    import { useUsersQuery } from "./generated/graphql.d";
    
    const App = () => {
    
     const { data } = useUsersQuery();
    
     return (
    
       <div className="App">
    
         {data &&
    
           data.users?.map((user) => (
    
             <div className="card" key={user?.id}>
    
               <div className="user-id">{user?.id}</div>
    
               <div className="user-name">{user?.name} {`${user?.age} y/o`}</div>
    
             </div>
    
           ))}
    
       </div>
    
     );
    
    };

    Где useUsersQuery — это хук, который был для нас сгенерирован, а data содержит информацию о пользователях, причем типизированную (мы сгенерировали все типы в TypeScript).

    Мы получили:

    1. Очень простой код с нужными типами при минимуме ручной работы. Описываем, что нужно, подключаем хук, обрабатываем данные.
    2. Если часть по генерации типов будет частью CI/CD-процесса, то у нас будут те же типы, что и на бэкенде. Если на бэкенде что-то поменяется, например, кто-то переименует поле name, мы узнаем об этом не на продакшене, а при билде, т. к. типы не совпадут, и мы получим ошибку TS.

    Миф №1: Загружаем только нужные данные

    Когда говорят про преимущества GraphQL, один из основных аргументов — загрузка только запрашиваемых данных без изменения бэкенда. Т. е. на примере выше мы могли не загружать, например, age. 

    Это больше миф, т. к. функционал легко реализуется и с REST, например, так: /users/42?fields=name,age. Подобный подход используют разные компании, в том числе Facebook*. Т. е. иногда это удобно, но не является killer feature GraphQL.

    Управление состоянием в приложении

    Killer feature здесь может быть работа с состоянием. Я подготовил пример: examples/2-StateReact. В нем за счет мутации updateUser попробуем изменить состояние пользователя и посмотреть, что произойдет со списком, загруженным ранее. 

    Для этого обновим предыдущее приложение, добавим новую мутацию updateUser.graphql и обновим основной компонент:

    import { useState } from "react";
    
    import { useUsersQuery, useUpdateUserMutation } from "./generated/graphql.d";
    
    const App = () => {
    
     const [id, setId] = useState("");
    
     const [name, setName] = useState("");
    
     const [age, setAge] = useState(0);
    
     const { data } = useUsersQuery();
    
     const [updateUser] = useUpdateUserMutation();
    
     const handleUpdate = () => {
    
       updateUser({
    
         variables: {
    
           id,
    
           name,
    
           age,
    
         },
    
       });
    
     };
    
     return (
    
       <div className="App">
    
         <div className="container">
    
           {data &&
    
             data.users?.map((user) => (
    
               <div className="card" key={user?.id}>
    
                 <div className="user-id">{user?.id}</div>
    
                 <div className="user-name">
    
                   {user?.name} {`${user?.age} y/o`}
    
                 </div>
    
               </div>
    
             ))}
    
         </div>
    
         <div className="card-edit">
    
           <input onChange={(e) => setId(e.target.value)} />
    
           <input onChange={(e) => setName(e.target.value)} />
    
           <input type="number" onChange={(e) => setAge(Number(e.target.value))} />
    
           <button onClick={handleUpdate}>update</button>
    
         </div>
    
       </div>
    
     );
    
    };

    У нас появился новый хук, который обновляет заданного пользователя. Все вместе это выглядит вот так:

    Список слева загружен хуком из предыдущего примера. Что произойдет, когда мы нажмем кнопку update и попытаемся обновить пользователя с ID 42?

    Получим обновленное состояние:

    С помощью GraphQL и хуков проводим update пользователя

    Все дело в том, что у Apollo есть нормализованный кэш данных. Так как GraphQL передает дополнительную информацию о типе объекта, Apollo может связать ответ мутации (возвращаем обновленного пользователя и его ID) и самостоятельно обновить кэш. 

    Это очень мощная фича, которая при использовании GraphQL может заменить state management в вашем приложении. Почитать о том, как устроен кэш, можно здесь. Там же есть продвинутые техники использования. 

    Для отладки или чтобы посмотреть, как это устроено внутри, советую родное расширение для Google Chrome. Вот так хранится объект в кэше:

    Миф №2: GraphQL — приемник REST, поэтому он во всем лучше 

    GraphQL — это не преемник REST, а одна из альтернатив, у которой есть свои недостатки.

    1. Реализация бэкенда может быть затратнее. Это зависит от специфики, но,  например, GitHub рекомендует использовать REST для некоторых операций. Я часто слышал мнение, что GraphQL сложнее в оптимизации, т. к. один запрос может содержать любое число подзапросов и полей. Оптимизация всех комбинаций — головная боль. 
    2. Некоторые операции, например, загрузку файлов или обработку ошибок  реализовать сложнее. 
    3. Правильная архитектура схемы — отдельная нетривиальная задача. В REST можно создавать новые типы под каждый запрос. В GraphQL, если многое завязано на кэше, это неэффективно.

    Что дает GraphQL 

    Ускоряет разработку на фронтенде. Это связано с несколькими факторами, упомянутыми выше:

    1. Самодокументированный API. Вы всегда знаете, что доступно на бэкенде, и можете сгенерировать нужные типы и методы (например, хуки в случае React).
    2. Возможность работать с бэкендом из песочницы.
    3. Так как есть вся информация о типах и их взаимосвязях, легко организовать работу с состоянием (например, кэш в Apollo). В более сложных сценариях использования подписок это позволит получать изменения через WebSocket без дополнительных запросов. Будет проще реализовать, например, многопользовательскую работу, когда важно получать измененные данные в ответ на чью-то активность.
    4. Продвинутый инструментарий. Есть полезные техники работы с запросами. Например, повторный запрос (refetching) данных в ответ на мутацию, возможность переиспользовать части схемы (fragments), возможность написания кастомных модификаторов кэша, тонкие настройки политик-запросов и т. п.
    5. Можно реализовать SSR (Server Side Rendering) и вшивать данные на сервере, чтобы потом не запрашивать их при старте приложения. Полезно для оптимизации начальной загрузки.

    Также GraphQL хорошо «ложится» на микросервсиную архитектуру или serverless. Потому что у вас уже распределенная система, и серверные resolvers просто направляют запросы к ответственным сервисам или функциям в случае serverless. Для монолитных систем это работает хуже.

    Стоит ли переходить на GraphQL

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

    Чтобы попробовать, можно взять примеры из моего репозитория, реализовать более сложные кейсы. 

    *Принадлежит компании Meta, признанной в России экстремистской организацией. 

    Инструкция

    Поделиться

    Скопировано
    0 комментариев
    Комментарии