Всем привет! Меня зовут Илья, я фронтенд-разработчик в hh.ru. В статье расскажу, как внедрить GraphQL на фронте, не переломав всё на своем пути.


В проекте мы используем React и Redux, для асинхронных запросов у нас есть собственная библиотека, а бэкенд работает на Java. Для получения данных используем страничные URL, а когда заходим на страницу, прямо по URl-у забираем все данные Аяксом. Это влечет за собой две проблемы — overfetching и underfetching. Проще говоря, либо у нас избыток данных, которые используются в данном рендере, либо их нехватка. Эту беду и призван решить GraphQL.


Если лень читать или больше нравится видеоформат — вам сюда.


Внедрение GraphQL на фронтенде без переломов


Начнем ресерч


Итак, мы вместе с бэкендом уходим на ресерч. В процессе бэк выбирает свой фреймворк, а мы смотрим, что у нас там есть на фронте. А там целых три фреймворка — Relay, Apollo и URQL.


Relay


https://relay.dev/


Relay — это библиотека Фейсбука, очень мощный инструмент. В нем целая плеяда возможностей, но он оказывает довольно сильное влияние на архитектуру фронтенда и даже бэкенда. Это нам не очень подходит.


Apollo


https://www.apollographql.com/docs/react/


У Apollo потрясающее комьюнити, очень много пакетов, плюшек и свистелок. Это очень подкупает, но поскольку это большой фреймворк, он теряет в гибкости. Впрочем, все еще оставаясь привлекательным. Советую посмотреть доклады Павла Черторогова, он очень здорово качает тему GraphQL, и Apollo в частности. Если вы начинаете делать что-то с нуля, я бы посоветовал использовать только Apollo, он дает офигенный старт, и в принципе создает хорошую базу для приложения или клиент-сервера на Node.js или BFF.


URQL


https://formidable.com/open-source/urql/docs/


Теперь про URQL. В отличие от двух предыдущих — это библиотека, и нам она очень понравилась. Она позволяет гибко все настроить и оказывает не такое сильное влияние на архитектуру в самом приложении. А поскольку у нас React и Redux, достаточно старый стек и приложению больше пяти лет, нам требуется гибкое решение, чтобы пробовать разные гипотезы. URQL хорошо подходит для использования GraphQL на маленьких и больших проектах. Он максимально гибкий и при этом имеет такие же плюшки, как Apollo. Да, их не настолько много, но комьюнити активно развивается.


Ниже приведена таблица с указанием плюсов и минусов URQL по отношению к другим фреймворкам.


https://formidable.com/open-source/urql/docs/comparison/#comparison-by-features


