65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 .NET 8 GraphQL 和 React 构建实时股票价格追踪器:Market Pulse

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2024 年 9 月 10 日

CPOL

5分钟阅读

viewsIcon

31125

downloadIcon

724

了解如何使用 .NET 8 构建实时股票价格追踪应用程序 Market Pulse,使用 GraphQL 作为后端,React 和 Apollo Client 作为前端。

引言

在不断发展的软件开发领域,创建提供实时数据和无缝用户体验的应用程序变得越来越重要。在本文中,我们将探讨如何使用 .NET 8(后端使用 GraphQL,前端使用 React)构建一个强大而动态的股票价格追踪应用程序,名为 Market Pulse。通过利用 GraphQL 的强大功能,我们可以实现高效的数据获取和实时更新,而 React 客户端则提供了一个现代且响应迅速的用户界面。无论您是想了解更多关于 GraphQL 的开发者,还是希望在应用程序中集成实时数据,本指南都将引导您使用最新的工具和技术构建一个全栈解决方案。

使用 Visual Studio 2022 创建 MarketPulse 解决方案

必备组件

  • Visual Studio 2022(17.11 或更高版本)。
  • Node.js 20.17 或更高版本

创建 MarketPulse.Server Web API 项目

启动 Visual Studio 2022,选择“ASP.Net Core Web API”项目。

创建项目后,在 **Debug Launch profile** 中将 **Url** 更改为“graphql/ui”。

然后在同一解决方案中添加 **React App (typescript)** 项目,名为 **marketpulse.client**。

解决方案资源管理器应如下图所示。

后端 GraphQL 实现

安装 Hot Chocolate 所需的 NuGet 包

dotnet add package HotChocolate.AspNetCore

dotnet add package HotChocolate.Data

dotnet add package HotChocolate.Subscriptions

或者,您可以在 Visual Studio 的 NuGet 包管理器中添加它们。

使用 Hot Chocolate 配置 GraphQL 服务器

我们需要在 Program.cs 中配置 GraphQL 服务器来处理查询、突变和订阅。
打开 Program.cs 并按如下方式修改它

using MarketPulse.Server.Services;
using MarketPulse.Server.Queries;
using MarketPulse.Server.GraphQL;
using HotChocolate.AspNetCore;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

// Add CORS services
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy
            .AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader();
    });
});

// Register HttpClient for FinnhubService
builder.Services.AddHttpClient<FinnhubService>();

// Register GraphQL services
builder.Services
    .AddGraphQLServer()
    .AddQueryType<Query>()
    .AddSubscriptionType<Subscription>() // Register the Subscription type
    .AddProjections()
    .AddFiltering()
    .AddSorting()
    .AddInMemorySubscriptions();  // Necessary for enabling subscriptions

// Register background service
builder.Services.AddHostedService<StockPriceBackgroundService>();

var app = builder.Build();

// Enable CORS
app.UseCors();

// Enable WebSocket support
app.UseWebSockets();

// Configure the GraphQL endpoints
app.MapGraphQL();
app.MapBananaCakePop("/graphql/ui");

app.Run();

此配置启用了用于实时更新的 WebSockets,设置了 CORS,并使用 **Banana Cake Pop** 映射了用于测试的 GraphQL 端点。

定义 GraphQL Schema 和类型

创建一个名为 **Queries** 的新文件夹,并添加 **Query.cs** 和 **Subscription.cs** 来定义 schema

Query.cs:

using HotChocolate;
using MarketPulse.Server.Models;
using MarketPulse.Server.Services;

public class Query
{
    public async Task<List<StockQuote>> GetStockQuotes([Service] FinnhubService finnhubService)
    {
        return await finnhubService.GetMultipleStockQuotesAsync(new List<string> { "AAPL", "MSFT", "AMZN", "NVDA", "BTC-USD" });
    }
}

Subscription.cs:

using HotChocolate;
using HotChocolate.Subscriptions;
using MarketPulse.Server.Models;
using System.Threading;
using System.Threading.Tasks;

