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

使用 .NET 6 构建 Angular 13 应用程序(全球市场)- 第二部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (16投票s)

2022 年 5 月 16 日

CPOL

8分钟阅读

viewsIcon

22080

downloadIcon

399

使用 Angular 13 和 .NET 6 构建一个简单的金融应用程序

引言

第一部分中,我们使用 Visual Studio 2022 创建了一个 Angular 13 前端和 .NET 6 后端。本文将继续构建这个金融应用程序,在后端调用 Yahoo Finance API,并使用 Angular Material 增强前端。

Yahoo Finance API

Yahoo Finance API 是一系列库/API/方法,用于获取各种金融市场和产品的历史和实时数据,如Yahoo Finance所示。

其部分产品包括加密货币、普通货币、股票和债券的市场数据、基本面和期权数据,以及市场分析和新闻。使用 Yahoo Finance API 的一个好理由是它是完全免费的。而且,它简单易用。

在使用此 API 之前,您需要登录 Yahoo Finance 以获取自己的 API 密钥。

Global Market API

回到 GlobalMarketAPI 项目。删除默认创建的 WeatherForecastController.csWeatherForecast.cs 文件。我们将使用 Yahoo Finance API 构建 Finance Controller。

查看Yahoo Finance API 规范,我们使用 http Get /v6/finance/quote 来获取股票、加密货币、期货等的实时报价数据。

Finance Controller

右键单击“Controllers”文件夹以添加新控制器。

选择“API Controller – Empty”以添加一个空的控制器。

将其命名为 FinanceController.cs,将创建一个空的 API 控制器,其中包含以下代码:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace GlobalMarketAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class FinanceController : ControllerBase
    {
    }
}

Quote Response Model

根据 Yahoo Finance API 规范,我们获取 Get Quote API 的响应 JSON。

根据规范,我们首先添加 Quote 类,该类包含 ShortNameFullExchangeNameQuoteTypeRegularMarketPriceRegularMarketDayHighRegularMarketDayLowBidAsk 属性。

namespace GlobalMarketAPI.Models
{
    public class Quote
    {
        public string? ShortName { get; set; }

        public string? FullExchangeName { get; set; }

        public string? QuoteType { get; set; }

        public decimal RegularMarketPrice { get; set; }

        public decimal RegularMarketDayHigh { get; set; }

        public decimal RegularMarketDayLow { get; set; }

        public decimal Bid { get; set; }

        public decimal Ask { get; set; }
    }
}

然后添加 QuoteResult 类,该类包含 Quote 列表和 error string

namespace GlobalMarketAPI.Models
{
    public class QuoteResult
    {
        public List<Quote>? Result { get; set; }

        public string? Error { get; set; }
    }
}

最后是 YahooQuoteResponse 类,它包含 QuoteResult

namespace GlobalMarketAPI.Models
{
    public class YahooQuoteResponse
    {
        public QuoteResult? QuoteResponse { get; set; }
    }
}

Yahoo Finance Settings

调用 Yahoo Finance API 时,我们需要 API URL 和 API 密钥。我们将这些信息放在应用程序设置中。在 appsettings.json 中添加 Yahoo Finance 设置。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "YahooFinanceSettings": {
    /* Please replace with your own api key */
    "APIKey": "****************",
    "BaseURL": "https://yfapi.net"
}

Base URL 是一个常量字符串,https://yfapi.net。请将 API 密钥替换为您自己的 API 密钥。

创建 YahooFinanceSettings 类。

namespace GlobalMarketAPI.Settings
{
    public class YahooFinanceSettings
    {
        public string? APIKey { get; set; }
        public string? BaseURL { get; set; }
    }
}

Program.cs 中注入 Yahoo Finance 设置。

using GlobalMarketAPI.Settings;

var builder = WebApplication.CreateBuilder(args);
ConfigurationManager configuration = builder.Configuration;

builder.Services.AddTransient(p =>
{
    YahooFinanceSettings settings = configuration.GetSection
                (nameof(YahooFinanceSettings)).Get<YahooFinanceSettings>();

    return settings;
});

Finance Service

现在创建 Yahoo Finance Service。报价 URL 是 /v6/finance/quote

我们可以这样编写“get quote”函数:

var url = $"v6/finance/quote?symbols={symbol}";
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("X-API-KEY", new[] { settings.APIKey });
httpClient.BaseAddress = new Uri(settings.BaseURL ?? "");
var data = await httpClient.GetStringAsync(url);

添加 Finance Service 接口

using GlobalMarketAPI.Models;

namespace GlobalMarketAPI.Services
{
    public interface IFinanceService
    {
        Task<Quote> GetQuote(string symbol);
    }
}

添加 Finance Service 类

using GlobalMarketAPI.Models;
using Newtonsoft.Json;
using GlobalMarketAPI.Settings;

namespace GlobalMarketAPI.Services
{
    public class FinanceService : IFinanceService
    {
        private readonly HttpClient _httpClient;
        const string QuoteURL = "v6/finance/quote";
        
        public FinanceService(YahooFinanceSettings settings)
        {
            _httpClient = new HttpClient();
            _httpClient.DefaultRequestHeaders.Add
                 ("X-API-KEY", new[] { settings.APIKey });
            _httpClient.BaseAddress = new Uri(settings.BaseURL ?? "");
        }

        public async Task<Quote> GetQuote(string symbol)
        {
            var url = QuoteURL + $"?symbols={symbol}";
            try
            {
                var data = await _httpClient.GetStringAsync(url);
                var result = JsonConvert.DeserializeObject<YahooQuoteResponse>(data);
                return result?.QuoteResponse?.Result?.FirstOrDefault() ?? new Quote();
            }
            catch (Exception)
            {
                throw;
            }
        }
    }
}

Program.cs 中注入 FinanceService 实例。

builder.Services.AddTransient<IFinanceService, FinanceService>();

Http Get Quote Endpoint

现在我们可以在 Finance controller 中编写 HttpGet Quote 端点。

[HttpGet]
[Route("quote")]
public async Task<Quote> GetQuote([FromQuery] string symbol)
{
    return await _financeService.GetQuote(symbol);
}

每个控制器方法的路由模板是前缀加上 Route 属性中指定的 string。对于 GetQuote 方法,route 模板包含字符串“quote”,并且 URL 包含 string Symbol 作为查询字符串参数。

现在点击“Start”按钮运行解决方案。GlobalMarketGlobalMarketAPI 都将启动。

您应该能看到 GlobalMarketAPI 控制台窗口。

然后我们可以检查 Swagger UI 来验证 API 端点。

https://:5219/swagger

Global Market Frontend

我们想做什么?

交易小部件在我们的前端运行得非常好。现在我们增加一点趣味性。我们想要一个符号下拉列表。当我们在下拉列表中选择一个符号时,调用后端 API 获取该符号的实时报价,同时重新加载该符号的交易小部件。

集成后端 API 项目

更改反向代理配置以连接后端 Finance Controller。

更新 GlobalMarket/projects/trading 目录下的 proxy.config.js 文件。

const PROXY_CONFIG = [
  {
    context: [
      "/api",
    ],
    target: "https://:7219",
    secure: false
  }
]

module.exports = PROXY_CONFIG;

虽然我们可以在 context 中添加多个 URL,但如果您不想每次都更改此代理配置文件,可以添加父 URL,例如“/api”。

现在我们开始使用 Angular Material 来样式化前端。

Angular Material

Angular 团队构建和维护了常见的 UI 组件以及帮助您构建自己的自定义组件的工具。Angular Material 是 Angular 应用程序的 Material Design UI 组件。

右键单击 Visual Studio 2022 Solution Explorer 中的 GlobalMarket 项目,然后单击“Open in Terminal”。

运行以下命令来安装 Angular Material。

ng add @angular/material
  • 选择一个预建主题名称,或者选择“custom”来自定义主题。
  • 设置全局 Angular Material 排版样式。
  • 为 Angular Material 设置浏览器动画。

当然,我们还需要 Bootstrap,这是最受欢迎的 CSS 库。

npm install bootstrap --save

安装后,我们在 GlobalMarkt/projects/trading/src/styles.css 中导入 Bootstrap 和 Angular Material 的 CSS 样式。

/* You can add global styles to this file, and also import other style files */

@import "~bootstrap/dist/css/bootstrap.css";

@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';

Angular Material 提供了高质量的组件。您可以在此链接处查看。在使用这些组件之前,需要导入组件模块。以下是我们前端使用的组件:

  • mat-card

    <mat-card> 是一个内容容器,用于在一个单一主题的上下文中展示文本、照片和操作。

  • mat-form-field

    <mat-form-field> 是一个组件,用于包装几个 Angular Material 组件,并应用通用的文本字段样式,例如下划线、浮动标签和提示消息。

  • mat-select

    <mat-select> 是一个表单控件,用于从一组选项中选择一个值,类似于原生的 <select> 元素。

  • mat-chip-list

    <mat-chip-list> 以单独的、可访问键盘的芯片形式显示值的列表。

  • mat-progress-bar

    <mat-progress-bar> 是一个水平进度条,用于指示进度和活动。

  • mat-divider

    <mat-divider> 组件允许对带有各种方向选项的线条分隔符进行 Material 样式化。

