几年来,我一直在努力寻找框架、产品和服务,让技术人员能够专注于扩大其知识产权的价值。这对我来说仍然是一段美妙的旅程,充满了独特的学习机会。
我内心的工程师最近想知道,是否存在一种情况,即我能够为之前讨论过的现有概念找到次要优势。换句话说,我能否找到另一种与之前认识到的原始父解决方案具有相同影响程度的优势?
对于这篇文章,我想深入研究 GraphQL 看看能找到什么。
在我的“何时该放弃 REST ”一文中,我谈到了在现实场景中 GraphQL 比 RESTful 服务更可取。我们介绍了如何使用 Apollo Server 构建和部署 GraphQL API。
在这篇后续文章中,我计划通过介绍用于实时数据检索的订阅来提升我对 GraphQL 的了解。我们还将构建一个 WebSocket 服务来使用订阅。
我之前的文章围绕客户 360 用例展开,其中我虚构的企业的顾客维护以下数据集:
使用 GraphQL 的一大优势是,单个 GraphQL 请求可以检索客户令牌(唯一身份)所需的所有数据。
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 }
使用 RESTful 方法检索客户的单一 (360) 视图需要将多个请求和响应拼接在一起。GraphQL 为我们提供了一个性能更好的解决方案。
为了在生活的任何方面取得进步,人们必须实现新的目标。对于我自己的目标,这意味着:
当满足以下条件时,在 GraphQL 中使用订阅而不是查询和变异的想法是首选方法:
这很重要,因为在 GraphQL 中实现订阅并非易事。不仅底层服务器需要更新,而且消费应用程序也需要重新设计。
幸运的是,我们在 Customer 360 示例中追求的用例非常适合订阅。此外,我们将实施 WebSocket 方法来利用这些订阅。
和以前一样,我将继续使用 Apollo。
首先,我们需要安装必要的库来支持我的 Apollo GraphQL 服务器的订阅:
npm install ws npm install graphql-ws @graphql-tools/schema npm install graphql-subscriptions
安装这些项目后,我专注于从原始存储库更新index.ts
,以使用以下内容扩展typedefs
常量:
type Subscription { creditUpdated: Credit }
我还建立了一个常量来容纳新的PubSub
实例,并创建了一个我们稍后将使用的示例订阅:
const pubsub = new PubSub(); pubsub.publish('CREDIT_BALANCE_UPDATED', { creditUpdated: { } });
我清理了现有的解析器,并为这个新用例添加了一个新的Subscription
:
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']), } } };
然后我重构了服务器配置并引入了订阅设计:
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`); });
为了模拟客户驱动的更新,我创建了以下方法,在服务运行时每五秒将信用余额增加 50 美元。一旦余额达到(或超过)10,000 美元的信用限额,我将余额重置回 2,500 美元,模拟余额支付。
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();
完整的index.ts
文件可以在这里找到。
服务准备就绪后,我们就该部署服务,以便与其交互。由于 Heroku 上次运行良好(而且我使用起来很方便),所以我们继续使用这种方法。
首先,我需要运行以下 Heroku CLI 命令:
$ 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
该命令还自动将 Heroku 使用的存储库添加为远程存储库:
$ git remote heroku origin
正如我在之前的文章中提到的,Apollo Server 在生产环境中禁用了 Apollo Explorer。为了让 Apollo Explorer 能够满足我们的需求,我需要将NODE_ENV
环境变量设置为开发。我使用以下 CLI 命令进行设置:
$ heroku config:set NODE_ENV=development Setting NODE_ENV and restarting ⬢ jvc-graphql-server-sub... done, v3 NODE_ENV: development
我已准备好将代码部署到 Heroku:
$ git commit --allow-empty -m 'Deploy to Heroku' $ git push heroku
快速查看 Heroku 仪表板显示我的 Apollo 服务器运行正常:
在“设置”部分中,我找到了此服务实例的 Heroku 应用程序 URL:
https://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/
请注意 - 本文发布时,此链接将不再有效。
目前,我可以将 GraphQL 附加到此 URL 以启动 Apollo Server Studio。这让我看到订阅按预期工作:
注意屏幕右侧的订阅响应。
我们可以利用WebSocket 支持和 Heroku 的功能来创建一个使用我们所创建的订阅的实现。
在我的例子中,我创建了一个包含以下内容的 index.js 文件。基本上,这创建了一个 WebSocket 客户端,还建立了一个虚拟 HTTP 服务,我可以使用它来验证客户端是否正在运行:
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'), } );
完整的index.js
文件可以 在这里找到。
我们也可以将这个简单的 Node.js 应用程序部署到 Heroku,确保将GRAPHQL_SUBSCRIPTION_HOST
环境变量设置为我们之前使用的 Heroku 应用程序 URL。
我还创建了以下Procfile
来告诉 Heroku 如何启动我的应用程序:
web: node src/index.js
接下来,我创建了一个新的 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
然后,我将GRAPHQL_SUBSCRIPTION_HOST
环境变量设置为指向我正在运行的 GraphQL 服务器:
$ heroku --app jvc-websocket-example \ config:set \ GRAPHQL_SUBSCRIPTION_HOST=ws://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/graphql
此时,我们已准备好将代码部署到 Heroku:
$ git commit --allow-empty -m 'Deploy to Heroku' $ git push heroku
一旦 WebSocket 客户端启动,我们就可以在 Heroku Dashboard 中看到它的状态:
通过查看jvc-websocket-example
实例的 Heroku 仪表板中的日志,我们可以看到jvc-graphql-server-sub
服务的balance
属性的多次更新。在我的演示中,我甚至能够捕获余额减少到零的用例,模拟付款:
在终端中,我们可以使用 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]: }
我们不仅拥有一个正在运行的具有订阅实现的 GraphQL 服务,而且现在还有一个使用这些更新的 WebSocket 客户端。
我的读者可能还记得我的个人使命宣言,我觉得它适用于任何 IT 专业人士:
“将时间集中在提供能够扩展知识产权价值的功能上。其他一切都可以利用框架、产品和服务。” — J. Vester
在深入研究 GraphQL 订阅的过程中,我们已成功使用 Heroku 上运行的另一项服务(基于 Node.js 且使用 WebSockets 的应用程序)从 Heroku 上运行的 Apollo Server 中获取更新。通过利用轻量级订阅,我们避免发送针对不变数据的查询,而只需订阅以在发生信用余额更新时接收更新即可。
在介绍中,我提到在之前写过的一个主题中寻找额外的价值原则。GraphQL 订阅就是我所想的一个很好的例子,因为它允许消费者立即收到更新,而无需对源数据进行查询。这将使客户 360 数据的消费者非常兴奋,因为他们知道他们可以实时获取更新。
Heroku 是另一个继续坚持我的使命宣言的例子,它提供了一个平台,使我能够使用 CLI 和标准 Git 命令快速制作解决方案原型。这不仅让我能够轻松展示我的订阅用例,而且还可以使用 WebSockets 实现消费者。
如果您对本文的源代码感兴趣,请查看我在 GitLab 上的存储库:
我可以自信地说,通过这次努力,我成功地提升了我的 GraphQL 技能。这段旅程对我来说既新颖又充满挑战……也非常有趣!
我计划接下来深入研究身份验证,希望这能为 GraphQL 和 Apollo Server 提供另一个升级机会。敬请期待!
祝您度过愉快的一天!