public class Subscription
{
    [Subscribe]
    public async ValueTask<ISourceStream<StockQuote>> OnStockPriceUpdated(
        [Service] ITopicEventReceiver eventReceiver,
        CancellationToken cancellationToken)
    {
        return await eventReceiver.SubscribeAsync<StockQuote>("StockPriceUpdated", cancellationToken);
    }
}

这些文件定义了一个用于获取股票行情的查询和一个用于实时更新的订阅。

创建股票价格后台服务

为了向 **GraphQL subscription** 提供实时数据,我们需要一个后台服务来定期从 **Finnhub API** 获取数据。

创建一个名为 Services 的新文件夹,并添加 **StockPriceBackgroundService.cs**

StockPriceBackgroundService.cs:

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
using MarketPulse.Server.Services;
using HotChocolate.Subscriptions;
using System.Collections.Generic;

public class StockPriceBackgroundService : BackgroundService
{
    private readonly ILogger<StockPriceBackgroundService> _logger;
    private readonly FinnhubService _finnhubService;
    private readonly ITopicEventSender _eventSender;
    private static readonly List<string> Symbols = new() { "AAPL", "MSFT", "AMZN", "NVDA", "BTC-USD" };

    public StockPriceBackgroundService(
        ILogger<StockPriceBackgroundService> logger,
        FinnhubService finnhubService,
        ITopicEventSender eventSender)
    {
        _logger = logger;
        _finnhubService = finnhubService;
        _eventSender = eventSender;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                foreach (var symbol in Symbols)
                {
                    var stockQuote = await _finnhubService.GetStockQuoteAsync(symbol);
                    if (stockQuote != null)
                    {
                        _logger.LogInformation($"Sending update for {symbol}: {stockQuote.CurrentPrice}");
                        await _eventSender.SendAsync("StockPriceUpdated", stockQuote, stoppingToken);
                    }
                }

                await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); // Poll every 10 seconds
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error fetching stock prices.");
            }
        }
    }
}

从 Finnhub API 获取数据

在 Services 文件夹中创建 FinnhubService.cs 来处理 API 请求

FinnhubService.cs:

using System.Net.Http;
using System.Threading.Tasks;
using System.Text.Json;
using System.Collections.Generic;
using MarketPulse.Server.Models;

namespace MarketPulse.Server.Services
{
    public class FinnhubService
    {
        private readonly HttpClient _httpClient;
        private readonly string _apiKey = "YOUR_API_KEY";  // Replace with your Finnhub API Key

        public FinnhubService(HttpClient httpClient)
        {
            _httpClient = httpClient;
            _httpClient.BaseAddress = new Uri("https://finnhub.io/api/v1/");
        }

        public async Task<StockQuote> GetStockQuoteAsync(string symbol)
        {
            var response = await _httpClient.GetAsync($"quote?symbol={symbol}&token={_apiKey}");
            response.EnsureSuccessStatusCode();

            var jsonResponse = await response.Content.ReadAsStringAsync();
            
            // Deserialize JSON using a dictionary for flexible field mapping
            var stockData = JsonSerializer.Deserialize<Dictionary<string, decimal>>(jsonResponse);

            if (stockData == null) return null;

            return new StockQuote
            {
                Symbol = symbol,
                CurrentPrice = stockData.GetValueOrDefault("c"),
                Change = stockData.GetValueOrDefault("d"),
                PercentChange = stockData.GetValueOrDefault("dp"),
                HighPrice = stockData.GetValueOrDefault("h"),
                LowPrice = stockData.GetValueOrDefault("l"),
                OpenPrice = stockData.GetValueOrDefault("o"),
                PreviousClosePrice = stockData.GetValueOrDefault("pc")
            };
        }

        public async Task<List<StockQuote>> GetMultipleStockQuotesAsync(List<string> symbols)
        {
            var stockQuotes = new List<StockQuote>();

            foreach (var symbol in symbols)
            {
                var quote = await GetStockQuoteAsync(symbol);
                if (quote != null)
                {
                    stockQuotes.Add(quote);
                }
            }

            return stockQuotes;
        }
    }
}

Banana Cake Pop

从 Hot Chocolate 12 开始,**Banana Cake Pop** 是内置的中间件,供开发人员测试 **GraphQL**。默认情况下,当您使用 **MapGraphQL()** 映射 GraphQL 端点时,Banana Cake Pop 会自动在 /graphql 端点提供服务。在这里,我们将 **Banana Cake Pop** 映射到 /graphql/ui

