Depuis quelques années, j'essaie d'identifier les cadres, les produits et les services qui permettent aux technologues de se concentrer sur l'extension de la valeur de leur propriété intellectuelle. Cela continue d'être pour moi un merveilleux voyage, rempli d'opportunités d'apprentissage uniques.
L'ingénieur en moi s'est récemment demandé s'il existait une situation dans laquelle je pourrais trouver un avantage secondaire à un concept existant dont j'ai déjà parlé. En d'autres termes, pourrais-je identifier un autre avantage ayant le même niveau d'impact que la solution parente d'origine précédemment reconnue ?
Pour cet article, je voulais approfondir GraphQL pour voir ce que je pouvais trouver.
Dans mon article « Quand il est temps de laisser REST reposer », j'ai expliqué qu'il existe des scénarios réels dans lesquels GraphQL est préférable à un service RESTful. Nous avons expliqué comment créer et déployer une API GraphQL à l'aide d'Apollo Server.
Dans cet article de suivi, je prévois d'améliorer mes connaissances sur GraphQL en parcourant les abonnements pour la récupération de données en temps réel. Nous allons également créer un service WebSocket pour consommer les abonnements.
Mon article précédent était centré sur un cas d'utilisation de Customer 360, où les clients de mon entreprise fictive conservent les collections de données suivantes :
L’un des grands avantages de l’utilisation de GraphQL est qu’une seule requête GraphQL peut récupérer toutes les données nécessaires au jeton d’un client (identité unique).
type Query { addresses: [Address] address(customer_token: String): Address contacts: [Contact] contact(customer_token: String): Contact customers: [Customer] customer(token: String): Customer credits: [Credit] credit(customer_token: String): Credit }
L'utilisation d'une approche RESTful pour récupérer la vue unique (360°) du client aurait nécessité l'assemblage de plusieurs requêtes et réponses. GraphQL nous offre une solution bien plus performante.
Pour progresser dans n’importe quel aspect de la vie, il faut atteindre de nouveaux objectifs. Pour mes propres objectifs, cela signifie :
L'idée d'utiliser des abonnements plutôt que des requêtes et des mutations dans GraphQL est la méthode préférée lorsque les conditions suivantes sont remplies :
Ceci est important car l'implémentation des abonnements dans GraphQL n'est pas une mince affaire. Non seulement le serveur sous-jacent devra être mis à jour, mais l'application consommatrice nécessitera également une refonte.
Heureusement, le cas d'utilisation que nous poursuivons avec notre exemple Customer 360 est parfaitement adapté aux abonnements. De plus, nous allons mettre en œuvre une approche WebSocket pour tirer parti de ces abonnements.
Comme avant, je continuerai à utiliser Apollo à l'avenir.
Tout d’abord, nous devons installer les bibliothèques nécessaires pour prendre en charge les abonnements avec mon serveur Apollo GraphQL :
npm install ws npm install graphql-ws @graphql-tools/schema npm install graphql-subscriptions
Une fois ces éléments installés, je me suis concentré sur la mise à jour de l' index.ts
de mon référentiel d'origine pour étendre la constante typedefs
avec les éléments suivants :
type Subscription { creditUpdated: Credit }
J'ai également établi une constante pour héberger une nouvelle instance PubSub
et créé un exemple d'abonnement que nous utiliserons plus tard :
const pubsub = new PubSub(); pubsub.publish('CREDIT_BALANCE_UPDATED', { creditUpdated: { } });
J'ai nettoyé les résolveurs existants et ajouté un nouvel Subscription
pour ce nouveau cas d'utilisation :
const resolvers = { Query: { addresses: () => addresses, address: (parent, args) => { const customer_token = args.customer_token; return addresses.find(address => address.customer_token === customer_token); }, contacts: () => contacts, contact: (parent, args) => { const customer_token = args.customer_token; return contacts.find(contact => contact.customer_token === customer_token); }, customers: () => customers, customer: (parent, args) => { const token = args.token; return customers.find(customer => customer.token === token); }, credits: () => credits, credit: (parent, args) => { const customer_token = args.customer_token; return credits.find(credit => credit.customer_token === customer_token); } }, Subscription: { creditUpdated: { subscribe: () => pubsub.asyncIterator(['CREDIT_BALANCE_UPDATED']), } } };
J'ai ensuite refactorisé la configuration du serveur et introduit la conception de l'abonnement :
const app = express(); const httpServer = createServer(app); const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' }); const schema = makeExecutableSchema({ typeDefs, resolvers }); const serverCleanup = useServer({ schema }, wsServer); const server = new ApolloServer({ schema, plugins: [ ApolloServerPluginDrainHttpServer({ httpServer }), { async serverWillStart() { return { async drainServer() { serverCleanup.dispose(); } }; } } ], }); await server.start(); app.use('/graphql', cors(), express.json(), expressMiddleware(server, { context: async () => ({ pubsub }) })); const PORT = Number.parseInt(process.env.PORT) || 4000; httpServer.listen(PORT, () => { console.log(`Server is now running on http://localhost:${PORT}/graphql`); console.log(`Subscription is now running on ws://localhost:${PORT}/graphql`); });
Pour simuler les mises à jour pilotées par le client, j'ai créé la méthode suivante pour augmenter le solde créditeur de 50 $ toutes les cinq secondes pendant que le service est en cours d'exécution. Une fois que le solde atteint (ou dépasse) la limite de crédit de 10 000 $, je réinitialise le solde à 2 500 $, simulant ainsi le paiement d'un solde.
function incrementCreditBalance() { if (credits[0].balance >= credits[0].credit_limit) { credits[0].balance = 0.00; console.log(`Credit balance reset to ${credits[0].balance}`); } else { credits[0].balance += 50.00; console.log(`Credit balance updated to ${credits[0].balance}`); } pubsub.publish('CREDIT_BALANCE_UPDATED', { creditUpdated: credits[0] }); setTimeout(incrementCreditBalance, 5000); } incrementCreditBalance();
Le fichier index.ts
complet peut être trouvé ici .
Le service étant prêt, il est temps pour nous de le déployer afin de pouvoir interagir avec lui. Étant donné qu'Heroku a bien fonctionné la dernière fois (et qu'il est facile à utiliser pour moi), restons sur cette approche.
Pour commencer, j’ai dû exécuter les commandes CLI Heroku suivantes :
$ heroku login $ heroku create jvc-graphql-server-sub Creating ⬢ jvc-graphql-server-sub... done https://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/ | https://git.heroku.com/jvc-graphql-server-sub.git
La commande a également ajouté automatiquement le référentiel utilisé par Heroku comme distant :
$ git remote heroku origin
Comme je l'ai indiqué dans mon article précédent, Apollo Server désactive Apollo Explorer dans les environnements de production. Pour que Apollo Explorer reste disponible pour nos besoins, j'ai dû définir la variable d'environnement NODE_ENV
sur développement. J'ai défini cela avec la commande CLI suivante :
$ heroku config:set NODE_ENV=development Setting NODE_ENV and restarting ⬢ jvc-graphql-server-sub... done, v3 NODE_ENV: development
J'étais prêt à déployer mon code sur Heroku :
$ git commit --allow-empty -m 'Deploy to Heroku' $ git push heroku
Un aperçu rapide du tableau de bord Heroku a montré que mon serveur Apollo fonctionnait sans aucun problème :
Dans la section Paramètres, j'ai trouvé l'URL de l'application Heroku pour cette instance de service :
https://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/
Veuillez noter que ce lien ne sera plus en service au moment de la publication de cet article.
Pour le moment, je pourrais ajouter GraphQL à cette URL pour lancer Apollo Server Studio. Cela m'a permis de voir les abonnements fonctionner comme prévu :
Notez les réponses d’abonnement sur le côté droit de l’écran.
Nous pouvons tirer parti de la prise en charge de WebSocket et des capacités d'Heroku pour créer une implémentation qui consomme l'abonnement que nous avons créé.
Dans mon cas, j'ai créé un fichier index.js avec le contenu suivant. En gros, cela a créé un client WebSocket et a également établi un service HTTP factice que je pouvais utiliser pour valider que le client était en cours d'exécution :
import { createClient } from "graphql-ws"; import { WebSocket } from "ws"; import http from "http"; // Create a dummy HTTP server to bind to Heroku's $PORT const PORT = process.env.PORT || 3000; http.createServer((req, res) => res.end('Server is running')).listen(PORT, () => { console.log(`HTTP server running on port ${PORT}`); }); const host_url = process.env.GRAPHQL_SUBSCRIPTION_HOST || 'ws://localhost:4000/graphql'; const client = createClient({ url: host_url, webSocketImpl: WebSocket }); const query = `subscription { creditUpdated { token customer_token credit_limit balance credit_score } }`; function handleCreditUpdated(data) { console.log('Received credit update:', data); } // Subscribe to the creditUpdated subscription client.subscribe( { query, }, { next: (data) => handleCreditUpdated(data.data.creditUpdated), error: (err) => console.error('Subscription error:', err), complete: () => console.log('Subscription complete'), } );
Le fichier index.js
complet peut être trouvé ici .
Nous pouvons également déployer cette application Node.js simple sur Heroku, en veillant à définir la variable d’environnement GRAPHQL_SUBSCRIPTION_HOST
sur l’URL de l’application Heroku que nous avons utilisée précédemment.
J'ai également créé le Procfile
suivant pour indiquer à Heroku comment démarrer mon application :
web: node src/index.js
Ensuite, j'ai créé une nouvelle application Heroku :
$ heroku create jvc-websocket-example Creating ⬢ jvc-websocket-example... done https://jvc-websocket-example-62824c0b1df4.herokuapp.com/ | https://git.heroku.com/jvc-websocket-example.git
Ensuite, j'ai défini la variable d'environnement GRAPHQL_SUBSCRIPTION_HOST
pour qu'elle pointe vers mon serveur GraphQL en cours d'exécution :
$ heroku --app jvc-websocket-example \ config:set \ GRAPHQL_SUBSCRIPTION_HOST=ws://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/graphql
À ce stade, nous sommes prêts à déployer notre code sur Heroku :
$ git commit --allow-empty -m 'Deploy to Heroku' $ git push heroku
Une fois le client WebSocket démarré, nous pouvons voir son statut dans le tableau de bord Heroku :
En visualisant les journaux dans le tableau de bord Heroku pour l'instance jvc-websocket-example
, nous pouvons voir les multiples mises à jour de la propriété balance
du service jvc-graphql-server-sub
. Dans ma démonstration, j'ai même pu capturer le cas d'utilisation où le solde a été réduit à zéro, simulant qu'un paiement a été effectué :
Dans le terminal, nous pouvons accéder à ces mêmes journaux avec la commande CLI heroku logs.
2024-08-28T12:14:48.463846+00:00 app[web.1]: Received credit update: { 2024-08-28T12:14:48.463874+00:00 app[web.1]: token: 'credit-token-1', 2024-08-28T12:14:48.463875+00:00 app[web.1]: customer_token: 'customer-token-1', 2024-08-28T12:14:48.463875+00:00 app[web.1]: credit_limit: 10000, 2024-08-28T12:14:48.463875+00:00 app[web.1]: balance: 9950, 2024-08-28T12:14:48.463876+00:00 app[web.1]: credit_score: 750 2024-08-28T12:14:48.463876+00:00 app[web.1]: }
Non seulement nous avons un service GraphQL avec une implémentation d’abonnement en cours d’exécution, mais nous avons maintenant un client WebSocket qui consomme ces mises à jour.
Mes lecteurs se souviendront peut-être de ma déclaration de mission personnelle, qui, selon moi, peut s’appliquer à tout professionnel de l’informatique :
« Concentrez votre temps sur la fourniture de fonctionnalités qui étendent la valeur de votre propriété intellectuelle. Tirez parti des frameworks, des produits et des services pour tout le reste. » — J. Vester
Dans cette analyse approfondie des abonnements GraphQL, nous avons consommé avec succès des mises à jour d'un serveur Apollo exécuté sur Heroku en utilisant un autre service également exécuté sur Heroku, une application basée sur Node.js qui utilise WebSockets. En tirant parti des abonnements légers, nous avons évité d'envoyer des requêtes pour des données inchangées, mais nous nous sommes simplement abonnés pour recevoir les mises à jour du solde créditeur au fur et à mesure qu'elles se produisaient.
Dans l'introduction, j'ai mentionné la recherche d'un principe de valeur ajoutée dans un sujet sur lequel j'ai déjà écrit. Les abonnements GraphQL sont un excellent exemple de ce que j'avais en tête, car ils permettent aux consommateurs de recevoir des mises à jour immédiatement, sans avoir à effectuer de requêtes sur les données sources. Cela rendra les consommateurs des données Customer 360 très enthousiastes, sachant qu'ils peuvent vivre les mises à jour au fur et à mesure qu'elles se produisent.
Heroku est un autre exemple qui continue d'adhérer à ma déclaration de mission en offrant une plate-forme qui me permet de prototyper rapidement des solutions à l'aide de la CLI et des commandes Git standard. Cela me donne non seulement un moyen simple de présenter le cas d'utilisation de mon abonnement, mais aussi d'implémenter un consommateur à l'aide de WebSockets.
Si vous êtes intéressé par le code source de cet article, consultez mes dépôts sur GitLab :
Je suis confiant lorsque je dis que j'ai réussi à améliorer mes compétences en GraphQL grâce à cet effort. Ce voyage était nouveau et stimulant pour moi… et aussi très amusant !
Je prévois de me plonger dans l'authentification ensuite, ce qui, je l'espère, me fournira une autre occasion de progresser avec GraphQL et Apollo Server. Restez à l'écoute !
Passe une très bonne journée !