Le temps réel avec GraphQL: Facile ou pas facile?

Maxime Richard
Maxime RichardFebruary 13, 2020
#graphql#tutorial

Lors de ma veille quotidienne, j'ai lu un tweet d'Alex Banks qui détaille le sujet de la présentation qu'il donnera à la conférence GraphQL Summit:

Twitter might track you and we would rather have your consent before loading this tweet.

Always allow

Il s'agit d'un site web avec deux pages, une page pour participer à un sondage, et une page pour voir le résultat des votes, qui s'actualise en temps réel.

Intrigué et n'ayant jamais fait de temps réel en GraphQL, je jette un coup d’œil à son profil Github pour regarder le code qu'il pourrait proposer. Je constate cependant qu'il n'y a aucun code. Je décide donc de me lancer dans le développement de ce projet.

Cet article est le récit de ce que j'ai appris sur le sujet. Vous pouvez retrouver le code final sur github.com/zyhou/poll-realtime-graphql.

Si vous ne connaissez pas GraphQL je vous renvoie vers la série Dive Into GraphQL publiée sur ce blog en 2017.

Création du serveur

Pour aller vite dans le développement, je crée le site web avec create-react-app et le serveur avec apollo-server.

Je commence par créer le schéma GraphQL avec un modèle simple pour un sondage:

const { gql } = require("apollo-server");

const typeDefs = gql`
  type Poll {
    question: String!
    answers: [Answer]
  }

  type Answer {
    percent: Int
    option: String
  }

  type Vote {
    id: ID!
    choice: Int!
  }

  type Query {
    poll: Poll
    answers: [Answer]
  }

  type Mutation {
    addVote(id: ID!, choice: Int!): Vote!
  }
`;

Je définis ensuite la question et les réponses possibles pour le sondage.

let question = "Who let the dogs out?";
let answers = [{ option: "Who" }, { option: "Who" }];
let votes = [];

La variable votes set de persistance en mémoire pour les resolvers qui vont contenir les implémentations de mes Query et Mutation.

const resolvers = {
  Query: {
    poll: () => ({ question })
  },
  Poll: {
    answers: () => {
      return computeAnswersPercent(votes, answers); // Business function
    }
  },
  Mutation: {
    addVote: (_, { id, choice }) => {
      votes.push({ id, choice });
      return { id, choice };
    }
  }
};

A ce stade, j'ai tout ce qu'il faut pour lancer un serveur GraphQL.

const { ApolloServer } = require("apollo-server");

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: true,
  playground: true
});

server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});

Test avec GraphQL Playground

Apollo server propose GraphQL Playground, un équivalent de GraphiQL qui permet d'interroger facilement un serveur GraphQL.

Je récupère le sondage ainsi que les différentes options en faisant une query sur poll.

Poll Query

J'ajoute un vote pour le choix 2. Pour cela je fais une mutation sur addVote.

Vote Mutation

J’exécute une nouvelle fois la première query et je vois bien que mon vote a été pris en compte. Tout fonctionne!

Ajout du temps réel

Lorsque j'ajoute un vote, je souhaite que GraphQL me notifie de ce changement. L'ajout du temps réel dans GraphQL est rendu possible grâce au type Subscription que je déclare dans le schéma.

J'ajoute l'action voteAdded qui me retourne le tableau des réponses.

const typeDefs = gql`
    ...
    
    type Subscription {
        voteAdded: [Answer]!
    }
`;

Maintenant je vais créer une instance de PubSub. C'est une simple implémentation pubsub, basée sur EventEmitter.

const { PubSub } = require("apollo-server");

const pubsub = new PubSub();

J'utilise ce pubsub dans le resolver de la subscription voteAdded.

const VOTE_ADDED = "VOTE_ADDED";

const resolvers = {
  //...

  Subscription: {
    voteAdded: {
      subscribe: () => pubsub.asyncIterator([VOTE_ADDED])
    }
  }
};

