GraphQL + Apollo на клиенте
Небольшой рассказ о технологии GraphQL, а также пример её использования на фронтенде вместе с библиотекой Apollo.
GraphQL - это особый язык запросов между клиентом и сервером. На низком уровне так же, как в REST- или RPC-архитектуре, используется обычный HTTP-протокол. Различие в двух основных вещах: как формируется тело запроса и как этот запрос обрабатывается сервером.
Запрос всегда выполняется методом POST (мы отличие от REST, не надо думать, какой метод использовать). Обработка запросов GraphQL происходит по единственному адресу, который можно прописать в конфиге веб-приложения. Таким образом, в отличие от REST-архитектуры, GraphQL всегда обращается по одному адресу с помощью одного метода, что упрощает применение на фронт-енде.
В теле HTTP запроса объект GraphQL переводится в JSON-подобную строку, ответом является объект data c JSON внутри. Посмотреть, как это работает, можно здесь: https://docs.github.com/en/graphql/overview/explorer . Нужно лишь зайти под своим логином из GitHub:
// GraphQL
query {
viewer {
login
}
}
// JSON
{
"data": {
"viewer": {
"login": "botyaslonim"
}
}
}
Посмотрим на конструкцию запроса:
import gql from 'graphql-tag';
const statitonFragment = gql`
fragment fuel_stationFragment on fuel_station {
latitude
longitude
name
operator
picture
status
}` export default gql` query getFuelStation($id: bigint!) { fuel_station_by_pk(id: $id) { brand fuel_station_pictures { id x y zoom image_key } id ...fuel_stationFragment picture status test_purchases(order_by: { date: asc }, where: { active: { _eq: true } }) { active ... } user_comments { rating } } content { id key text } }
${statitonFragment}
`;
В тело запроса можно помещать дополнительные инструкции для бэка (на проекте из примера бэкенд на Hasura), такие как order_by и другие для релевантного отображения данных (по Hasura см. подробную документацию).
Выше используется техника фрагментов: часть запроса можно хранить отдельно для дальнейшего переиспользования. Для упрощения написания запроса использована утилита graphql-tag.
Сам запрос имеет три важных части:
- operation type (query | mutation | subscription), в нашем случае query ( о типах ниже);
- operation name - имя запроса для удобства отладки и тестирования, в нашем случае getFuelStation;
- variable definitions - переменные запроса, у нас это $id.
Видно, то переменная может использоваться внутри любой части запроса. Сами поля запроса - это JSON-поля, значения которых должен вернуть сервер. Мы просто передаём список того, что хотим получить и в какой структуре.
Для изменения данных (аналог REST-овских POST, PUT) вместо query используется mutation:
import gql from 'graphql-tag';
export default gql`
mutation addComment(
$fuelStationId: bigint!
$userId: Int!
$text: String!
$rating: numeric!
$userName: String!
$userPicture: String!
) {
insert_user_comment(
objects: {
fuel_station_id: $fuelStationId
user_id: $userId
text: $text
rating: $rating
user_name: $userName
user_picture: $userPicture
}
) {
returning {
id
rating
text
user_name
...
fuel_station {
picture
id
latitude
longitude
...
user_comments {
rating
}
fuel_station_pictures {
url
}
test_purchases {
rating
octane_rating
volume_rating
}
}
}
}
}
`;
Вызов, понятно, происходит не при рендере компонента (как с получением данных query), а при определённых действиях:
<Formik
onSubmit={(values, { setSubmitting, resetForm }) => {
client
.mutate({
mutation: addComment,
variables: {
fuelStationId: station.id,
userId: user.id,
text: values.text,
rating: values.rating,
userName: values.userName,
userPicture: values.userPicture,
},
...
...
Вызов мутаций последовательный, то есть при изменении одних и тех же полей сначала выполнится первый запрос, только по завершению его следующий. Более подробно о мутациях также см. документацию.
Операция subscription устанавливает подписку на серверные события через WebSocket.
Apollo - наиболее распостранённая библиотека для работы с GraphQL-запросами. Ниже пример её использования (за основу взяты React-компоненты на классах).
Чтобы подключить Apollo, нужно создать клиент:
export default new ApolloClient({
uri: '.../graphql',
headers: token
? {
authorization: `Bearer ${token}`,
'content-type': 'application/json',
}
: {
'content-type': 'application/json',
},
});
Далее импортировать его в компоненте верхнего уровня:
import client from './GraphQL/client';
class Index extends Component {
render = () => (
<ApolloProvider client={client}>
<App />
</ApolloProvider>
);
}
На данный момент рекомендуется использовать React на хуках. Для этого у Apollo существует хук useQuery, который возвращает три объекта - статус загрузки, ошибку и сами данные. Пример из туториала:
import { gql, useQuery } from '@apollo/client';
const GET_DOGS = gql`
query GetDogs {
dogs {
id
breed
}
}
`;
function Dogs({ onDogSelected }) {
const { loading, error, data } = useQuery(GET_DOGS);
if (loading) return 'Loading...';
if (error) return `Error! ${error.message}`;
return (
<select name="dog" onChange={onDogSelected}>
{data.dogs.map(dog => (
<option key={dog.id} value={dog.breed}>
{dog.breed}
</option>
))}
</select>
);
}
Запросы кэшируются автоматически, однако функциональностью кэширования можно управлять (стратегии polling, refetching, см. документацию).
Но с помощью Apollo в React удобно делать запросы и в классовых компонентах:
import stationQuery from '../../GraphQL/queries/stationShort.js';
import { Query } from 'react-apollo';
...
export default class Line extends Component {
render() {
const { station } = this.props;
return (
<Query
query={stationQuery}
variables={{
id: station,
}}
>
{({ error, loading, data }) => (
<View
station={data && data.fuel_station_by_pk ? data.fuel_station_by_pk : null}
error={error}
loading={loading}
/>
)}
</Query>
);
}
}
Здесь видно, что в компоненте-обёртке (Query) нужно передать собственно запросы (query), а также - если нужно - его переменные. Дело в том, что часть полей запроса может формироваться динамически, на основе переменных.