Фрагменты и директивы
Фрагменты позволяют один раз описать набор полей и переиспользовать его, а директивы @include и @skip — включать или выключать поля прямо в запросе по условию.
Не повторяйся в запросах: то, что в коде делают функции, во фрагментах делают именованные наборы полей.
Когда несколько частей запроса просят один и тот же набор полей, копировать его — путь к ошибкам. Фрагмент — это именованный кусок выборки на конкретном типе, который можно подставлять через ...:
fragment UserCard on User {
id
name
avatarUrl
}
query {
me { ...UserCard }
user(id: "42") { ...UserCard }
}
Теперь карточка пользователя описана один раз. Поменяешь фрагмент — изменится во всех местах сразу. Это особенно важно на клиенте: компоненты часто объявляют собственные фрагменты с нужными им полями, а страница собирает из них общий запрос. Такой подход называют co-location (соседство): фрагмент с нужными данными лежит рядом с компонентом, который их рисует. Когда компонент меняет свои потребности в данных, он правит только свой фрагмент — а родительская страница автоматически запросит обновлённый набор полей. Так UI и его запросы данных эволюционируют вместе, не разъезжаясь; это один из приёмов, благодаря которым GraphQL так удобно ложится на компонентную архитектуру React, Vue и подобных фреймворков.
Inline-фрагменты
Если поле возвращает интерфейс или объединение (union), inline-фрагмент позволяет выбрать поля в зависимости от конкретного типа:
query {
search(text: "graphql") {
... on User { name }
... on Post { title }
}
}
Директивы @include и @skip
Директивы меняют выполнение запроса. Две встроенные — @include(if:) и @skip(if:) — позволяют условно включать поля, не переписывая текст запроса:
query GetUser($withEmail: Boolean!) {
user(id: "42") {
name
email @include(if: $withEmail)
phone @skip(if: $withEmail)
}
}
Если $withEmail = true, в ответ попадёт email, а phone пропустится; при false — наоборот. Один запрос гибко подстраивается под контекст.
Как работает под капотом
Фрагменты сервер «разворачивает» — подставляет их поля на место ...Имя ещё до выполнения. Директивы @include/@skip вычисляются на этапе разбора: поле либо остаётся в дереве, либо вырезается до вызова резолверов. Смоделируем оба механизма на JS:
// фрагмент = переиспользуемый список полей
const UserCard = ["id", "name", "avatarUrl"];
const user = {
id: 42, name: "Аня", avatarUrl: "a.png",
email: "[email protected]", phone: "+7..."
};
function buildSelection(withEmail) {
// разворачиваем фрагмент
const fields = [...UserCard];
// применяем директивы
if (withEmail) fields.push("email"); // @include(if: withEmail)
else fields.push("phone"); // @skip(if: withEmail)
return fields;
}
function pick(obj, fields) {
return fields.reduce((a, f) => (a[f] = obj[f], a), {});
}
console.log(pick(user, buildSelection(true))); // ...карточка + email
console.log(pick(user, buildSelection(false))); // ...карточка + phone
Попробуй сам ▶ — добавь во фрагмент UserCard поле "name" дважды или новое поле и посмотри, как оно появится сразу в обоих ответах. Так фрагмент централизует выборку.
Частые ошибки
- Фрагмент на неподходящем типе.
fragment X on Userнельзя применить к полю, возвращающемуPost— типы должны совпадать. - Путать @skip и @include.
@skip(if: true)убирает поле,@include(if: true)оставляет. Лёгкий способ запутаться и получить не то. - Дублировать поля вместо фрагмента. Копипаст выборки в нескольких запросах рано или поздно разъедется и даст несогласованность.
Best practices
- Объявляй фрагменты рядом с UI-компонентами, которым нужны эти поля — это паттерн «colocation» в Apollo и Relay.
- Имя фрагмента делай говорящим (
UserCard,PostSummary): по нему сразу понятно, для какого блока интерфейса он нужен. - Используй
@include/@skipдля опциональных тяжёлых полей, которые нужны не на каждом экране.
Итоги
Фрагменты — это переиспользуемые наборы полей на конкретном типе, подключаемые через ...; они убирают дублирование и лежат в основе компонентного подхода клиентов. Директивы @include и @skip условно включают поля прямо в запросе. Дальше переходим к изменению данных — мутациям.