Désormais, le moteur GraphQL sait que voteAdded est une subscription. A chaque fois que je vais utiliser pubsub.publish sur VOTE_ADDED. GraphQL publiera l'événement en utilisant le transport que nous exploitons.

Je modifie la mutation addVote pour ajouter le lancement de l'action. VOTE_ADDED.

const resolvers = {
  //...
  Mutation: {
    addVote: (_, { id, choice }) => {
      votes.push({ id, choice });
      pubsub.publish(VOTE_ADDED, {
        voteAdded: computeAnswersPercent(votes, answers) // Business function
      });
      return { id, choice };
    }
  }
};

A partir de ce point, j'ai mon serveur GraphQL qui m'envoie bien un événement lors de l'ajout d'un vote. Cela s'exécute par l'utilisation de websocket.

Realtime

L'objet PubSub proposé par Apollo est prévu pour les démos. Il fonctionne uniquement dans le cas où vous avez une seule instance, ou un nombre de clients réduit. Pour un usage en production, il est préférable d'utiliser d'autres implémentations PubSub comme Redis.

Création du client

Je vais seulement parler de la partie board des résultats. Si vous voulez voir les deux boutons d'actions pour voter au sondage, tout se trouve dans le composant Poll.js.

J'utilise Apollo comme client GraphQL. La mise en place de cet outil devient un peu plus complexe lorsqu'on veut faire des subscriptions.

import { ApolloClient } from 'apollo-client';
import { ApolloProvider } from '@apollo/react-hooks';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { split } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';

// Create an http link:
const httpLink = new HttpLink({
    uri: `http://localhost:4000/graphql`,
});

// Create a WebSocket link:
const wsLink = new WebSocketLink({
    uri: `ws://localhost:4000/graphql`,
    options: {
        reconnect: true,
    },
});

// split based on operation type
const link = split(
    ({ query }) => {
        const { kind, operation } = getMainDefinition(query);
        return kind === 'OperationDefinition' && operation === 'subscription';
    },
    wsLink,
    httpLink,
);

const client = new ApolloClient({
    link,
    cache: new InMemoryCache(),
});

ReactDOM.render(
    <ApolloProvider client={client}>
        <App />
    </ApolloProvider>,
    document.getElementById('root'),
);

Grâce à l'utilisation de la fonction split et des deux Link apollo, apollo-link-http et apollo-link-ws, j'ai séparé les transports par type de requête. Les queries et les mutations passent à travers HTTP, comme d'habitude. Et les subscriptions sont faites via websocket.

J'écris ma subscription comme nous l'avons vu précédemment lors du test dans GraphQL Playground.

const VOTE_SUBSCRIPTION = gql`
    subscription {
        voteAdded {
            percent
            option
        }
    }
`;

J'utilise le hook useSubscription qui va exécuter ma requête GraphQL et je réalise un design qui ressemble à celui d'Alex.

import { useSubscription } from '@apollo/react-hooks';

const GraphAnswers = () => {
    const { loading, error, data } = useSubscription(VOTE_SUBSCRIPTION);

    if (loading) return <p>Loading...</p>;
    if (error) return <p>Error :(</p>;

    const { voteAdded: choices } = data;

    return (
        <div className="halfcircle-container">
            <HalfCircle percent={choices[0].percent} />
            <div className="halfcircle-text">
                <div className="color-left">{choices[0].option}</div>
                <div className="color-right">{choices[1].option}</div>
            </div>
        </div>
    );
};

J'obtiens bien le comportement attendu comme explicité dans le tweet.

Vote and board

Conclusion

Le fait de reproduire ce que l'on voit ou ce que l'on ne comprend pas, est selon moi, un exercice enrichissant. Cela peut partir d'un simple tweet ou d'un projet que l'on trouve intéressant.

Concernant le temps réel avec GraphQL, la mise en place et l'utilisation des subscriptions se fait sans trop de difficultés. Cela apporte une vraie plus-value à votre site web.

Vous pouvez trouver la conférence d'Alex Banks sur youtube.

Did you like this article? Share it!