Всем привет! Меня зовут Илья, я фронтенд-разработчик в hh.ru. В статье расскажу, как внедрить GraphQL на фронте, не переломав всё на своем пути.
В проекте мы используем React и Redux, для асинхронных запросов у нас есть собственная библиотека, а бэкенд работает на Java. Для получения данных используем страничные URL, а когда заходим на страницу, прямо по URl-у забираем все данные Аяксом. Это влечет за собой две проблемы — overfetching
и underfetching
. Проще говоря, либо у нас избыток данных, которые используются в данном рендере, либо их нехватка. Эту беду и призван решить GraphQL.
Если лень читать или больше нравится видеоформат — вам сюда.
Начнем ресерч
Итак, мы вместе с бэкендом уходим на ресерч. В процессе бэк выбирает свой фреймворк, а мы смотрим, что у нас там есть на фронте. А там целых три фреймворка — Relay, Apollo и URQL.
Relay
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, не сильно вмешиваясь в архитектуру фронтенд приложения. Мы договорились решать всё на сетевом уровне: оставляем приложение как есть, делаем запросы и инжектим клиент в библиотеку для асинхронных экшенов. Но прежде чем это сделать, мы выдвинем некоторые требования, которые хотелось бы видеть от нашей разработки.
Требования
- GraphQL клиент
- CI/CD и pre-commit
- Playground
- Unit-тесты
- Стабы
- 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.
Заходим в плейграунд и выполняем простой запрос, который будет выглядеть так: выберем всех кандидатов, которые есть в базе.
Запрос выполнен — получили ожидаемый результат (плейграунд с готовой документацией и хорошей интроспекцией). А написанный запрос точно так же переносится в код.
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.
Подготавливаем сет данных 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.
Такой механизм стабов помогает разрабатывать в отрыве от бэкенда, готовить компоненты к данным или пробовать на фронтенде сложную логику.
Более подробно об этой фиче рассказывает Никита Мостовой в докладе "Стабы для фронтенда".
Получение 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)
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" } } }
Который без костылей ложится в кеш.
IlyaGorsky Автор
01.09.2022 14:48Согласен, этот недостаток стоит иметь ввиду при выборе graphql.
Фреймворки и библиотеки
маскируютумеют работать с этим недостатком, как на клиенте, так и на сервере.
Можно посмотреть эту https://github.com/gajus/graphql-deduplicator реализацию. Она сопровождается статьей на медиуме https://gajus.medium.com/reducing-graphql-response-size-by-a-lot-ff5ba9dcb60C HARP не работал. Но здорово, что там эта проблема учтена.
dubr
Деструктивная секта =) Это же просто
<закончил ворчать>
IlyaGorsky Автор
Спасибо за комментарий! Заценил скриншот ;)
В статье старался прояснить, что затягивание любого из фреймворков влечет за собой изменения в архитектуре фронтенд приложения. А нашей целью было попробовать graphql, не меняя текущего флоу работы.
О том, как переезжали без боли с нашего решения на библиотеку или фреймворк и как отказались от redux, расскажем в следующей охэхэнной истории на Youtube.
Да, согласен, можно было избежать деструктивизации
query
, предложенный в статье сниппет решил оставить, чтобы статья была близка к видеоряду. Сейчас наш снипет выглядит так: