本文章是由機器翻譯。

技術最前線

利用 SignalR 建置進度列

Dino Esposito

下載代碼示例


在此專欄過去的兩篇文章中,我曾討論了如何構建 ASP.NET 解決方案,以解決從 Web 應用程式的用戶端監視遠端任務進度的持續問題。儘管 AJAX 取得成功並得到採用,但仍缺乏不借助 Silverlight 或 Flash 即可在 Web 應用程式中顯示上下文相關進度欄的被廣泛接受的綜合解決方案。

老實說,可實現此目的的方法不多。如果您願意,可以構建自己的解決方案,但基本模式不會與我在以前的專欄中所提供的模式有所不同 — 專門面向 ASP.NET MVC。這個月,我重新討論同一主題,但我將討論如何使用以下仍在不斷完善的新庫構建進度欄: SignalR.

SignalR 是 ASP.NET 團隊正在開發的一個 Microsoft .NET Framework 庫和 jQuery 外掛程式,可能包括在以後版本的 ASP.NET 平臺中。它提供了一些前景極為光明的功能,而這些功能正是 .NET Framework 當前不曾具有的,並且是越來越多的開發者所需要的。

SignalR 概覽

SignalR 是一個集成的用戶端與伺服器庫,基於流覽器的用戶端和基於 ASP.NET 的伺服器元件可以借助它來進行雙向多步對話。換句話說,該對話可不受限制地進行單個無狀態請求/回應資料交換;它將繼續,直到明確關閉。對話通過永久連接進行,允許用戶端向伺服器發送多個消息,並允許伺服器做出相應答覆,值得注意的是,還允許伺服器向用戶端發送非同步消息。

我將使用一個聊天應用程式來闡明 SignalR 的主要功能,這不足為奇。用戶端通過向伺服器發送消息來開始對話;伺服器(ASP.NET 終結點)答覆並持續偵聽新請求。

SignalR 專用於 Web 方案,並需要用戶端上有 jQuery 1.6(或更高版本)並且伺服器上有 ASP.NET。您可以通過 NuGet 或通過從位於 github.com/SignalR/SignalR 上的 GitHub 存儲庫直接下載二進位檔案來安裝 SignalR。圖 1 顯示了具有所有 SignalR 包的 NuGet 頁面。您至少需要下載 SignalR,它依賴框架的伺服器端部件 SignalR.Server 和框架的 Web 用戶端部件 SignalR.Js。您在圖 1 中看到的其他包用於其他特定目的,例如提供 .NET 用戶端、Ninject 依賴關係解析程式和基於 HTML5 Web 通訊端的備用傳輸機制。

圖 1 NuGet 平臺上提供的 SignalR 包

深入探討聊天示例

在我嘗試構建進度欄解決方案之前,通過查看隨可下載的原始程式碼 (archive.msdn.microsoft.com/mag201203CuttingEdge) 一起分發的聊天示例和 Web 上當前提供的(一些)相關文章中引用的其他資訊來熟悉庫將很有用。但請注意,SignalR 不是已發佈專案。

在 ASP.NET MVC 專案的上下文中,您首先引用一些指令檔,如下所示:

    <script src="@Url.Content("~/Scripts/jquery-1.6.4.min.js")"
      type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.signalr.min.js")"
      type="text/javascript"></script>
    <script src="@Url.Content("~/signalr/hubs")"
      type="text/javascript"></script>

請注意,SignalR 中沒有任何內容是特定于 ASP.NET MVC 的,此庫同樣可用於 Web 表單應用程式。

值得關注的是前兩個連結引用特定指令檔。 而第三個連結仍引用一些 JavaScript 內容,但該內容是動態生成的,並依賴宿主 ASP.NET 應用程式中具有的一些其他代碼。 還請注意,如果您打算支援低於 Internet Explorer 8 版的版本,則需要 JSON2 庫。

頁面載入完成時,您便完成了用戶端設置並打開連接。 圖 2 顯示您需要的代碼。 您可能希望從 jQuery 的就緒事件中調用此代碼。 代碼將腳本處理常式綁定到 HTML 元素(非介入式 JavaScript)並準備 SignalR 對話。

圖 2 為聊天示例建立 SignalR 庫

    <script type="text/javascript">
      $(document).ready(function () {    // Add handler to Send button
        $("#sendButton").click(function () {
          chat.send($('#msg').val());
        });
        // Create a proxy for the server endpoint
        var chat = $.connection.chat; 
        // Add a client-side callback to process any data
        // received from the server
        chat.addMessage = function (message) {
          $('#messages').append('<li>' + message + '</li>');
        };
        // Start the conversation
        $.connection.hub.start();
      });
    </script>

