一个简单的仪表板:ASP.NET Web API 服务配合 AngularJS 和 Angular 2 客户端






4.93/5 (41投票s)
ASP.NET Web API、SignalR 和 AngularJS / Angular 2 协同工作
引言
本文介绍了一个协作式 RESTful Web 服务与 AngularJS(下称ng1)和 Angular 2(ng2)客户端的示例。一个简单的基于 Web 的仪表板服务显示了由硬件设备控制的各种进程的状态。这些设备已连接到网络,并能够使用 HTTP 与专用于特定设备类型的 Web 服务进行通信。每个此类服务(在代码示例中为DeviceXApp)会按一定时间间隔从设备接收数据,处理数据以确定被控进程的状态,并将该状态信息转发给仪表板 Web 应用程序(DashboardApp)。DeviceXApp 和 DashboardApp 之间的通信通过 SignalR 执行。两个 Web 应用程序均使用 ASP.NET Web API 开发。DashboardApp 的客户端侧有两种形式:ng1 和 ng2。两个客户端都具有相同的功能和用户界面,并使用 SignalR 与相同的服务器软件进行全双工通信。
流程
系统的整体结构如下图 1 所示。
DevicesSimulator 控制台应用程序模拟硬件设备的通信活动,向 DeviceXApp 发送周期性的 HTTP 请求。每个设备在首次发送消息时会向 DeviceXApp 注册(假定 DeviceXApp 在设备注册之前已启动)。DeviceXApp 将设备注册数据传递给 DashboardApp,后者根据设备的 ID 和为每个类别预配置的设备 ID 范围,将设备归入适当的类别。成功注册后,设备会继续向 DeviceXApp 发送其测量数据。后者根据接收到的数据确定设备进程的状态(Ok、Warning 或 Error),并将其转发给 DashboardApp 以便呈现给用户。如果在预配置的时间间隔内设备未能发送其数据,DeviceXApp 将报告设备处于 NoData 状态。ng1 和 ng2 应用程序呈现 DashboardApp 的客户端用户界面,根据设备类别显示设备信息和状态。用户界面如下图 2 所示(请不要对它有过高的要求,因为它只是一个小而不太真实的示例)。
每个类别表都可以展开或折叠。在示例的信息流中,我们希望尽可能避免移动不必要的数据,以减少网络负载并提高性能。因此,每个设备在启动时都会向 DeviceXApp 服务注册自己,然后只向其发送测量数据。
工作场景
设备(在本例中由 DevicesSimulator 控制台应用程序模拟)通过 HTTP GET 请求向 DeviceXApp 服务注册自己,该请求调用 DeviceXApp 的 DeviceController.RegisterDevice()
远程方法。通过此注册,设备向服务提供其 ID、类型、名称、描述和一些附加数据(例如校准表等)。为了启用安全连接,使用了 HTTPS 协议。RequireHttpsAttribute : AuthorizationFilterAttribute
特性位于一个单独的程序集 RequiresHttps.dll 中,因为该特性在所有 Web 服务中都使用。RegisterDevice()
方法反过来通过远程调用 SignalR ConnectivityHub
的 RegisterDeviceWithDashboard()
方法来注册设备到 DashboardApp Web 服务。每次注册后,设备会继续通过远程调用 DeviceController
类型的 DeviceData()
方法向 DeviceXApp 服务发送其更新数据。该方法根据接收到的数据获取设备状态,并远程异步调用 SignalR ConnectivityHub
的 DashboardApp 服务方法 SetDeviceStatus()
。在服务 DashboardApp 中,CommunicationHub
的方法将设备数据和状态放入单例 DeviceDataProvider
类的适当集合中。DashboardApp 通过 ng1 和 ng2 应用程序与最终用户通信,利用全双工 SignalR 通过 DevicesHub
提供设备信息和状态。在初始化时,ng1 和 ng2 应用程序调用 DevicesHub
的 Init()
方法,以获取所有当前活动设备的信息。
HubClientHandler.dll 程序集包含 HubClient
类,该类为 SignalR hub 客户端提供代理。某些环境会在不活动一段时间后自动停止服务。为避免这种情况,提供了一种保持活动机制。KeepAlive.dll 程序集包含 KeepAliveController
,它会向自身发送周期性的 HTTP GET 请求。保持活动机制从 Application_BeginRequest()
方法激活。默认情况下禁用,通过将 KeepAliveTimeoutInSec 配置参数设置为正值来启用。
Angular
这些相当小的 ng 应用程序是为了提供 SignalR 通信示例,并感受 ng1 和 ng2 方法之间的差异而开发的。如上所述,我们的 ng1 和 ng2 应用程序支持与 DashboardApp 服务的双工 SignalR 通信。它们的视觉部分相当简单(在图 2 中为 ng2 呈现;对于 ng1,它相同,只是将 (ng2) 替换为 (ng1))。它包含一个带有可展开/折叠行集合的表。每个这样的集合代表一个设备类别,而每行代表一个设备。为每个类别和设备都显示了其名称、描述和最后状态。类别状态反映了属于该类别的设备的最高状态。表会定期更新,其中包含 DashboardApp 推送的数据。在表上方放置了一个显示服务器时间的 HTML <p>
标签和一个带有显式更新的按钮(它看起来不太吸引人,但该按钮说明了从 ng 应用程序到 DashboardApp 的直接调用)。
AngularJS (ng1)
为了启用 ng1 应用程序,适当的 jQuery 和 Angular JavaScript 文件被放置在 .\DevicesManagement\DashboardApp\Scripts 文件夹下的相应文件夹中。文件 .\Scripts\_Custom\ng1.js 包含 ng1 应用程序代码。该应用程序从 .\DevicesManagement\DashboardApp\ng1.html 文件激活,并包含三个主要方法,即:
app.run() | 初始化全局变量并启动与 DashboardApp 的连接,使用 devicesHub 。 | |
devicesServer = app.factory('devicesHubProxy', ... | 提供 SignalR 通信机制。 | |
devicesServer.controller('DevicesController', ... | 调用 DashboardApp devicesHub 的 getServerTime() 方法,订阅来自 devicesHub 的 all 、allDevices 和 serverTime 推送调用,使用 devicesHub.on('pushMethodName' , ... ,并调用 DashboardApp devicesHub 的 init() 方法(应在订阅推送调用后调用)。 |
控制器还包含在 ng1.html 文件中使用的变量和函数的赋值。
为了确保与 SignalR 的通信,ng1.html 文件包含相关引用。
<!-- Reference the SignalR library. -->
<script src="node_modules/jquery.signalR-2.2.0.min.js"></script>
<!-- Reference the autogenerated SignalR hub script. -->
<script src="/signalr/hubs"></script>
Angular 2 (ng2)
撰写本文时,我无意提供 ng2 教程。为此,我推荐一本我认为非常出色的书 [1]。这项工作旨在说明 ng2 应用程序的 SignalR 通信方面。ng2 平台尚未正式发布。在我们的应用程序中使用了版本 2.0.0.-beta.6。ng2 是根据 [1] 中的建议,通过 Node.js 安装的。特别是使用了 Node Package Management(npm)实用程序。命令
"C:\Program Files\nodejs"\npm init -y
在 DashboardApp 的工作文件夹 .\Devicesmanagement\DashboardApp 下执行,创建 package.json 配置文件的初始版本。然后手动编辑此文件 - 请参阅其在 .\DashboardApp 文件夹中的修改版本。
命令
"C:\Program Files\nodejs"\npm install
创建 node_module 文件夹,其中包含 package.json 文件中指定的依赖项。新创建的 node_module 文件夹包含 JavaScript 和 TypeScript 文件子文件夹,供我们的应用程序引用。然后,在 .\DashboardApp 文件夹中创建了 config.js 文件来配置 ng2 应用程序。在我们的例子中,它指出我们的应用程序代码在 app 文件夹中,而启动代码位于文件 .\app\main.ts 中。这样的包配置允许我们在 index.html 文件中使用以下行:
<script>System.import('app')</script>
该行加载 .\app\main.ts 的内容。文件 index.html 作为 ng2 应用程序的主要 HTML 文件。其 <head>
标签提供了对相应 JavaScript 文件和样式表 CSS 文件的引用。为了简化,引用的 JavaScript 文件被移动到 .\DashboardApp\node_module 文件夹,该文件夹的其余内容已被删除。文件 index.html 包含与 ng1.html 在 ng1 情况下的 SignalR 相关引用相同的引用。
ng2 应用程序位于 .\DashboardApp\app 文件夹及其子文件夹中。ng2 应用程序的文件结构如下所示:
DeviceManagement DashboardApp app components application application.css application.html application.ts services communication.ts main.ts
文件 main.ts 是应用程序的起点。它启动在文件 application.ts 中实现的 ApplicationComponent
。ApplicationComponent
使用负责 SignalR 支持的 CommunicationService
(文件 communication.ts)。ApplicationComponent
的代码如下所示:
import {bootstrap} from 'angular2/platform/browser'; import {Component} from 'angular2/core'; import {NgFor} from 'angular2/common'; import {CommunicationService} from 'app/services/communication.ts'; @Component({ selector: 'main1', providers: [CommunicationService], templateUrl: 'app/components/application/application.html', styleUrls: ['app/components/application/application.css', 'Styles/styles.css'], directives: [NgFor] }) export default class ApplicationComponent { public currentServerTime: string; private comm: CommunicationService; public imagesUrl: Array<string> = ['expand.png', 'collapse.png']; public imageIndex: Array<int> = []; public image: Array<string> = []; public categoryIndicator: Array<int> = []; public categoryIndicatorAsString: Array<string> = []; public allCategoriesInfo: Array<any> = []; public allDevicesInfo: Array<any> = []; public allDevices: Array<any> = []; constructor(private comm: CommunicationService) { if (comm) { this.comm = comm; comm.getServerTime(data => this.currentServerTime = data); comm.subscribeToBrowserFunction('devicesHub', 'all', lst => { this.allCategoriesInfo = lst[0]; if (this.allCategoriesInfo != null) for (var i = 0; i < this.allCategoriesInfo.length; i++) { this.imageIndex.push(0); this.image.push(this.imagesUrl[0]); this.categoryIndicator.push(0); this.categoryIndicatorAsString.push('NoData'); } this.allDevicesInfo = lst[1]; this.allDevicesFunc(lst[2]); }); comm.subscribeToBrowserFunction('devicesHub', 'allDevices', devices => { this.allDevicesFunc(devices); } }); comm.subscribeToBrowserFunction('devicesHub', 'serverTime', timeAsString => { this.currentServerTime = timeAsString; }); comm.init(); } } allDevicesFunc(devices: Array<any>) { this.allDevices = devices; if (this.allDevicesInfo != null && this.allDevices != null && this.allCategoriesInfo != null && this.allDevicesInfo.length == this.allDevices.length && this.allDevices.length > 0) { // Overall category indicator for (var i = 0; i < this.allCategoriesInfo.length; i++) { this.categoryIndicator[i] = 0; for (var j = 0; j < this.allDevices.length; j++) { if (this.allCategoriesInfo[i].Id == this.allDevicesInfo[j].CategoryId && this.allDevices[j].Status >= this.categoryIndicator[i]) { this.categoryIndicator[i] = this.allDevices[j].Status; this.categoryIndicatorAsString[i] = this.allDevices[j].StatusAsString; } } } } } toggleImage(categoryIndex: int) { this.imageIndex[categoryIndex] = (this.imageIndex[categoryIndex] + 1) % 2; this.image[categoryIndex] = this.imagesUrl[this.imageIndex[categoryIndex]]; } getServerTime() { this.comm.getServerTime(data => this.currentServerTime = data); } }
文件 application.ts
文件 application.html 和 application.css 为应用程序提供 HTML 和 CSS 标记。ApplicationComponent
的元数据引用了这些文件,并包含选择器 main1
,该选择器在 index.html 的 <body>
中定义为一个标签。ApplicationComponent
的构造函数调用 DashboardApp 的 DevicesHub
的 getServerTime()
和 init()
方法,并订阅 all
、allDevices
和 serverTime
推送调用,因此其行为类似于 ng1 应用程序中的 DevicesController
(正如您可能知道的,ng2 中不支持 ng1 控制器)。
类 CommunicationService
(文件 communication.ts)负责与 DashboardApp 进行全双工 SignalR 通信。
export class CommunicationService { getServerTime(callback: Function) { // Start hub, call .NET method of this hub and call callback on its result. $.connection.hub.start().then(() => $.connection.devicesHub.server.getServerTime().then(t => callback(t))); } init() { $.connection.hub.start().then(() => $.connection.devicesHub.server.init()); } subscribeToBrowserFunction(hubName: string, funcName: string, callback: Function) { var connection = $.hubConnection(''); connection.createHubProxy(hubName).on(funcName, data => callback(data)); connection.start(); } }
文件 communication.ts
在上面的代码片段中,方法 subscribeToBrowserFunction()
提供了订阅服务器推送调用的基础架构,并将其连接到其处理程序 callback
。方法 getServerTime()
和 init()
执行对 DashboardApp 的 DevicesHub
类型的相应方法的远程调用。
目前,ng2 应用程序及其引用的文件尚未集成到 DashboardApp Visual Studio 项目中,它们仅通过 index.html 文件链接。ng2 应用程序在运行时不会被转译(即从 TypeScript 编译为 JavaScript)。尽管很明显,预转译允许 ng2 应用程序在任何浏览器中运行,包括那些尚不支持 TypeScript 的浏览器,但我们决定避免繁琐的转译操作,希望浏览器对 ng2 的完全支持只是 ng2 最终发布以及短暂时间的问题。因此,假设在浏览器中进行转译。
注意:目前并非所有现代浏览器都支持转译。在 Windows 上,这个简单的 ng2 应用程序可以在 Google Chrome 和 Mozilla Firefox 中正常运行。Microsoft Edge 无法展开类别,而 Internet Explorer 完全无法运行基于 TypeScript 的 ng2 应用程序。以上所有浏览器都能毫无问题地运行 ng1 应用程序。
运行示例
该示例应使用 Visual Studio(VS 2013 或 VS 2015)进行构建。然后应启动多个启动项目:DeviceXApp、DashboardApp 和 DevicesSimulator。为了简单起见,可以使用 IIS Express。等待服务完全启动,然后按 DevicesSimulator 中的任意键开始设备模拟。或者,构建后可以从命令行启动服务。为此,对于 DashboardApp 和 DeviceXApp,以管理员身份启动命令控制台 cmd.exe,将 IIS Express 文件夹(很可能是 C:\Program Files (x86)\IIS Express)设置为当前目录,并为所需站点运行 iisexpress(这些站点应在 IIS Express 配置文件 ..\Documents\IISExpress\config\applicationhost.config 中进行描述)。
C:\...>iisexpress /site:DashboardApp
C:\...>iisexpress /site:DeviceXApp
然后启动 DevicesSimulator,并在服务就绪时按任意键。
要调用客户端应用程序,您应该浏览:
- 对于 ng1: https://:44300/ng1.html ,以及
- 对于 ng2: https://:44300 。
What Next?
可以将许多功能添加到此示例中,使其成为更现实的解决方案,例如登录、使用数据库持久化 DeviceXApp 中的设备状态、发布到云端(我曾简要尝试从 Visual Studio 将 DeviceXApp 和 DashboardApp 发布为 Microsoft Azure Web Apps - 只有 ng1 版本奏效。显然,浏览器转译尚不支持)。还将 ng2 应用程序完全集成到 Visual Studio 解决方案中并提供适当的转译过程也将是一件好事。
在 DeviceXApp 中,处理来自设备的请求非常基础(因为这不是本文的重点):每个请求在接收时进行处理,并执行 DashboardApp 方法的调用。对于真实世界的应用程序,应实现更复杂的机制来处理多个并行设备请求。
结论
本文介绍了简单的 ASP.NET Web API 应用程序,配合 AngularJS 和 Angular 2 客户端。这些应用程序使用 SignalR 进行客户端之间的通信,并使用纯 HTTP 请求接收来自外部世界(一些外部设备或其模拟器)的信息。AngularJS 和 Angular 2 客户端共享同一服务这一事实,使我们能够比较 Angular 的两个版本。
谢谢
我非常感谢 Kosta Trosman 对仪表板布局的建议,Meir Bechor 提供的及时且小小的提示,当然还有 Michael Molotsky 在本文范围内进行的非常有益的讨论。
参考文献
[1] Yakov Fain 和 Anton Moiseev 著《Angular 2 Development with TypeScript》,2016 年出版
注意
本文的代码曾托管在 CodePlex,现在已不可用。现在可以直接下载代码。一些引用可以在 NuGet 中找到。