All features are marked to indicate the following:


  • ✅ Supported 1st-class and documented.
  • ???? Supported and documented, but requires custom user-code to implement.
  • ???? Supported, but as an unofficial 3rd-party library. (Provided it's commonly used)
  • ???? Not officially supported or documented.

Core Features


urql Apollo Relay
Extensible on a network level ✅ Exchanges ✅ Links ✅ Network Layers
Extensible on a cache / control flow level ✅ Exchanges ???? ????
Base Bundle Size 5.9kB (7.1kB with bindings) 32.9kB 27.7kB (34.1kB with bindings)
Devtools
Subscriptions
Client-side Rehydration
Polled Queries ????
Lazy Queries
Stale while Revalidate / Cache and Network
Focus Refetching @urql/exchange-refocus ???? ????
Stale Time Configuration @urql/exchange-request-policy ????
Persisted Queries @urql/exchange-persisted-fetch apollo-link-persisted-queries
Batched Queries ???? apollo-link-batch-http ???? react-relay-network-layer
Live Queries ???? ????
Defer & Stream Directives ???? ???? (unreleased)
Switching to GET method ???? react-relay-network-layer
File Uploads @urql/exchange-multipart-fetch ???? apollo-upload-client ????
Retrying Failed Queries @urql/exchange-retry apollo-link-retry DefaultNetworkLayer
Easy Authentication Flows @urql/exchange-auth ???? (no docs for refresh-based authentication) ???? react-relay-network-layer
Automatic Refetch after Mutation ✅ (with document cache) ????

Итого: в результате ресерча мы поняли, что нам не подходит ни Relay, ни Apollo, ни URQL. Бэкенд сообщил, что ему необходимо сделать достаточно много изменений в бэке и архитектуре, и, если бы они начали это делать одновременно с нашим фронтендом, то мы бы просто заморозили разработку.


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


Требования


  1. GraphQL клиент
  2. CI/CD и pre-commit
  3. Playground
  4. Unit-тесты
  5. Стабы
  6. GraphQL схема

Делаем GraphQL клиент


Мы используем на проекте axios и к нему написано много интерцептеров, которые также должны работать и для graphql запросов.


Для работы graphql на проекте понадобятся пакеты graphql, graphql-tag


Выполняем
yarn add graphql
yarn add -D graphql-tag


// Функция print работает с ast, преобразовывая его в строку запроса
import { print } from 'graphql/language/printer';
// Наша обертка над axios
import { HttpClient, HttpClientError } from 'Modules/http/httpClient';
import { graphQLEndPoint } from 'Utils/domain';

const httpGraphqlClient = new HttpClient({
  baseURL: graphQLEndPoint,
});

const getHttpConfig = ({ query, variables, ...rest }, operationName) => ({
  method: 'POST',
  data: {
    // в query попадает ast, которое подготовлено лоадером вебпак graphql-tag/loader
    // утилитой print из graphql ast преобразуем query в строку
    query: print(query),
    variables,
    // данные для мониторинга
    operationName,
  },
  params: {
    // дополнительно к запросу запишем параметр operationName, чтобы в логах видеть конкретную операцию graphqlEndpoint?operationName=xxx
    operationName,
  },
  ...rest,
});

const extractOperationName = (query) => {
  const { definitions } = query;
  const [def] = definitions;
  const {
    name: { value },
  } = def;
  return value;
};

const checkOperationType = (query, operationType) => {
  const { definitions } = query;
  const [def] = definitions;
  const { operation } = def;
  return operation === operationType;
};

const request = async (config) => {
  const { query, operationType } = config;
  const operationName = extractOperationName(query);

  if (!checkOperationType(query, operationType)) {
    throw new HttpClientError('fetcherGraphQL request invalid operation');
  }

  const { data } = await httpGraphqlClient.request(getHttpConfig(config, operationName));

  return data;
};

// Example 
async function fn() {
  const { data } = await request({ query: GraphQlQueryFile })
  return data;
}

Наша библиотека для асинхронных экшенов


Поговорим о нашей асинхронной библиотеке, чтобы вы понимали, как у нас работает код, и в чем фишка внедрения GraphQL на уровне сети. Для асинхронных операций у нас есть middleware. Называется она signal-middleware и находится в этом репозитории. Signal-middleware достаточно простая: у нас есть некоторый сигнал, мы на этот сигнал подписываемся по ключу и выполняем тот или иной action. Сюда мы можем заинжектить любые нужные нам функции. В данном случае сюда инжектится базово dispatch и, например, store.


Получили по ключику нашу функцию callback, исполнили, сделали запрос, записали ответ, и если что-то не так — обработали. В нашем случае библиотека доработана. Мы дополнительно провайдим наши клиенты для http-запросов. Добавив префикс :gql: в наименование сигнала, мы понимаем, какой http-клиент нужно заинжектить.


import { addReaction } from 'signal-middleware';

// http клиенты
import fetcher from 'Utils/fetcher/fetcher';
import fetcherGraphQL from 'Utils/fetcher/fetcherGraphQL';

// addReaction.js
export default (signalName, cancelType, signalReaction) => {
  let [cancelName, reactionHandler] = [cancelType, signalReaction];
  const processFetcher = signalName.includes(':gql:') ? fetcherGraphQL : fetcher;

  if (typeof cancelName === 'function') {
    cancelName = null;
    reactionHandler = cancelType;
  }

  const reaction = (storeUtils, ...args) =>
    reactionHandler(
      {
        ...storeUtils,
        fetcher: cancelName === null ? processFetcher : processFetcher.uniq(cancelName),
      },
      ...args
    );

  addReaction(signalName, reaction);
};

// signal.js
addReaction(`:gql:signal`, async ({ fetcher }) => {
    const data  = await fetcher.query(GraphQLQueryFile);
    //....
})

Проверки на CI/CD и pre-commit


Проверки на этапах CI/CD и pre-commit нужны в ситуации, если мы как-то ошибемся и наша схема не сойдется с бэкендом. Или что-то может пойти не так: мы передадим не тот аргумент, обратимся к полю, которого не существует или опечатаемся.


Настраиваем конфигурацию graphql на проекте, используя graphql-config


# .graphqlrc

# Путь до схемы
schema: "./.codegen/schema.graphql"
# Путь до операций и фрагментов
documents: "./app/**/**/*.graphql"

Далее подключаем плагин для eslint @graphql-eslint/eslint-plugin


Настраиваем конфигурацию:


// .eslintrc
    {

      "files": "*.graphql",
      "extends": "plugin:@graphql-eslint/operations-recommended",
      "plugins": ["@graphql-eslint"],
      "rules": {
        "prettier/prettier": [2, { "parser": "graphql" }],
        "spaced-comment": "off",
        "@graphql-eslint/executable-definitions": "error",
        "@graphql-eslint/fields-on-correct-type": "error",
        "@graphql-eslint/fragments-on-composite-type": "error",
        "@graphql-eslint/known-argument-names": "error",
        "@graphql-eslint/known-directives": "error",
        "@graphql-eslint/known-type-names": "error",
        "@graphql-eslint/no-anonymous-operations": "error",
        "@graphql-eslint/no-deprecated": "error",
        "@graphql-eslint/no-unused-variables": "error",
        "@graphql-eslint/provided-required-arguments": "error",
        "@graphql-eslint/scalar-leafs": "error",
        "@graphql-eslint/unique-argument-names": "error",
        "@graphql-eslint/unique-input-field-names": "error",
        "@graphql-eslint/unique-variable-names": "error",
        "@graphql-eslint/value-literals-of-correct-type": "error",
        "@graphql-eslint/variables-are-input-types": "error",
        "@graphql-eslint/variables-in-allowed-position": "error",
        "@graphql-eslint/require-id-when-available": "off",
        // Ограничиваем глубину запроса
        "@graphql-eslint/selection-set-depth": ["error", { "maxDepth": 20}],
        // Правила нейминга для операции и фрагментов
        "@graphql-eslint/naming-convention": [
          "error",
          {
            "VariableDefinition": "camelCase",
            "OperationDefinition": {
              "style": "PascalCase",
              "forbiddenPrefixes": ["Query", "Mutation", "Subscription", "Get"],
              "forbiddenSuffixes": ["Query", "Mutation", "Subscription"]
            },
            "FragmentDefinition": {
              "style": "camelCase",
              "forbiddenPrefixes": ["Fragment"],
              "forbiddenSuffixes": ["Fragment"]
            }
          }
        ],
        // Метчинг нейминга для файлов
        "@graphql-eslint/match-document-filename": [
          "error",
          {
            "fileExtension": ".graphql",
            "query": { "style": "camelCase", "suffix": ".query" },
            "mutation": { "style": "camelCase", "suffix": ".mutation" },
            "fragment": { "style": "camelCase", "suffix": ".fragment" }
          }
        ]
      }
    }

Напишем запрос:


query Person($id: Int!) {
  person(id: $id) {
    ... on PersonItem {
      area {
        someField
      }
    }
  }
}

Делаем наш обычный git-add и выполним команду lint-staged, который у нас запускает линтинг. Команда запустилась, но линтинг сообщает, что мы не можем это зафиксить: someField нет на типе Area.


  5:9  error  Cannot query field "someField" on type "Area"  @graphql-eslint/fields-on-correct-type
✖ 1 problem (1 error, 0 warnings)

Как результат, ошибку отловим на pre-commit или на этапе CI/CD.


Playground


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


Мы решили подключать его как cdn. У нас есть сборка, которую мы перекладываем в папочку build, подготовленные файлы — graphiql.min.jsи graphiql.min.css. И подключаем в html.


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


graphiql


Запрос выполнен — получили ожидаемый результат (плейграунд с готовой документацией и хорошей интроспекцией). А написанный запрос точно так же переносится в код.


Unit-тесты


Для нашего GraphQL клиента написали небольшую функцию, с помощью которой можно замокать данные. Под капотом мы используем Nock, библиотеку для мок сети, к ней написали маленькую обертку.


Пример запроса:


query PersonExample {
  person {
    name
  }
}

// ...
const result = await request(PersonExample);
// Имя запроса будет подставлено параметром
// http://localhost/graphql?operationName=PersonExample
//...

Напишем функцию, которая будет перехватывать все запросы по параметру operationName.


import nock from 'nock';

import { graphQLEndPoint } from 'Utils/domain';

const [endPointUrl] = graphQLEndPoint.split('/graphql');

/**
 * Мок функция для удобной работы с сетевыми http-запросами graphql
 *
 * @param {string} requestName имя query или mutation для сопоставления запроса
 * @prop {object={}} params
 * @prop {boolean} [params.persist] сохранять все перехватчики
 */
export const createMockFetcherGraphQL = (requestName, { persist = false } = {}) => {
  const scope = persist ? nock(endPointUrl).persist() : nock(endPointUrl);
  //
  const request = scope.post(`/graphql?operationName=${requestName}`);

  return {
    /**
     * Прокси метод над nock.reply
     *
     * @see https://github.com/nock/nock#specifying-replies
     * @see https://github.com/nock/nock/blob/main/types/index.d.ts#L165-L184
     * @param {number} statusCode
     * @param {object|function} [reply={}] для удобства используем как body или callback для доступа к req
     * @returns
     */
    reply(statusCode, reply) {
      return request.reply(statusCode, reply);
    },
  };
};

/**
 * Сброс моков
 */
export const clearMocksFetcherGraphQL = () => {
  nock.cleanAll();
  nock.restore();
};

Теперь любой запрос на проекте можем перехватить c помощью createMockFetcherGraphQL('operationName') и подменить тело ответа.


Для наглядности рассмотрим пример, в котором хотим проверить, правильно ли прокидываются наши variables.


FetcherGraphQLGetTest#example


Подготавливаем сет данных expectVariables и expectResponse, в которых мы заинтересованы. Вызываем функцию, она должна сработать, когда у нас будет FetcherGraphQLGetTest.


Делаем запрос, сравниваем, подходят ли перехваченные variables, которые у нас есть в запросе.


Проверка прошла успешно. Именно таким образом мы можем мокать данные в unit-тестах.


Стабы


В нашем стандартном клиенте есть механизм стабов, поэтому хотелось бы видеть его и в GraphQL. Тут достаточно простая реализация: у нас есть некоторый запрос, мы смотрим на фиче-флаг — если он включен, отдаем стабы по operationName.


async function request(config) {
    //..
  try {
    if (featureEnabled(FEATURE_STUB)) {
      const result = await stubs(`graphql/${operationName}`, 'POST');
      try {
        if (result) {
            // result это объект new Response(стаб);
          const { data } = await result();
          return data;
        }
      } catch (err) {
        return Promise.reject(err.response.data);
      }
    }
  } catch (ignore) {}
  //...
}

Подготавливаем json файлы, которые будут подменять тело ответа при вызове stubs(graphql/operationName);


// Делаем проверку на окружение, чтобы стабы не попали в продакшн сборку
if (process.env.NODE_ENV === 'development') {
  // eslint-disable-next-line import/no-dynamic-require
  const requireHelper = (file) => require(`App/stubs/json/${file}.json`);
  hardcodedItems = [
    {
      regexp: 'example',
      action: createResponse(bindReceiver(requireHelper('get-example')), 200),
      method: 'GET',
      delay: 0,
    },
    {
      regexp: 'graphql/PersonsList',
      method: 'POST',
      action: createResponse(bindReceiver(requireHelper('persons.query')), 200),
      delay: 0,
    },
  ];
}

У нас есть подготовленный стаб для операции PersonsList.


Есть страничка “persons”. На скриншоте видно, что данные получаем из запроса graphql?operationName=PersonsList



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


stub


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


Более подробно об этой фиче рассказывает Никита Мостовой в докладе "Стабы для фронтенда".


Получение GraphQL схемы


Для этого написали простой скрипт, который делает запрос к graphql серверу и преобразовывает ответ в готовый SDL graphql.


const { getIntrospectionQuery, buildClientSchema, printSchema } = require('graphql/utilities');

async function fetchSchema() {
  try {
    // eslint-disable-next-line no-console
    console.log('fetchSchema', 'start', GRAPHQL_END_POINT_SCHEMA);
    const {
      data: { data: introspection },
    } = await axios({
      url: GRAPHQL_END_POINT_SCHEMA,
      method: 'POST',
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
      data: JSON.stringify({ query: getIntrospectionQuery(), operationName: 'IntrospectionQuery' }),
    });
    // eslint-disable-next-line no-console
    console.log('fetchSchema', 'ok');
    return buildClientSchema(introspection);
  } catch (e) {
    console.error(`fetchSchema ${e.message}`);
  }
}
//...

Полученную схему складываем в папку codegen.



По такому принципу мы и работаем с бэкендом. А если говорить о подходе сode-first, то мы просто переключаемся на стенды, где раскатана бэк-ветка с новой схемой, и оттуда забираем схему.


Поскольку наш проект живет в одном репозитории, бэкенды сами умеют подкладывать файлы на билде в нужную нам папку без какого-либо сетевого запроса.


Боевая задача


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


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



Попробуем перенести все это на GraphQL и сделаем запрос в GraphQL Playground.


Пробуем в Playground


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



Посмотрим, что у нас внутри person: фильтры, pageinfo и items. Из айтемов нам нужны: firstName, lastName, age и id. Запишем в запрос:



Справа на скриншоте — тело ответа.


Окей, persons получили. Теперь попробуем сделать пагинацию. Проваливаемся в документацию и выбираем pageInfo:



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



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


Еще здесь есть аргумент after, в который мы вставим то, что скопировали из endCursor:



Запрос выполнился, появилась следующая пачка кандидатов. Пагинация работает как надо.


Следующая задача — сделать фильтры из сайдбара, который я демонстрировал выше. В рамках нашей тестовой задачи будем фильтровать кандидатов по возрасту. Для начала нам нужно понять, какие состояния у фильтров. Пишем filters и смотрим, что GraphQL предлагает нам по PersonsFilters:



Очень удобно, что можно провалиться прямо на ходу — интроспекция отличная. Можно посмотреть информацию о каждом поле. У нас есть ageFrom и поле used, которое говорит, какой возраст используется в данный момент.


Напишем, что наш query принимает аргументы и попробуем их передать. Начнем с $cursor, потому что мы уже его проработали. Обозначим, что это поле string. В after тоже будет приходить $cursor. Теперь попробуем его прокинуть.



Запрос выполнился, пришли те же самые кандидаты. А, значит, все работает. Теперь попробуем с фильтром. Впишем его в query, назовем $filterInput: PersonFilterInput. Передадим его в persons и выполним запрос.



Запрос прошел, но used пустой. Выходит, нужно прокинуть параметры для фильтра. Пишем ageFrom и фильтруем кандидатов от 25 лет. Запускаем.



Огонь! Отфильтровали новых кандидатов — все, кому меньше 25 лет, полностью исключены из выборки. Также в поле used появился флажок с активным фильтром. Итого: у нас получилось организовать фильтрацию и пагинацию — всё, что требовалось для выполнения задачи.


Пробуем в коде


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



Как это будет выглядеть в GraphQL? Да практически так же. У нас точно такой же сигнал и обработка. Единственное отличие — префикс, который позволяет прокидывать по другому префиксу правильный клиент для запроса. В нашем случае он называется fetcher:



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


Передаем в запрос PesonaListQuery и variables, которые мы прокидывали еще в playground.



Вот и вся наша реализация. Мы приложили минимум усилий, чтобы сделать GraphQL-запрос. У нас просто появился флажок, который спрашивает, что использовать для загрузки списка персон: GraphQL или старый страничный url. А сам запрос состоит из тех же самых полей.


Вместо заключения


У GraphQL есть свои преимущества и недостатки. Преимущества: бэкенд и фронтенд работают по строго типизированной схеме, документированный API из коробки и большое комьюнити. Недостатком является отсутствие в GraphQL классического подхода обработки ошибок (как с HTTP-кодами: мониторинг, подсчет SLA, откаты релизов при “пятисотках").


Итак, у нас получилось бесшовно внедрить GraphQL и получить плюшки фреймворков, не переломав всё на своем пути, а также предусмотреть последующую интеграцию во фреймворк или библиотеку.

Комментарии (5)


  1. dubr
    01.09.2022 10:01
    +5

    Итого: в результате ресерча мы поняли, что нам не подходит ни Relay, ни Apollo, ни URQL.

    const extractOperationName = (query) => {
      const { definitions } = query;
      const [def] = definitions;
      const {
        name: { value },
      } = def;
      return value;
    };

    Деструктивная секта =) Это же просто

    const extractOperationName = (query) => query.definitions[0].name.value

    <закончил ворчать>


    1. IlyaGorsky Автор
      01.09.2022 12:46
      +2

      Спасибо за комментарий! Заценил скриншот ;)

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

      О том, как переезжали без боли с нашего решения на библиотеку или фреймворк и как отказались от redux, расскажем в следующей охэхэнной истории на Youtube.

      Да, согласен, можно было избежать деструктивизации query, предложенный в статье сниппет решил оставить, чтобы статья была близка к видеоряду. Сейчас наш снипет выглядит так:

      
      const extractOperationName = (query: RequestConfig['query']): string => {
        const def = query.definitions[0];
        if (def.kind === Kind.OPERATION_DEFINITION) {
          if (def.name) {
            return def.name.value;
          }
        }
        throw new HttpClientError('unknown operation name')
      };
      


  1. nin-jin
    01.09.2022 11:59
    -1

    Это влечет за собой две проблемы — overfetching и underfetching. Проще говоря, либо у нас избыток данных, которые используются в данном рендере, либо их нехватка. Эту беду и призван решить GraphQL.

    И ни слова про data duplication.

    Напишем запрос:

    query Person($id: Int!) {
      person(id: $id) {
        ... on PersonItem {
          area {
            someField
          }
        }
      }
    }

    Не, лучше так:

    const Area = HARP({
      name: Maybe( HARP( {}, String ) )
    })
    
    const Person = HARP({
      name: Maybe( HARP( {}, String ) )
      area: Maybe( Area )
    })
    
    const Query = HARP({
      person: Maybe( Person )
      area: Maybe( Area )
    })

    Теперь соберём запрос, подставив нужные данные:

    const query = Query.build({
      person: {
        '=': [ ['jin'], ['john'] ],
        name: {},
        area: {
          name: {},
        },
      },
      area: {
        '=': [[ 'msk']],
      },
    })

    Теперь загрузим данные:

    const data = await ( await fetch( query ) ).json()

    При этом на сервер уйдёт такой запрос:

    GET /person=jin,john[name;area[name]];area=msk

    А вернётся такой ответ:

    {
      "_query": {
        "person=jin,john[area[name]]": {
          "reply": [ "person=jin", "person=john" ]
        },
        "area=msk": {
          "reply": [ "area=msk" ]
        }
      },
      "person": {
        "jin": {
          "name": "Jin",
          "area": [ "area=spb" ]
        },
        "john": {
          "name": "John",
          "area": [ "area=msk" ]
        }
      },
      "area": {
        "spb": {
          "name": "Saint-Petersburg"
        },
        "msk": {
          "name": "Moscow"
        }
      }
    }

    Который без костылей ложится в кеш.


    1. IlyaGorsky Автор
      01.09.2022 14:48

      Согласен, этот недостаток стоит иметь ввиду при выборе graphql.

      Фреймворки и библиотеки маскируют умеют работать с этим недостатком, как на клиенте, так и на сервере.
      Можно посмотреть эту https://github.com/gajus/graphql-deduplicator реализацию. Она сопровождается статьей на медиуме https://gajus.medium.com/reducing-graphql-response-size-by-a-lot-ff5ba9dcb60

      C HARP не работал. Но здорово, что там эта проблема учтена.


  1. rdzior4
    01.09.2022 17:22

    ......