值得注意的是,$.connection 物件在 SignalR 指令檔中定義。 而 chat 物件在某種意義上是動態物件,其代碼是動態生成的並通過 Hub 腳本引用注入用戶端頁面。 chat 物件從根本上說是伺服器端物件的 JavaScript 代理。 此時應很清楚,圖 2 中的用戶端代碼如果沒有強大的伺服器端對應部分,則沒有多大意義(和作用)。

ASP.NET 專案應包括對 SignalR 程式集及其依賴項(例如 Microsoft.Web.Infrastructure)的引用。 伺服器端代碼包括與您創建的 JavaScript 物件匹配的託管類。 若要引用圖 2 中的代碼,您需要擁有與用戶端 Chat 物件具有相同的介面的伺服器端物件。 此伺服器類將從 SignalR 程式集中定義的 Hub 類繼承。 下麵是類簽名:

using System;
using SignalR.Hubs;
namespace SignalrProgressDemo.Progress
{
  public class Chat : Hub
  {
    public void Send(String message)
    {
      Clients.addMessage(message);
    }
  }
}

類中的每個公共方法必須與用戶端上的 JavaScript 方法匹配。 或者,至少在 JavaScript 物件上調用的任何方法都必須在伺服器類上具有匹配方法。 因此,正如前面所定義的那樣,您在圖 2 的腳本代碼中看到調用的 Send 方法以調用 Chat 物件的 Send 方法結束。 為了將資料發送回用戶端,伺服器代碼將使用 Hub 類上的 Clients 屬性。 Clients 成員是動態類型,這使它能夠引用動態確定的物件。 具體來說,Clients 屬性包含對在以下用戶端物件的介面之後構建的伺服器端物件的引用:Chat 物件。 因為圖 2 中的 Chat 物件具有 addMessage 方法,所以要求伺服器端 Chat 物件也公開相同的 addMessage 方法。

準備進度欄演示

現在,讓我們使用 SignalR 構建通知系統,該系統將在可能較長的任務期間向用戶端報告伺服器上執行的任何進度。 作為第一步,讓我們創建封裝任務的伺服器端類。 您為此類分配的名稱(任意選擇)將影響稍後編寫的用戶端代碼。 這僅僅意味著您有另一個原因來仔細選擇類名。 更重要的是,此類將繼承自 SignalR 提供的名為 Hub 的類。 下麵是簽名:

public class BookingHub : Hub
{
  ...
}

BookingHub 類將具有一些公共方法,大部分是接受對預期目的有意義的輸入參數的任意序列的 void 方法。 Hub 類上的每個公共方法表示用戶端要調用的可能終結點。 例如,讓我們添加一個方法來預訂機票:

public void BookFlight(String from, String to)
{
  ...
}

此方法需要包含執行給定操作(即預訂機票)的所有邏輯。 代碼還將包含各個階段的調用,它們將以某種方式將任何進度報告回用戶端。 假設 BookFlight 方法的框架如下所示:

public void BookFlight(String from, String to)
{
  // Book first leg  var ref1 = BookFlight(from, to);  // Book return flight
  var ref2 = BookFlight(to, from);
  // Handle payment
  PayFlight(ref1, ref2);
}

通過結合使用這些主操作,您希望通知使用者執行的進度。 Hub 基類提供了一個名為 Clients、定義為動態類型的屬性。 換句話說,您將調用此物件上的方法來回調用戶端。 但此方法的構成和形式由用戶端本身決定。 然後,讓我們轉到用戶端。

正如所提到的,在用戶端頁面中,您將具有一些在頁面載入時運行的腳本代碼。 如果您使用 jQuery,則 $(document).ready 事件是運行此代碼的好位置。 首先,獲取伺服器物件的代理:

var bookingHub = $.connection.bookingHub;
// Some config work
...
// Open the connection
$.connection.hub.start();

您在 $.connection SignalR 本機組件上引用的物件的名稱只是動態創建的代理,它將 BookingHub 物件的公共介面公開給用戶端。 此代理是通過頁面的 <script> 部分中具有的 signalr/hubs 連結生成的。 用於名稱的命名約定是 camelCase,這意味著 C# 中的 BookingHub 類將成為 JavaScript 中的 bookingHub 物件。 在此物件上,您將找到與伺服器物件的公共介面匹配的方法。 另外,對於方法,命名約定使用相同的名稱,而非 camelCased。 您可以將按一下處理常式添加到 HTML 按鈕中並通過 AJAX 啟動伺服器操作,如下所示:

bookingHub.bookFlight("fco", "jfk");