从 Banana Cake Pop,您可以看到您为 **GraphQL** 定义的所有 schema,如下所示。

您还可以在 **Banana Cake Pop** 上测试查询和订阅。

结论

通过此设置,**Market Pulse** 后端已准备好提供 **GraphQL** 查询和订阅,从而提供股票价格的实时更新。Hot Chocolate、.NET 8 和 Finnhub API 的集成允许构建一个可扩展且高性能的应用程序,该应用程序可以高效地提供数据。
接下来,我们将构建 **React 客户端** 来消费此 **GraphQL API**,并将数据展示在用户友好的界面中。

React Typescript 客户端

现在我们将构建一个 React 客户端来消费 **Market Pulse** 后端提供的 **GraphQL API**。客户端将使用 **GraphQL** subscriptions 显示实时股票价格,并使用 Chakra UI 提供现代、响应迅速的用户界面。通过利用 **Apollo Client**,我们可以高效地管理数据获取、缓存和实时更新。

设置 React 项目

使用 **Visual Studio Code** 打开 **marketpulse.client**。

安装 **Apollo Client**、**GraphQL**、**Chakra UI** 和 **WebSocket** 支持所需的依赖项

npm install @apollo/client graphql graphql-ws @chakra-ui/react @emotion/react @emotion/styled framer-motion

为 GraphQL 设置 Apollo Client

为了将 React 客户端连接到 GraphQL 后端,我们需要设置 **Apollo Client** 并启用 WebSocket 支持以进行订阅。
在 src 中创建一个 Apollo Client 设置文件
 

src/apolloClient.ts:

import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';

// HTTP link for regular queries and mutations
const httpLink = new HttpLink({
  uri: 'https://:7041/graphql',
});

// WebSocket link for subscriptions
const wsLink = new GraphQLWsLink(
  createClient({
    url: 'ws://:7041/graphql', // WebSocket endpoint
  })
);

// Split link to direct operations to the appropriate link
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink
);

// Create Apollo Client instance
const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
});

export default client;

在主入口文件中将 Apollo Client 与 React 集成

src/main.tsx:

import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import { ChakraProvider } from '@chakra-ui/react';
import App from './App';
import client from './apolloClient';

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <ChakraProvider>
        <App />
      </ChakraProvider>
    </ApolloProvider>
  </React.StrictMode>
);

创建 Stock List 组件

接下来,让我们创建一个组件来显示股票价格列表并订阅后端からの实时更新。

在 src/components 中创建一个 StockList 组件

src/components/StockList.tsx:

import React, { useEffect, useState } from 'react';
import { useSubscription, gql } from '@apollo/client';
import { Box, Flex, Heading, Text, VStack, HStack, Badge } from '@chakra-ui/react';

// Define the GraphQL subscription for real-time updates
const STOCK_PRICE_SUBSCRIPTION = gql`
  subscription OnStockPriceUpdated {
    onStockPriceUpdated {
      symbol
      currentPrice
      highPrice
      lowPrice
      previousClosePrice
      openPrice
    }
  }
`;

interface StockQuote {
  symbol: string;
  currentPrice: number;
  highPrice: number;
  lowPrice: number;
  openPrice: number;
  previousClosePrice: number;
}