app.module.ts 中导入这些模块。

import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { TradingviewWidgetModule } from 'tradingview-widget';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ClipboardModule } from '@angular/cdk/clipboard';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips'
import { MatDividerModule } from '@angular/material/divider'
import { MatFormFieldModule } from '@angular/material/form-field'
import { MatSelectModule } from '@angular/material/select'
import { MatProgressBarModule } from '@angular/material/progress-bar'

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    TradingviewWidgetModule,
    BrowserAnimationsModule,
    FormsModule,
    ReactiveFormsModule,
    ClipboardModule,
    DragDropModule,
    MatCardModule,
    MatChipsModule,
    MatDividerModule,
    MatFormFieldModule,
    MatSelectModule,
    MatProgressBarModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

我们需要使用 mat-form-fieldmat-select,因此需要导入 FormsModuleReactiveFormsModule

现在使用这些组件来修改 app.component.html

整个页面分为两个 Card。上面的 Card 是一个 Symbol Card,另一个 Card 显示交易小部件。

<mat-card>
  <mat-card-subtitle style="margin-top:-10px">Symbol Quote</mat-card-subtitle>
</mat-card>
<mat-divider></mat-divider>
<mat-card>
  <mat-card-subtitle style="margin-top:-10px">Live Chart</mat-card-subtitle>
  <tradingview-widget [widgetConfig]="widgetConfig"></tradingview-widget>
</mat-card>

显示 Symbol 下拉列表

打开 AppComponent 类文件并定义 symbols 数组。

symbols = ['MSFT',
    'AAPL',
    'AMZN',
    'TSLA',
    'WTC',
    'BTCUSD',
    'ETHUSD',
    'CLN2022'
  ];

打开 AppComponent 模板文件以添加 <mat-select>。使用 ngFor 显示列表,并将选定的符号值绑定到 widgetConfig.symbol

<mat-form-field appearance="fill">

  <mat-select id="symbol" class="symbol" [ngModel]="widgetConfig.symbol" 
                          (ngModelChange)="onSymbolChange($event)" required>

<mat-option *ngFor="let symbol of symbols" [value]="symbol">{{symbol}}</mat-option>

  </mat-select>

</mat-form-field>

当选定的符号更改时,我们希望重新加载交易视图小部件。打开 AppComponent 类文件以添加 OnSymbolChange 事件。

  onSymbolChange(event: any) {
    this.widgetConfig = {
      symbol: event,
      widgetType: 'widget',
      allow_symbol_change: true,
      height: 560,
      width: 980,
      hideideas: true,
      hide_legend: false,
      hide_side_toolbar: true,
      hide_top_toolbar: false,
      theme: Themes.LIGHT,
    };
    this.ngOnInit();
  }

在 Angular 中,调用 ngOnInit 会刷新组件。

显示获取报价结果

添加一个新的 app.model 类(app.model.ts),然后定义 Quote 接口和 DataItem 接口。

export interface Quote {
  shortName: string;
  fullExchangeName: string;
  quoteType: string;
  regularMarketPrice: number;
  regularMarketDayHigh: number;
  regularMarketDayLow: number;
  bid: number;
  ask: number;
}
export interface DataItem {
  name: string;
  value: number;
}

打开 AppComponent 类文件以调用后端获取报价 API。

this.http.get<Quote>(`/finance/quote?symbol=
      ${this.widgetConfig.symbol}`).subscribe(result => {
      this.quote = result;
      this.data = [
        { name: "Price", value: this.quote.regularMarketPrice },
        { name: "Day High", value: this.quote.regularMarketDayHigh },
        { name: "Day Low", value: this.quote.regularMarketDayLow },
        { name: "Ask", value: this.quote.ask },
        { name: "Bid", value: this.quote.bid },
      ];
    }, error => console.error(error));

打开 AppComponent 模板文件以添加 <mat-chip-list> 来显示获取报价结果。

<mat-chip-list class="symbol-price-chip"
           cdkDropList
           cdkDropListOrientation="horizontal"
           (cdkDropListDropped)="drop($event)">
    <mat-chip class="symbol-price-box"
            cdkDrag
            *ngFor="let item of data">
    {{item.name}}: {{item.value}}
    </mat-chip>
</mat-chip-list>

<mat-chip> 元素可以通过拖放改变顺序。在 AppComponent 类中添加 drop 事件。

  drop(event: CdkDragDrop<DataItem[]>) {
    moveItemInArray(this.data, event.previousIndex, event.currentIndex);
  }

显示进度条

打开 AppComponent 模板文件以添加 <mat-progress-bar>

<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isProgressing">
</mat-progress-bar>