現在,您可以定義用戶端方法來處理任何回應。 例如,您可以在用戶端代理上定義接收消息並通過 HTML 範圍標記顯示消息的 displayMessage 方法:

bookingHub.displayMessage = function (message) {
  $("#msg").html(message);
};

請注意,您負責 display­Message 方法的簽名。 您決定傳遞什麼內容和要求任何輸入是什麼類型。

在結束之前,只剩下最後一個問題:誰調用 displayMessage 以及誰最終負責傳遞資料? 答案是伺服器端 Hub 代碼。 您通過 Clients 物件從 Hub 物件中調用 displayMessage(以及您希望採用的任何其他回檔方法)。 圖 3 顯示了 Hub 類的最終版本。

圖 3 Hub 類的最終版本

public void BookFlight(String from, String to)
{
  // Book first leg
  Clients.displayMessage(    String.Format("Booking flight: {0}-{1} ...", from, to));
  Thread.Sleep(2000);
  // Book return
  Clients.displayMessage(    String.Format("Booking flight: {0}-{1} ...", to, from));
  Thread.Sleep(3000);
  // Book return
  Clients.displayMessage(    String.Format("Booking flight: {0}-{1} ...", to, from));
  Thread.Sleep(2000);
  // Some return value
  Clients.displayMessage("Flight booked successfully.");
}

請注意,在本例中,displayMessage 名稱必須與您在 JavaScript 代碼中使用的大小寫完全匹配。 如果您將它錯誤鍵入為 DisplayMessage 之類,則不會獲得任何異常,但也不會執行任何代碼。

Hub 代碼作為 Task 物件實現,因此它獲取自己的執行緒來運行,並且不會影響 ASP.NET 執行緒池。

如果伺服器任務導致計畫了非同步工作,則它會從標準工作執行緒池中選取執行緒。 優點是,SignalR 請求處理常式是非同步的,這意味著當它們處於等候狀態來等待新消息時,它們根本不使用執行緒。 當收到消息並且有要執行的工作時,將使用 ASP.NET 工作執行緒。

使用 HTML 的真正進度欄

在過去的專欄中以及在本專欄中,我頻繁使用術語進度欄,而沒有顯示作為用戶端 UI 示例的經典規杆。 具有規杆只是非常好的視覺效果,並且在非同步基礎結構中不需要更複雜的代碼。 不過,圖 4 顯示了動態構建給定百分比值的規杆的 JavaScript 代碼。 您可以通過合適的 CSS 類來更改 HTML 元素的外觀。

圖 4 創建基於 HTML 的規杆

var GaugeBar = GaugeBar || {};
GaugeBar.generate = function (percentage) {
  if (typeof (percentage) != "number")
    return;
  if (percentage > 100 || percentage < 0)
    return;
  var colspan = 1;
  var markup = "<table class='gauge-bar-table'><tr>" +
    "<td style='width:" + percentage.toString() +
    "%' class='gauge-bar-completed'></td>";
  if (percentage < 100) {
    markup += "<td class='gauge-bar-tobedone' style='width:" +
      (100 - percentage).toString() +
      "%'></td>";
    colspan++;
  }
  markup += "</tr><tr class='gauge-bar-statusline'><td colspan='" +
    colspan.toString() +
    "'>" +
    percentage.toString() +
    "% completed</td></tr></table>";
  return markup;
}

您從按鈕按一下處理常式調用此方法:

bookingHub.updateGaugeBar = function (perc) {
  $("#bar").html(GaugeBar.generate(perc));
};

因此,updateGaugeBar 方法是從使用不同的用戶端回檔來報告進度的另一個 Hub 方法調用的。您可以將以前使用的 displayMessage 替換為 Hub 方法中的 updateGaugeBar。

不僅是 Web 用戶端

我主要將 SignalR 作為需要 Web 前端的 API 進行介紹。雖然這可能是您可能希望在其中使用它的最引人注目的方案,但 SignalR 絕不局限于僅支援 Web 用戶端。您可以為 .NET 桌面應用程式下載用戶端,而且將很快發佈支援 Windows Phone 用戶端的另一個用戶端。

本專欄僅從 SignalR 提供了最簡單有效的程式設計方法的意義上對其進行了簡單介紹。在以後的專欄中,我將研究它執行的一些不可思議的深層功能以及包如何沿線路移動。請繼續關注。

Dino Esposito  是《Programming Microsoft ASP.NET MVC3》(Microsoft Press,2011 年)的作者,同時也是《Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2008).他定居於義大利,經常在世界各地的業內活動中發表演講。請關注他的 Twitter:twitter.com/despos

衷心感謝以下技術專家對本文的審閱: Damian Edwards