В Scaphold мы используем Apollo как для обогащения нашего внешнего интерфейса React-приложения данными, так и для создания быстрых backend-интерфейсов GraphQL API. Для нас было важно предоставить мощные функции в реальном времени для каждого приложения Scaphold, и новый протокол подписки Apollo позволил нам создать эти функции быстрее, чем мы могли надеяться. Этот пост представляет собой общий обзор того, как мы используем Apollo для быстрого создания функций на Scaphold.io!

Недавно мы опубликовали подробное руководство о том, как начать использовать Apollo Client и Scaphold.io для быстрого создания собственных приложений в реальном времени! В этом руководстве вы узнаете, как создать приложение для чата в реальном времени, которое мы называем Slackr. Проверить это!

Почему подписки GraphQL

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

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

Пример из реального мира

В Scaphold мы управляем большой частью инфраструктуры, чтобы API-интерфейсы наших клиентов оставались доступными и работоспособными. Одна из наших основных функций - это графический конструктор схем, который позволяет вам легко определять схему GraphQL, которая будет определять ваш Scaphold API. Однако, играя с нашим конструктором схемы, вы не видите сложной системы миграции, которая гарантирует, что база данных вашего API всегда актуальна и соответствует вашей схеме. Когда вы выполняете миграцию в свою схему, машина, которая заполняет запрос на миграцию, внесет необходимые изменения в вашу базу данных, чтобы ваш API оставался синхронизированным.

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

Введите подписки! Мы используем ту же инфраструктуру, которая поддерживает подписки в ваших клиентских API для питания систем в наших API управления. Например, наши веб-серверы подписываются на все события миграции схемы и обязательно аннулируют и перестраивают схемы, как только они изменяются. Таким образом, наше интерфейсное приложение всегда может получить самую последнюю версию вашей схемы, даже если оно может обмениваться данными с любым из наших многочисленных серверов.

Запрос подписки может выглядеть примерно так:

subscription SubscribeToSchemaMigration {
    subscribeToMigrateSchema {
        # Use this id to know which schema to invalidate.
        id
    }
}

Реализация подписок на сервере

Apollo отлично справляется с задачей упрощения логики, необходимой для реализации подписок на сервере. На практике подписки - это, по сути, интеллектуальная оболочка протокола pubsub. В системе pubsub любое количество подписчиков может «подписаться» на канал, и любое количество издателей может «публиковать» сообщения на этих каналах. Когда сообщение публикуется на канале, каждый подписчик, который подписан на этот канал, получит сообщение.

Apollo предоставляет класс SubscriptionManager, который устраняет многие трудности, связанные с поддержанием этих подписок. Давайте посмотрим, как это помогает нам думать о подписках.

Так выглядит конструктор SubscriptionManager:

class SubscriptionManager {
    constructor(options: {  
        schema: GraphQLSchema,
        setupFunctions: SetupFunctions,
        pubsub: PubSubEngine 
    }){ ... }
}

Как видите, SubscriptionManager принимает GraphQLSchema, таинственный объект SetupFunctions и PubSubEngine. Одна из приятных особенностей SubscriptionManager заключается в том, что он не делает никаких предположений относительно вашего PubSubEngine. Пока в вашем движке есть методы «публикации» и «подписки», он будет работать! Итак, это PubSubEngine, GraphQLSchema - это любой экземпляр схемы GraphQL, но как насчет SetupFunctions?

Функции настройки подписки

Это ключевая абстракция, которая делает нашу жизнь намного проще. Чтобы разобраться в этом, давайте взглянем на определение типа SetupFunctions:

interface SetupFunctions {
    [subscriptionName: string]: SetupFunction;
}
interface SetupFunction {
    (options: SubscriptionOptions, args: {[key: string]: any}, subscriptionName: string): TriggerMap;
}
interface TriggerMap {
    [triggerName: string]: TriggerConfig;
}
interface TriggerConfig {
    channelOptions?: Object;
    filter?: Function;
}

Во-первых, типы в JavaScript / TypeScript потрясающие. Типы упрощают анализ и понимание нашего кода. Если вы еще не используете их, я настоятельно рекомендую попробовать TypeScript или Flow.

Вернуться к подпискам. Как видите, объект SetupFunctions представляет собой сопоставление subscriptionName с функцией, которая возвращает TriggerMap, которое является сопоставлением из triggerName в TriggerConfig, который сам определяет два дополнительных свойства: channelOptions и filter. Вау, это был полный рот. Объект channelOptions позволяет нам динамически добавлять контекст к каналам (см. Хорошая статья о channelOptions), а функция фильтра позволяет вам решить, должен ли данный объект, который был передан через триггер, запускать запрос подписки или нет.

Примечание. triggerName является синонимом имен каналов PubSub, запускающих подписку.

Полный пример

Давайте возьмем простую схему и рассмотрим, как настроить SubscriptionManager.

# schema.graphql
type Schema {
    query: ...
    mutation: ...
    subscription: Subscription
}
type Subscription {
    subscribeToNewPosts(filter: PostFilter): Post
}
type Post {
    title: String!
    content: String
}
type StringFilter {
    eq: String
    lt: String
    gt: String
    matches: String
}
type PostSubscriptionFilter {
    title: StringFilter
    content: StringFilter
}

