提升我的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 中的 訂閱 價值主張
  • 使用 WebSocket 實現來消费 GraphQL 訂閱

在使用 GraphQL 中的訂閱代替查詢和變數是當下列條件滿足時的首選方法:

  • 對大物件進行小型、逐步的更改
  • 低延遲、實時更新(如聊天应用程序)

這很重要,因為在 GraphQL 內實現訂閱並不是一件小事。不僅底層服務器需要更新,而且消費應用程序也需要一些重构。

幸運的是,我們在客戶 360 示例中追尋的使用場景非常適合訂閱。此外,我們將使用 WebSocket 方法來利用這些訂閱。

像以前一樣,我將繼續使用 ApolloTor 向前發展。

通過訂閱能力提升信用

首先,我們需要安裝支撐與我的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: {
    }
});

我清理了现有的解藕器(resolvers),並為這個新的使用場景添加了一個新的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

該命令還 automatically 將Heroku所用倉庫作為远程添加:

Shell

 

$ git remote
heroku
origin

在先前的文章中,我提到过Apollo Server在生產環境中會禁用Apollo Explorer。為了讓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技能

我們可以 lever WebSocket支持和Heroku的性能,來創建一個消耗我們創建的訂閱的實現。

在 我的情況下,我創建了一個index.js文件,並使用以下內容。基本上,這created 了一個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屬性多次更改。在我的示例中,我甚至能夠捕捉到 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專業人士:

for the Apollo Server with GraphQL subscriptionsnodejs-graphql-client for the Node.js client that consumes the WebSocket

Here is the translated text:

將您的時間集中在提供可延伸您智慧財產價值的特性和功能性上。對於其他所有事情,利用框架、產品和服務。

— J. Vester

在這個深入探討GraphQL訂閱的過程中,我們成功地使用運行在Heroku上的另一個服務——基於Node.js的應用程序使用WebSocket,從運行在Heroku上的Apollo Server消費更新。通过利用輕量級訂閱,我們避免了針對不變數據發送查詢,而是簡單地訂閱以接收信用餘額更新的發生。

在簡介中,我提到過在之前寫過的主題中尋找額外的價值原則。GraphQL訂閱正是我心中所想的好例子,因為它讓用戶能夠立即接收更新,而無需對源數據發出查詢。這將使Customer 360數據的使用者非常興奮,知道他們可以接收實時更新。

Heroku是另一個符合我使命聲明的例子,它提供了一個平台,使我能夠使用CLI和標準Git命令快速原型解決方案。這不僅給我提供了一個展示訂閱用例的簡便方式,還實現了一個使用WebSocket的消費者。

如果您對這篇文章的源代碼感興趣,請查看我在GitLab上的倉庫:

我敢自信地说,通過這次的努力,我成功地提升了我的GraphQL技能。這趟旅程對我來說是新奇的,也是有挑戰性的——而且还非常好玩!

我计划在接下來的時間里學習驗證(authentication),這應該能給我帶來另一個提升GraphQL和Apollo Server技能的機會。請拭目以待!

祝你有個非常美好的一天!

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