const StockList: React.FC = () => {
  const { data: subscriptionData, error, loading } = useSubscription<{ onStockPriceUpdated: StockQuote }>(STOCK_PRICE_SUBSCRIPTION);
  const [stocks, setStocks] = useState<StockQuote[]>([]);

  useEffect(() => {
    if (loading) {
      console.log('Subscription is loading...');
    }

    if (error) {
      console.error('Subscription error:', error.message); // Log any subscription errors
    }

    if (subscriptionData) {
      const updatedStock = subscriptionData.onStockPriceUpdated;
      setStocks((prevStocks) =>
        prevStocks.map((stock) =>
          stock.symbol === updatedStock.symbol ? updatedStock : stock
        )
      );
    }
  }, [subscriptionData, loading, error]);

  return (
    <VStack spacing={6} align="start" p={4}>
      <Heading size="md" mb={4}>
        Stock Prices
      </Heading>
      <Flex wrap="wrap" justify="space-between" width="100%">
        {stocks.map((stock) => (
          <Box
            key={stock.symbol}
            p={4}
            shadow="md"
            borderWidth="1px"
            borderRadius="lg"
            width="30%"
            mb={4}
          >
            <HStack justify="space-between">
              <Text fontWeight="bold">{stock.symbol}</Text>
              <Badge colorScheme={stock.currentPrice >= stock.previousClosePrice ? 'green' : 'red'}>
                {stock.currentPrice >= stock.previousClosePrice ? '+' : ''}
                {((stock.currentPrice - stock.previousClosePrice) / stock.previousClosePrice * 100).toFixed(2)}%
              </Badge>
            </HStack>
            <Text mt={2}>Price: ${stock.currentPrice.toFixed(2)}</Text>
            <Text>High: ${stock.highPrice.toFixed(2)}</Text>
            <Text>Low: ${stock.lowPrice.toFixed(2)}</Text>
          </Box>
        ))}
      </Flex>
    </VStack>
  );
};

export default StockList;

将 Stock List 组件添加到 App 中

将 StockList 组件添加到主 App 组件中,以便在应用程序中显示它

src/App.tsx:

import React from 'react';
import { Container } from '@chakra-ui/react';
import StockList from './components/StockList';

const App: React.FC = () => {
  return (
    <Container maxW="container.xl" p={4}>
      <StockList />
    </Container>
  );
};

export default App;

运行 React 客户端

启动开发服务器

npm run dev

在浏览器中打开 https://:5173,您将看到 Apple、Microsoft、Amazon、NVIDIA 和 Bitcoin 的实时股票价格。

结论

通过遵循这些步骤,您已成功构建了一个 React 客户端,该客户端连接到 GraphQL 后端,获取股票数据,并使用 subscriptions 显示实时更新。此设置演示了 GraphQL 在管理高效数据获取和实时更新方面的强大功能,并结合了 React 和 Apollo Client 在现代 Web 开发中的灵活性。

从 Visual Studio 启动服务器和客户端项目

右键单击解决方案,然后选择“设置启动项目”。将启动项目从“单个启动项目”更改为“多个启动项目”。为每个项目的操作选择“启动”。

确保后端项目排在前端项目之前,以便它先启动。

现在按“F5”或单击顶部菜单中的“开始”按钮来启动解决方案。

 您可以看到服务器和客户端项目都已启动。

摘要

在本文中,我们探讨了如何使用现代 Web 开发工具和框架构建一个强大、实时的股票价格追踪应用程序,名为 **Market Pulse**。我们首先使用 **Hot Chocolate** 使用 **.NET 8** 和 **GraphQL** 实现了一个强大的后端,为查询和订阅实时股票数据提供了高效的 API。后端利用 **Finnhub API** 获取实时市场数据,并使用 GraphQL 订阅将更新推送到连接的客户端。

然后,我们使用 **TypeScript** 和 **Apollo Client** 开发了一个 **React** 客户端来消费 GraphQL API。通过集成 **Chakra UI**,我们构建了一个现代、响应迅速的用户界面,该界面显示了 Apple、Microsoft、Amazon、NVIDIA 和 Bitcoin 等公司的实时股票价格和市场趋势。客户端通过 GraphQL subscriptions 无缝处理实时更新,提供动态的用户体验。

本项目展示了结合使用 **.NET 8**、**GraphQL** 和 **React** 来创建高效且响应迅速的全栈应用程序的强大功能。无论您是想了解更多关于 GraphQL 的知识,提高 React 的前端技能,还是利用 .NET 进行现代 Web 开发,本文介绍的概念和实现都为您提供了一个坚实的基础。

通过遵循本指南,您现在已经掌握了构建自己的实时应用程序的知识,这些应用程序可以利用现代 Web 技术的强大功能来提供出色的用户体验。

 

使用 .NET 8 GraphQL 和 React 构建实时股票价格追踪器:Market Pulse - CodeProject - 代码之家
© . All rights reserved.