Пока мы занимаемся этим, давайте создадим фактический объект GraphQLSchema из нашего документа graphql. makeExecutableSchema берет строку документа graphql и карту преобразователя и объединяет их в graphql-js GraphQLSchema.

import schemaGql from './schema.graphql';
import { makeExecutableSchema } from 'graphql-tools';
const resolverMap = {
  Mutation: ...,
  Query: ...,
  Subscription: {
    subscribeToNewPosts(newPost, args, context, info) {
      return newPost;
    },
  },
};
const schema = makeExecutableSchema({
  typeDefs: schemaGql,
  resolvers: resolverMap,
});

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

subscription SubscribeToNewPosts(
  $postFilter: PostSubscriptionFilter
) {
    subscribeToNewPosts(filter: $postFilter) {
        title
        content
    }
}

С переменными:

{
    "postFilter": {
        "title": {
            "matches": ".*GraphQL.*"
        }
    }
}

Эта подписка будет получать данные каждый раз, когда создается новое сообщение с заголовком, соответствующим регулярному выражению /.*GraphQL.*/. Теперь, когда у нас есть определенный API, давайте взглянем на SetupFunctions для нашей схемы.

Функции настройки

Чтобы реализовать нашу схему, мы должны передать SubscriptionManager объект SetupFunctions следующим образом:

/*
A simple example. The pubsub implementation is based on nodejs EventEmitter's and thus will only work if your whole app runs in a single process. In production you would likely use something like this excellent redis implementation: https://github.com/davidyaha/graphql-redis-subscriptions.
*/
import { PubSub, SubscriptionManager } from 'graphql-subscriptions';
import schema from './schema';
const pubsub = new PubSub();
const ourSetupFunctions = {
    // The name of the subscription in our schema
    subscribeToNewPosts: (options, { filter }, subName) => ({
        // A pubsub channel that fires the 
        // 'subscribeToNewPosts' subscription
        ['mutation.createPost']: {
            filter: post => {
                // We can do any filtering we want here. 
                // Returning true will push data to the client.
                return valueSatisfiesFilter(filter, post);
            }
        },
    }),
};
const subscriptionManager = new SubscriptionManager({
    schema: schema,
    pubsub: pubsub,
    setupFunctions: ourSetupFunctions,
});

Обратите внимание на структуру нашей функции настройки. Ключи первого уровня - это названия наших подписок, а ключи второго уровня - это названия любых каналов, которые должны активировать эту подписку. В нашем примере единственный канал, запускающий subscribeToNewPosts подписку, - это mutation.createPost. Это было сделано исключительно для простоты, так как вы можете иметь больше, если хотите.

Например, Scaphold позволяет подписаться на несколько событий с помощью одной подписки.

subscription SubscribeToNewAndUpdatedPosts(
  $postFilter: PostSubscriptionFilter
) {
    subscribeToPosts(
      mutations: [createPost, updatePost], 
      filter: $postFilter
    ) {
        mutation
        value {
            title
            content
        }
    }
}

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

const ourSetupFunctions = {
    subscribeToPosts: (options, { filter }, subName) => ({
        // The channel for create mutations
        mutation.createPost: {
            filter: post => {
                return valueSatisfiesFilter(filter, post);
            }
        },
        // The channel for update mutations
        mutation.updatePost: {
            filter: post => {
                return valueSatisfiesFilter(filter, post);
            }
        },
    }),
};

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

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

Заканчивать

Теперь, когда у нас есть SubscriptionManager, все, что нам нужно сделать, - это подключить его к HTTP-серверу, чтобы наш API мог отправлять запросы из наших клиентских приложений.

import { createServer } from 'http';
import { Server } from 'subscriptions-transport-ws';
import mySubcriptionManager from './subscriptionManager';
const WS_PORT = 3001;
const httpServer = createServer((request, response) => {
  response.writeHead(404);
  response.end();
});
httpServer.listen(WS_PORT, () => console.log(
  `Websocket Server is now running on http://localhost:${WS_PORT}`
));
const server = new Server({ 
    subscriptionManager: mySubcriptionManager 
}, httpServer);

Вот и все! Теперь у нас есть сервер GraphQL, работающий в реальном времени!

Take Aways

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

При этом, если вы хотите начать создавать приложения GraphQL в реальном времени без необходимости писать собственный API или управлять собственной инфраструктурой, взгляните на Scaphold.io! Через несколько минут у вас может быть развернутый в реальном времени GraphQL API вашего собственного дизайна, готовый для работы вашего следующего приложения. Дизайнер схемы проведет вас через определение вашей модели данных, после чего вы сможете сразу же начать выпускать запросы подписки прямо из GraphiQL. Самое приятное: это бесплатно!

Спасибо за внимание! Если вам все еще интересно, ознакомьтесь с нашим Руководством по интерфейсным подпискам GraphQL и следите за тем, как мы создаем Slackr, приложение для чата в реальном времени.