打开 AppComponent 类文件以设置 isProgressing。在调用 API 之前,将 isProgessing 设置为 true,然后在订阅回调中将 isProgressing 设置为 false

this.isProgressing = true;
this.http.get<Quote>(`/finance/quote?symbol=
     ${this.widgetConfig.symbol}`).subscribe(result => {
  ...
  this.isProgressing = false;
}, error => console.error(error));

Global Market 全新外观

现在,经过后端和前端的更改后,我们的应用程序焕然一新。按 F5 或点击顶部菜单栏上的“Start”按钮。如我们之前配置的,GlobalMarketAPI (后端) 和 GlobalMarket (前端) 都将启动。

您可以拖放“Price”、“Day High”、“Day Low”、“Ask”、“Bid”来随意更改它们的顺序。

在符号下拉列表中将符号更改为 AAPL – Apple。

然后您可以看到 Apple 公司的交易图表和报价信息。

正如您所知,加密货币最近暴跌。所以我真的很想看看比特币的交易图表。在下拉列表中将符号更改为“BTCUSD”(比特币)。

哎呀,真是场悲剧。

但是等等,为什么没有报价信息?让我们来调试一下。

调试 Angular 前端和 .NET 后端

在前端,我们在调用 get quote API 和订阅回调处设置了断点。

在后端,我们在 finance controller 的 GetQuote 函数处设置了断点。

现在将符号更改为 BTCUSD

第一个断点被触发。您可以看到我们正确地传递了符号“BTCUSD”。

F5 或点击菜单栏上的“Continue”让它继续执行。

finance controller 的 GetQuote 处的断点被触发。您还可以看到符号“BTCISD”已正确传递。

让我们按 F11 进入。

Yahoo Finance API URL 也正确。我们在这里设置一个断点来捕获异常。

F10 单步执行。

没有发生异常,但结果显示为空。这意味着 Yahoo Finance 找不到关于此符号的任何信息。为什么?这是因为 Yahoo Finance 的符号并不都与 Trading View 的符号相同。例如,Yahoo Finance 中“BTCUSD”的符号是“BTC-USD”,而“ETHUSD”的符号是“ETH-USD”等等。

为了解决这个问题,我们需要在 Trading View 符号和 Yahoo Finance 符号之间添加映射。

我们可以将此映射添加到 GlobalMarketAPI 的应用程序设置中。

"YahooFinanceSettings": {
    /* Please replace with your own api key */
    "APIKey": "******",
    "BaseURL": "https://yfapi.net",
    "SymbolMapInfo": {
      "WTC": "WTC.AX",
      "BTCUSD": "BTC-USD",
      "ETHUSD": "ETH-USD",
      "CLN2022": "CLN22.NYM"
    }
}
  • WTC – 广度科技全球
  • BTCUSD – 比特币
  • ETHUSD – 以太坊
  • CLN2022 – 2022 年 7 月原油

现在在 GlobalMarketAPIYahooFinaceSettings.cs 中添加字典。它将在程序启动时加载。

namespace GlobalMarketAPI.Settings
{
    public class YahooFinanceSettings
    {
        public string? APIKey { get; set; }
        public string? BaseURL { get; set; }
        public Dictionary<string, string>? SymbolMapInfo
        {
            get;
            set;
        }
    }
}

打开 FinanceService 类,添加 symbol map 字典。

private readonly Dictionary<string, string> _symbolMap = new Dictionary<string, string>();

在构造函数中设置此字典。

if (settings.SymbolMapInfo != null && settings.SymbolMapInfo.Count > 0)
{
    _symbolMap = settings.SymbolMapInfo.ToDictionary(x => x.Key, x => x.Value);
}

如果符号有映射,则替换该符号。

symbol = _symbolMap.ContainsKey(symbol) ? _symbolMap[symbol] : symbol;

现在再次调试。

Symbol 是映射后的符号,“BTC-USD”。

Yahoo Finance API 返回结果。

按“F5”继续。Angular http client 回调函数处的断点被触发。

按“F5”继续。最终,获取到比特币报价信息。

如何使用源代码

下载并解压源代码,使用 Visual Studio 2022 打开 GlobalMaket.sln

然后,在 **Solution Explorer** 中右键单击 GlobalMarket 项目,选择“Open in Terminal”。

需要先构建 tradingview-widget 库。

npm install

ng build tradingview-widget

结论

接续第一部分,在本文中,我们集成了 Angular 前端和 ASP.NET Core Web API,并使用 Angular Material 对前端进行了样式化。最后,我们向您展示了在 Visual Studio 2022 中轻松地一起调试前端和后端的方法。您可以从github获取所有源代码。祝您编码愉快!

历史

  • 2022 年 5 月 16 日:初始版本
© . All rights reserved.