使用 ASP.NET Core 2.0、SignalR、Angular 5 和 Chart.js 构建的简单仪表板






4.92/5 (28投票s)
该示例提供设备状态的 Web 访问,并使用 ASP.NET Core 2.0、SignalR、Angular 5 和 Chart.js 显示动态指标图表。
引言
本文介绍了一个简单的仪表板,可动态显示信息源的状态及其近期指标。信息源(设备)使用 HTTP 协议定期将其测量值发送到专门用于特定设备类型的相应 Web 服务。然后,数据会传播到仪表板 Web 服务。Web 应用程序从仪表板服务获取数据并在浏览器中显示它们。
我早在两年前就在 这篇文章 中向 CodeProject 介绍了该系统的第一个版本。整体结构保持不变,但实现方式却大相径庭。主要区别在于:
- 用于 Web API 服务的 ASP.NET Core
- 用于前端的 Angular 5(已弃用对 AngularJS,也称为 Angular 1 的支持)
- 使用 Chart.js 动态展示近期测量历史
- 总体 UI 得到增强
之前的 Windows .NET Framework 实现使用 SignalR 库在服务之间以及与 Web 客户端之间进行通信。当前版本也采用 SignalR for .NET Core,目前该版本仍处于预发布阶段。SignalR 允许开发人员相对轻松地提供具有远程方法调用的双向通信。
系统结构
让我们简要描述一下系统组件和数据流。整体系统结构如图 2 所示。
后端
模拟器
在实际应用中,信息源(设备)会为系统生成指标。在我们的示例中,此类设备的角色由控制台应用程序 DevicesSimulator
扮演。自上一个版本以来,它几乎没有变化。模拟器使用 Windows .NET Framework(而非 .NET Core)DLL 编写,因此可能无法在非 Windows 操作系统上运行。模拟器将生成的数据发送到 DeviceXApp
Web API 服务。启动时,模拟器首先创建并发送所有可用设备的描述符,然后开始生成它们的测量值。设备描述符包含整数 deviceId
、typeId
和字符串 deviceName
、description
、deviceSpecificData
。
Web API 服务
与模拟器不同,后端服务 DeviceXApp
和 DashboardApp2
均使用 .NET Core 兼容库开发。模拟器生成的数据通过 HTTP GET 传输到 DeviceXApp
,并调用 DeviceController
类型的 RegisterDevice()
方法来接收数据。DeviceController
类型中没有太多代码,它依赖于 BaseDeviceControllerLib
库中的基类 BaseDeviceController
。同样,调用 BaseDeviceController.SendMeasurement()
方法来定期接收模拟器发送到 DeviceXApp
的指标。
DeviceXApp
服务处理来自相应类型设备(在本例中为模拟器)的原始数据。在此设计中,每种设备类型都有一个专门的数据处理服务,类似于 DeviceXApp
。BaseDeviceControllerLib
类 BaseDeviceController
是未来可能开发的所有此类服务的控制器的基类。在实际应用中,它可能使用特定于设备和/或设备类型的(例如校准表)数据,但在我们的简单示例中,数据被转发到 DashboardApp2
Web 服务,保持原样。此外,DeviceXApp
根据 DeviceXApp
从其配置中获取的设备类型信息来确定设备状态。因此,上述用于设备注册和数据接收的方法使用 SignalR 客户端 DLL HubClientHandlerLib
将此数据传输到 DashboardApp2
。其类型 HubClientHandlerLib.HubClient
由 BaseDeviceController
类用于此目的。
Web 服务 DashboardApp2
作为 DeviceXApp
数据提供程序和用 Angular 5 编写的前端 Web 应用程序的服务器。因此,仪表板服务提供两个不同的 SignalR 中枢:DevicesHub
用于 .NET Core 数据提供程序 DeviceXApp
,WebHub
用于 UI Web 应用程序。仪表板也有自己的控制器,但系统未使用它,仅用于在浏览器中指示服务正在运行。
SignalR 为客户端调用提供不同的中枢类实例。因此,中枢类应该是无状态的。但是 DashboardApp2
必须保留设备描述符、它们的类型和测量历史记录的集合。它还需要提供 DevicesHub
和 WebHub
之间的数据交换。单例类 InterHubHelper
完成此工作。
为了动态更新 Web UI,我们必须提供从 DashboardApp2
到 Web 应用程序的连续数据流。这可以通过轮询 Web 应用程序 DashboardApp2
的 WebHub
或由 WebHub
将数据推送到 Web 应用程序来实现。后一种情况是通过 SignalR 流式传输机制实现的。在与 DashboardApp2
服务器建立连接后,Web 客户端会调用 WebHub
类型的 StartStreaming()
方法。
public IObservable<MeasurementsResult> StartStreaming()
{
return Observable.Create<MeasurementsResult>(async observer =>
{
while (!cts.Token.IsCancellationRequested)
{
await Task.Delay(InterHubHelper.Instance.StreamingIntervalInMs, cts.Token);
observer.OnNext(new MeasurementsResult
{ Measurements = InterHubHelper.Instance.MeasurementsObj });
}
});
}
正如你所见,SignalR 流式传输利用可观察模式将数据连续推送到 Web 客户端。当前的 InterHubHelper.Instance.MeasurementsObj
对象包含自上次推送以来接收到的测量值,并且由单例的 InterHubHelper
的计时器处理程序更新。
timer = new Timer(_ =>
{
lock (locker)
{
if (lastTime > DateTime.MinValue)
{
// Output only measurements received since last update for this connection
measurements = lstMeasurement.Where(m => m.Timestamp > lastTime).ToArray();
// Forget old measurements
lstMeasurement = lstMeasurement.Where(m =>
m.Timestamp > DateTime.Now - TimeSpan.FromSeconds(historyDepthInSec)).ToList();
}
}
lastTime = DateTime.Now;
},
null, StreamingIntervalInMs, StreamingIntervalInMs);
其中
Measurement[] measurements;
public Measurement[] MeasurementsObj
{
get
{
lock (locker)
return measurements;
}
}
因此,SignalR 流式传输提供浏览器 UI 的连续异步更新。正如下面将在“前端”章节中讨论的那样,这不仅允许用户看到设备状态的更新,还能在近乎实时的情况下观察相应指标的运行图表。
Web API 服务 DeviceXApp
和 DashboardApp2
都在其 Startup
类型中实现了“跨域资源共享 (CORS)”,允许任何客户端使用其服务。
前端
前端 Web 应用程序使用 Angular 5 开发。它使用外部包来支持通信(SignalR)和图表(Chart.js)。这两个包都应该使用 npm 包管理器进行安装。
由于安装了 angular/cli,对于在本地计算机上运行的开发项目,我们使用 NG Live Development Server 作为 Web 服务器。默认情况下,它监听端口 4200。DashboardApp2
的 CORS 支持允许 Web 应用程序与 DashboardApp2
通信。
除了标准的预定义 AppComponent
外,Web 应用程序还包含几个组件,即 HomeComponent
(仅作为占位符),用于导航的 DevicesComponent
、ChartComponent
和 NavComponent
,以及一个提供程序(服务)CommunicationService
。后者放置在共享模块中,以便将来可能在 Web 应用程序的不同模块中使用。该服务是可注入的。但是,我们需要一个地方来存放 SignalR 客户端以及其他状态对象,如组、设备、设备类型和近期测量值的集合。这些数据应该对所有组件可用。这通过引入额外的“单例”类 Communication
(在文件 communication.service.ts 中)来实现。由于 Communication
类是不可导出的,因此只能通过其持有者类 CommunicationService
访问它。因此,任何注入了 CommunicationService
的组件都可以使用 CommunicationService.comm
“前缀”访问 Communication
的公共变量和方法。
应用程序的主要功能之一是为每个设备提供动态移动图表。Chart.js 的 Chart
类型提供了对图表的支持。图表由 DevicesComponent
一创建设备注册就创建。为确保设备图表的正常运行,我们必须保留它们的实例。但这在传统的 Angular 组件导航中是不可能的。通常,在激活下一个组件时,前一个组件会被销毁。为了保留图表,我们必须保持 DevicesComponent
的实例存活,并仅在组件切换时将其设为不可见。在我们的代码中,实现了一种简单(甚至是天真)的解决方案。引导组件 AppComponent
在其 template
属性中列出了所有其他组件的选择器(在本例中是 NavComponent
、HomeComponent
和 DevicesComponent
),从而强制创建它们。NavComponent
永久可见,而其他组件则在其 HTML 中根据 Boolean
变量 isShown
提供可见性切换机制。在所有组件中可用的单例类 Communication
提供了 selectComponentSubject: Subject<any>
事件。HomeComponent
和 DevicesComponent
订阅它,而 NavComponent
在按下菜单按钮时激活它。选定组件的名称会发送给所有订阅的组件,它们会相应地更新其可见性状态。相同的机制确保在应用程序启动时出现 HomeComponent
,而与 URL 中的相对路径无关。这种行为通常通过 Angular 的 Guards 来实现。因此,对于我们的 Web 应用程序,“没有 Guard”并不意味着“不设防”。
正如前面在讨论后端时所说,Web 应用程序作为 SignalR 客户端与 DashboardApp2
通信。我们的 Communication
类有一个变量 hubConnection: HubConnection
和主题(事件)streamSubject: Subject<any>
来支持流式传输,以及另一个主题 receiveMeasurementSubject: Subject<any>
来通知组件它们应该从 Communication
的 mapMeasurements: Map<number, Array<DeviceMeasurement>>
中获取新测量值。下面给出了激活该类的 Communication
类的 start()
方法。
async start() {
this.hubConnection = new HubConnection(this.BASE_URL);
// Handler on streaming push measurements
this.hubConnection.on('registerDeviceWithWeb', result => this.onRegisterDevice(result.device));
try {
// Start connection
await this.hubConnection.start();
this.isConnected = true;
await this.getInitInfo();
// Handler to receive measurements
await this.streamSubject.asObservable().subscribe(result => this.onReceiveMeasurements(result));
await this.hubConnection.stream("StartStreaming").subscribe(this.streamSubject);
}
catch (err) {
console.error(err + ': Unable to establish connection with ' + `${this.BASE_URL}`);
}
}
Web 客户端 DashboardApp2
服务的 URL 在 Communication
类中被硬编码为 BASE_URL
。
使用代码示例
本文的代码示例包含两个单独的下载:后端和前端。我仅在 Window 10 操作系统本地计算机上测试了该示例,因此我将仅描述在此环境下的操作步骤。请创建目录 Dashboard,然后将后端文件 Backend_src.zip 解压到该目录中。名为 Backend 的服务器端解决方案将出现在 Dashboard 目录中。现在,在 Backend 目录中,您可以在 VS 2017 中打开 DashboardBackend.sln 并构建它。
NuGet 将安装所需的包,特别是 Microsoft.AspNetCore.SignalR (1.0.0-preview1-final)。然后应启动以下项目:DashboardApp2
、DeviceXApp
和 DevicesSimulator
。请在运行前检查相应的 URL。DevicesSimulator
中硬编码了 DeviceXApp
的 URL,并且 DeviceXApp
在其 appsettings.json 配置文件中包含 DashboardApp2
设备中枢的 URL。如上所述,DevicesSimulator
的给定实现使用的是 .NET Framework DLL 开发的,因此可能无法在非 Windows 环境中运行。
注意 1. 有时,VS 2017 可能无法将后端项目作为“多个启动项目”启动,尤其是在最初几次尝试时,会显示一些奇怪的消息,如“无法启动先前选择的调试器。请选择另一个。”但经过几次初始失败后,VS 就能正常工作。
要运行前端,需要进行一些初步安装。首先,应该安装 Node.js 和 npm 包管理器。以下资源解释了如何执行此操作:[1],[2]。然后应使用以下命令全局安装 angular/cli:
npm install -g @angular/cli
可以使用任何集成开发环境 (IDE)。我的选择是 VS Code。这里 [3]、[4] 可以找到如何使用 VS Code 进行 Angular 开发的方法。完成这些准备后,请打开控制台,转到您的根 Dashboard 目录,然后从中创建一个新的 Angular 项目:
ng new Frontend
(有关详细信息,请参阅 [5])。
注意 2. 所有外部库的安装命令都应从新创建的 Frontend 目录执行。
请安装项目中使用的 Angular Material UI 库:
npm install --save @angular/material @angular/cdk
(有关详细信息,请参阅 [6])。
Angular Font Awesome 包通过以下命令安装:
npm install --save font-awesome angular-font-awesome
(有关详细信息,请参阅 [7])。
然后必须安装 SignalR,运行以下命令:
npm install @aspnet/signalr
(详细信息可在此处找到:[8])。
最后一个要安装的外部库是 Chart.js:
npm install chart.js --save
(另请参阅 [9])。
使用的主要软件包版本如下:
Node.js | 8.10.0 |
npm | 5.6.0 |
Angular | 5.2.9 |
Angular CLI | 1.7.4 |
Typescript | 2.5.3 |
在安装完所有必需的库后,您必须将 Frontend_src.zip 文件下载到 Frontend/src 目录,并用 Frontend_src.zip 文件的内容替换其中的 app 目录以及 styles.css 和 fivicon.ico 文件。现在您可以在 VS Code 中打开 Dashboard/Frontend 目录。
要运行示例,请先启动后端。在 VS 2017 中,运行以下项目:DashboardApp2
、DeviceXApp
和 DevicesSimulator
(请参阅上面的注意 1)。您将看到两个显示消息“This is DeviceXApp
”和“This is DashboardApp2
”的浏览器窗口。DevicesSimulator
在控制台中运行,显示消息“Press any key to start generate measurements...”。在按下某个键后,它将开始生成测量值。要运行前端,请在菜单中打开 View -> Integrated Terminal,然后在其中运行以下命令:
ng serve -o
此命令构建 Web 应用程序,并在 https://:4200 上启动浏览器,连接到监听此 IP 地址的 NG Live Development Server。Web 应用程序实例显示“This is Home page”消息。通过按下 Devices 菜单按钮,您将导航到(或者更准确地说,使可见)相应的页面,该页面看起来与图 1 类似,具有变化的设备状态和移动的历史指标图表。经过一段时间后,为了测试目的,您可以关闭 DevicesSimulator
(仍保持两个服务运行),并观察到所有设备的状态变为“NoData
”。然后重新启动 DevicesSimulator
,按任意键开始生成测量值,并观察到数据流已恢复。
前端项目的调试可以按以下方式进行。激活 ng serve -o
命令后,任何保存的代码更新都会导致 Web 应用程序重新启动。可以通过在代码中编写 debugger;
语句来设置断点。然后打开浏览器中的 Inspect 窗口(对于 Google Chrome,可以通过 右键菜单 -> Inspect 或快捷键 Ctrl-Shift-I 实现)。一旦达到该行,浏览器将停止并在 Source 选项卡中显示断点。从那里,您可以在浏览器中继续进行调试。如果 Inspect 窗口未打开,debugger;
语句将不起作用。要使用 VS Code 进行调试,应安装附加组件。特别是对于 Google Chrome 浏览器,可以使用 Debugger for Chrome(有关详细信息,请参阅 [10])。
注意 3. 命令 ng serve -o
也可以在不使用 VS Code 或其他 IDE 的情况下,从 Dashboard/Frontend 目录中的命令控制台中激活,效果相同。如果选择此方法,则开发人员可以使用任何可用编辑器和 debugger;
语句技术在浏览器中进行调试,从而完全在 IDE 之外处理 Web 应用程序。
结论
这项工作提供了一种结合 ASP.NET Core、SignalR、Angular 和 Chart.js 的概念,用于实现响应式近乎实时的数据采集和呈现。它绝不打算成为一个功能齐全的产品。因此,Web 应用程序的一些重要部分,如身份验证、登录等,以及部署,都没有在此讨论——有大量的出版物对此进行了全面描述。为了进一步开发,实现使用 TLS/SSL 协议进行通信以确保数据传输安全将是一个不错的选择。应检查是否有能力为大量设备提供图表。在非 Windows 操作系统上测试 DeviceXApp
和 DashboardApp2
项目将很有用。