ASP.NET SignalR 是幾年前推出的工具,可供 ASP.NET 開發人員使用,以嚮應用程式添加即時功能。只要基於 ASP.NET 的應用程式必須接收來自伺服器(從監視系統到遊戲)的頻繁非同步更新,就屬於典型的庫用例。這些年來,我還使用它來重新整理 CQRS 體繫結構方案中的 UI,以及在 socialware 應用程式中實現與 Facebook 類似的通知系統。從更具技術性的角度來看,SignalR 是抽象層,產生依據為一部分可以在完全相容的用戶端和伺服器之間建立即時串連的傳輸機制。用戶端通常為 網頁瀏覽器,伺服器通常為 Web 服務器,但兩者都不僅限於此。
ASP.NET SignalR 屬於 ASP.NET Core 2.1。雖然庫的總體編程模型與經典 ASP.NET 的編程模型類似,但庫本身實際上已經完全重寫。儘管如此,只要開發人員適應各方面的變化,應該就可以快速熟練掌握新方案。本文將介紹如何在規範 Web 應用程式中使用新庫來監視可能會很漫長的遠程任務。 設定環境
可能需要以下多個 NuGet 包,才能使用庫:Microsoft.AspNetCore.SignalR 和 Microsoft.AspNetCore.SignalR.Client。前一個包提供核心功能;後一個包是 .NET 用戶端,且只有在產生 .NET 用戶端應用程式時才需要。此樣本將通過 網頁用戶端來使用庫,因此改為需要 SignalR NPM 包。本文稍後將詳細介紹這一點。請注意,在基於 MVC 應用程式模型的 Web 應用程式的上下文中使用 SignalR 並不是一項強制性要求。可以直接通過 ASP.NET Core 控制台應用程式使用 SignalR 庫服務,還可以在控制台應用程式中託管 SignalR 的伺服器部分。
應用程式的啟動類需要包含一些特定代碼,這一點不足為奇。具體而言,將把 SignalR 服務添加到系統服務集合中,並將它配置為可供實際使用。圖 1 展示了使用 SignalR 的啟動類的典型狀態。
圖 1:SignalR ASP.NET Core 應用程式的啟動類
public class Startup{ public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddSignalR(); } public void Configure(IApplicationBuilder app) { app.UseStaticFiles(); app.UseDeveloperExceptionPage(); // SignalR app.UseSignalR(routes => { routes.MapHub<UpdaterHub>("/updaterDemo"); routes.MapHub<ProgressHub>("/progressDemo"); }); app.UseMvcWithDefaultRoute(); }}
SignalR 服務配置包括一個或多個伺服器路由的定義,這些路由綁定到伺服器端環境中的一個或多個終結點。MapHub<T> 方法將請求 URL 中的指定名稱連結到 Hub 類的執行個體中。Hub 類既是實現 SignalR 協議的核心所在,也是處理用戶端調用的位置所在。為伺服器端打算接受和處理的每組邏輯相關調用建立 Hub。SignalR 對話由雙方之間交換的訊息組成,一方對另一方調用方法的結果可能是沒有響應,可能是收到一個或多個響應,也可能是僅收到錯誤通知。任何 ASP.NET Core SignalR 伺服器實現都會公開一個或多個 Hub。在圖 1 中,有兩個 Hub 類(UpdaterHub 和 ProgressHub)綁定到唯一字串,這些字串在內部用於產生實際調用的 URL 目標。 Hub 類
SignalR 應用程式中的 Hub 類是普通的簡單類,繼承自 Hub 基類。此基類僅用於免去開發人員一次又一次編寫相同樣本代碼的麻煩。基類只提供某基礎結構,而不提供預定義行為。具體而言,它定義了圖 2 中的成員。
圖 2:Hub 基類的成員
成員 |
說明 |
用戶端 |
公開當前由 Hub 託管的用戶端列表的屬性。 |
上下文 |
公開當前調用方內容相關的屬性,包括串連 ID 和使用者聲明(若有)等資訊。 |
組 |
公開各用戶端子集的屬性,這些用戶端可能已經以編程方式定義為完整用戶端列表中的組。組通常建立用於向選定受眾廣播特定訊息。 |
OnConnectedAsync |
每當有新用戶端串連到 Hub 時調用的虛擬方法。 |
OnDisconnectedAsync |
每當有新用戶端與 Hub 中斷連線時調用的虛擬方法。 |
最簡單的 Hub 類如下所示:
public class ProgressHub : Hub{}
有趣的是,如果在 ASP.NET Core MVC 應用程式上下文中從控制器方法內使用它,就會直接採用 Hub 形式。幾乎所有的 ASP.NET Core SignalR 樣本(包括聊天樣本)往往都會在用戶端和 Hub 之間進行直接綁定和雙向繫結,無需控制器提供任何形式調解。在這種情況下,Hub 採用的形式將會更有形一點:
public class SampleChat : Hub{ // Invoked from outside the hub public void Say(string message) { // Invoke method on listening client(s) return Clients.All.InvokeAsync("Said", message); }}
與數十篇部落格文章中換湯不換藥的規範 SignalR 聊天樣本不同,本文中的樣本其實並沒有在用戶端和伺服器之間建立雙向對話。雖然串連是從用戶端建立的,但在此之後,用戶端就不會發送其他任何請求。相反,伺服器會監視任務進度,並在適當時將資料推送回用戶端。也就是說,只有當用例要求用戶端直接調用公用方法時,Hub 類才必須像上面的代碼一樣使用這些方法。如果有點複雜難懂,下面的樣本足以闡明這一點。 監視遠程任務
它的具體情形是這樣的:ASP.NET Core 應用程式為使用者提供了某 HTML 介面,以方便使用者觸發可能會很漫長的遠程任務(如建立報告)。因此,作為開發人員,需要顯示進度列,以持續反饋進度(見圖 3)。
圖 3:使用 SignalR 監視遠程任務的進度
可以猜到,在此樣本中,用戶端和伺服器都在同一個 ASP.NET Core 項目的上下文中設定 SignalR 即時會話。在此開發階段中,MVC 項目功能齊全,它使用圖 1 中的啟動代碼進行了擴充。接下來,將設定用戶端架構。需要在與 SignalR 終結點互動的所有 Razor(或純 HTML)視圖中完成此設定。
若要在 網頁瀏覽器中與 SignalR 終結點進行通訊,首先要添加對 SignalR JavaScript 用戶端庫的引用:
<script src="~/scripts/signalr.min.js"></script>
可以通過多種方式擷取此 JavaScript 檔案。最值得推薦的方法是,使用幾乎所有開發電腦上都有的 Node.js 包管理器 (NPM) 工具(特別是在 Visual Studio 2017 版本推出後)。通過 NPM,尋找並安裝名為 @aspnet/signalr 的 ASP.NET Core SignalR 用戶端。它會將許多 JavaScript 檔案複製到磁碟,但其中只有一個檔案才是大多數情況唯一需要的。不管怎樣,這就是簡單地連結 JavaScript 檔案,還可以通過其他許多方式來擷取此檔案,包括從舊版 ASP.NET Core SignalR 項目中複製它。然而,NPM 是團隊提供的唯一受支援的指令碼擷取方式。另請注意,ASP.NET Core SignalR 不再依賴 jQuery。
在用戶端應用程式中,還需要另一段更具體的 JavaScript 代碼。特別是,需要如下代碼:
var progressConnection = new signalR.HubConnection("/progressDemo");progressConnection.start();
與 SignalR Hub 建立的串連與指定路徑匹配。更確切地說,以參數形式傳遞到 HubConnection 的名稱,應該是映射到啟動類中路由的名稱之一。在內部,HubConnection 對象準備了串聯當前伺服器 URL 和給定名稱產生的 URL 字串。只有當此 URL 與已配置的路由之一匹配時,才會處理它。另請注意,如果用戶端和伺服器不是相同的 Web 應用程式,那麼必須向 HubConnection 傳遞託管 SignalR Hub 的 ASP.NET Core 應用程式的完整 URL,外加 Hub名稱。
然後,必須通過 start 方法開啟 JavaScript Hub 連線物件。可以使用 JavaScript 承諾(特別是 then 方法)或 TypeScript 中的 async/await 執行後續操作(如初始化某使用者介面)。SignalR 串連由字串 ID 唯一標識。
請務必注意,如果傳輸串連或伺服器失敗,ASP.NET Core SignalR 就不再支援自動重新串連。在舊版中,如果發生伺服器故障,用戶端會嘗試根據計划算法重建立立串連。如果成功,它會使用相同的 ID 重新開啟串連。在 SignalR Core 中,如果串連中斷,用戶端只能通過 start 方法再次啟動串連,這就會產生串連 ID 不同的其他串連執行個體。 用戶端回調 API
需要的另一段基本 JavaScript 代碼是,Hub 回調的 JavaScript,用於重新整理介面並在用戶端上反映伺服器上的進度。雖然這段代碼在 ASP.NET Core SignalR 中的編寫方式與舊版中的略有不同,但意向是完全相同的。此樣本中有以下三個方法能夠從伺服器回調:initProgressBar、updateProgressBar 和 clearProgressBar。不用說,可以使用任意名稱和簽名。以下是實現樣本:
progressConnection.on("initProgressBar", () => { setProgress(0); $("#notification").show();});progressConnection.on("updateProgressBar", (perc) => { setProgress(perc);});progressConnection.on("clearProgressBar", () => { setProgress(100); $("#notification").hide();});
例如,如果從伺服器回調 initProgressBar 方法,協助程式 setProgress JavaScript 函數就會配置並顯示進度列(此示範使用的是啟動進度列組件)。請注意,代碼中使用了 jQuery 庫,但僅用於更新 UI。如前所述,用戶端 SignalR Core 庫不再是 jQuery 外掛程式。也就是說,如果 UI 是基於 Angular 等,可能根本無需使用 jQuery。 伺服器端事件
缺少的解決方案部分是,決定何時調用用戶端函數。主要有以下兩種方案。一種是在用戶端通過 Web API 或控制器終結點調用伺服器操作時調用。另一種是在用戶端直接調用 Hub 時調用。最後,只需決定在哪裡為回調用戶端的任務編寫代碼。
在規範聊天樣本中,這一切都發生在 Hub 中:執行所需的全部邏輯,並將訊息指派給相應串連。監視遠程任務是另一回事。它假設後台正在運行某商務程序,以通過某種方式通知進度。從技術角度來講,可能會在 Hub 中編碼此流程,並從中建立與用戶端 UI 之間的對話。也可以讓控制器 (API) 觸發此流程,Hub 僅用於將事件傳遞給用戶端。比此樣本更為實際的做法是,在低於控制器層級的層中編碼此流程。
總而言之,可以定義 Hub 類,並隨時可將它用於決定何時以及是否調用用戶端函數。有趣的地方在於,需要什麼才能將 Hub 執行個體注入控制器或其他業務類。此示範在控制器中注入 Hub,但也會對其他更深層級的類執行完全相同的操作。樣本 TaskController 是通過 JavaScript 直接從用戶端調用,以觸發進度列將顯示其進度的遠程任務:
public class TaskController : Controller{ private readonly IHubContext<ProgressHub> _progressHubContext; public TaskController(IHubContext<ProgressHub> progressHubContext) { _progressHubContext = progressHubContext; } public IActionResult Lengthy() { // Perform the task and call back }}
通過 IHubContext<THub> 介面在控制器或其他任何類中注入 Hub。IHubContext 介面封裝 Hub 執行個體,但無法直接存取它。從中可以將訊息指派回 UI,但無法訪問串連 ID(舉個例子)。假設遠程任務是在 Lengthy 方法中執行,並需要在其中更新用戶端進度列:
progressHubContext .Clients .Client(connId) .InvokeAsync("updateProgressBar", 45);
串連 ID 可以從 Hub 類中進行檢索,但無法像此樣本一樣從通用 Hub 上下文中進行檢索。因此,最簡單的方法是,讓用戶端方法在啟動遠程任務時就傳遞連接字串:
public void Lengthy([Bind(Prefix="id")] string connId) { … }
最後,控制器類接收 SignalR 串連 ID,注入有 Hub 上下文,並使用通過非類型化通用 API 呼叫的上下文方法(InvokeAsync 方法)執行操作。這樣一來,Hub 類就無需包含任何方法。如果覺得這很奇怪,請參閱 bit.ly/2DWd8SV 中的代碼。 總結
本文介紹了如何在 Web 應用程式上下文中使用 ASP.NET Core SignalR 監視遠程任務。Hub 幾乎是空的,因為所有通知邏輯都被內建到控制器中,並使用通過 DI 注入的 Hub 上下文進行編排。這隻是 ASP.NET Core SignalR 漫長旅程的起點。接下來,我將深入研究基礎結構,並探索類型化 Hub。
Dino Esposito 在他 25 年的職業生涯中撰寫了超過 20 本書籍和 1,000 篇文章。Esposito 不僅是舞台劇《事業中斷》的作者,還是 BaxEnergy 的數字策略分析師,正忙於編寫有助於建設環保世界的軟體。可以在 Twitter 上關注他 (@despos)。
原文:https://msdn.microsoft.com/zh-cn/magazine/mt846469
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com