提升我的GraphQL技能:实时订阅

几年前,我就开始尝试找出各种框架、产品和服务的优劣,以帮助技术人士专注于提升他们知识产权的价值。这个过程对我来说一直是一个美妙的旅程,充满了独特的学习机会。

作为一名工程师,我最近在想,是否有可能找到一个场景,为之前我提到过的一个现有概念找到一个次要的好处。也就是说,我能否找到一个与原始父解决方案先前被认可的影响相同水平的另一个好处?

为了这篇文章,我想深入研究一下GraphQL,看看我能发现什么。

在我之前的“是时候让REST休息一下了”一文中,我讨论了在现实世界的场景中,GraphQL相对于RESTful服务有哪些优势。我们学习了如何使用Apollo Server构建和部署GraphQL API。

在本文的后续部分,我计划通过介绍实时数据获取的订阅功能来提高我对GraphQL的了解。我们还将构建一个WebSocket服务来消费这些订阅。

回顾:客户360使用案例

我之前的文章主要围绕一个客户360的使用案例展开,在这个案例中,我虚构的企业的客户维护着以下数据集合:

  • 客户信息
  • 地址信息
  • 联系方式
  • 信用属性

在使用GraphQL时,一个巨大的优势是单个GraphQL请求就能获取到客户令牌(唯一身份)所需的所有数据。

JavaScript

 

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中的subscriptions价值主张
  • 使用WebSocket实现来使用GraphQL订阅

在以下情况下,使用GraphQL中的订阅而非查询和变更被认为是首选方法:

  • 对大型对象的少量、增量更改
  • 低延迟、实时更新(例如聊天应用程序)

这是重要的,因为实现GraphQL内的订阅并不是一件简单的事。不仅底层服务器需要更新,而且消费应用程序也可能需要重新设计。

幸运的是,我们正在追求的客户360度示例用例非常适合订阅。此外,我们将实现一个WebSocket方法来利用这些订阅。

像之前一样,我将继续使用Apollo。

通过订阅凭证提升等级

首先,我们需要安装支持与我的Apollo GraphQL服务器进行订阅的必要库:

Shell

 

npm install ws
npm install graphql-ws @graphql-tools/schema
npm install graphql-subscriptions 

安装这些项目后,我专注于更新我的原始存储库中的index.ts文件,以扩展typedefs常量,添加以下内容:

JavaScript

 

type Subscription {
    creditUpdated: Credit
}

我还建立了一个常量,用于容纳一个新的PubSub实例,并创建了一个示例订阅,稍后我们将使用它:

JavaScript

 

const pubsub = new PubSub();

pubsub.publish('CREDIT_BALANCE_UPDATED', {
    creditUpdated: {
    }
});

我清理了现有的解析器,并添加了一个新的Subscription用于这个新的用例:

JavaScript

 

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']),
        }
    }
};

然后,我重构了服务器配置,并引入了订阅设计:

JavaScript

 

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美元,模拟余额支付。

JavaScript

 

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表现出色(而且对我来说很容易使用),让我们继续采用这种方法。

开始之前,我需要运行以下Heroku CLI命令:

Shell

 

$ 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使用的存储库作为远程添加:

Shell

 

$ git remote
heroku
origin

在我先前的文章中提到过,Apollo Server在生产环境中禁用了Apollo Explorer。为了满足我们的需求,我需要将NODE_ENV环境变量设置为开发环境。我是用以下命令行来设置的:

Shell

 

$ heroku config:set NODE_ENV=development

Setting NODE_ENV and restarting jvc-graphql-server-sub... done, v3
NODE_ENV: development

我准备将代码部署到Heroku上了:

Shell

 

$ git commit --allow-empty -m 'Deploy to Heroku'
$ git push heroku

快速查看Heroku控制台,显示我的Apollo Server运行正常:

设置部分,我找到了这个服务实例的Heroku应用URL:

https://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/

  • 请注意:到本文发表时,这个链接将不再有效。

暂时,我可以在这个URL后添加graphql来启动Apollo Server Studio。这让我能够看到订阅如预期般正常工作:

请注意屏幕右侧的订阅响应。

用WebSocket技能提升

我们可以利用WebSocket支持和Heroku的能力,创建一个消耗我们所创建的订阅的实现。

在我的案例中,我创建了一个index.js文件,内容如下。基本上,这创建了一个WebSocket客户端,同时也建立了一个用于验证客户端是否运行的虚拟HTTP服务:

JavaScript

 

import { createClient } from "graphql-ws";
import { WebSocket } from "ws";
import http from "http";

// 创建一个虚拟HTTP服务器来绑定到Heroku的$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);
}

// 订阅creditUpdated订阅
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如何启动我的应用程序:

Shell

 

web: node src/index.js

接下来,我创建了一个新的Heroku应用程序:

Shell

 

$ 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服务器:

Shell

 

$ heroku --app jvc-websocket-example \
    config:set \
GRAPHQL_SUBSCRIPTION_HOST=ws://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/graphql

此时,我们准备将代码部署到Heroku:

Shell

 

$ git commit --allow-empty -m 'Deploy to Heroku'
$ git push heroku

一旦WebSocket客户端启动,我们可以在Heroku控制台中看到其状态:

通过在Heroku控制台中查看jvc-websocket-example实例的日志,我们可以看到jvc-graphql-server-sub服务balance属性的多次更新。在我的演示中,我甚至能够捕获余额减少到零的使用情况,模拟支付:

在终端中,我们可以使用CLI命令heroku logs访问相同的日志。

Shell

 

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上的服务——一个使用WebSockets的Node.js应用程序——来消费从运行在Heroku上的Apollo服务器接收到的更新。通过利用轻量级订阅,我们避免了针对不变数据发送查询,而只是订阅接收信用余额的更新。

在介绍中,我提到过在我以前写过的主题中寻找一个额外的价值原则。GraphQL订阅是我所想的一个很好的例子,因为它允许消费者立即接收更新,而无需对源数据进行查询。这将使Customer 360数据的消费者非常兴奋,因为他们知道可以实时接收更新。

Heroku是另一个继续遵循我使命声明的例子,它提供了一个平台,使我能够使用CLI和标准Git命令快速原型化解决方案。这不仅给了我一个展示我订阅用例的简单方法,还可以实现使用WebSockets的消费者。

如果你对本文的源代码感兴趣,请查看我在GitLab上的代码库:

我在这里自信地说,通过这次努力,我已经成功地提升了我对GraphQL的技能。这段旅程对我来说既新奇又具有挑战性,同时也非常有趣!

我接下来打算深入研究认证,希望这能为使用GraphQL和Apollo Server提供另一个提升技能的机会。敬请期待!

祝您拥有一个非常美好的一天!

Source:
https://dzone.com/articles/leveling-up-my-graphql-skills-real-time-subscriptions