2016 年 9 月

第 31 卷,第 9 期

本文章是由機器翻譯。

回應式架構 - 使用 Reactive Extensions 建置非同步 AJAX 網頁

Peter Vogel

在之前的文章,討論如何觀察器模式可用來管理長時間執行工作 (msdn.com/magazine/mt707526)。結束該發行項時,我示範了 Microsoft Reactive Extensions (Rx) 如何提供簡易的機制,可以從長時間執行的處理序在 Windows 應用程式管理的事件順序。

使用 Rx 只監視從長時間執行工作的事件序列,不過,不充分的技術。Rx 的優點是它可以用來以非同步方式與任何其他處理序整合任何事件為基礎的處理序。在本文中,比方說,我將使用 Rx 進行非同步呼叫 Web 服務從按鈕在網頁中按一下 (按一下按鈕,實際上是,一個事件的順序)。若要在用戶端 Web 環境中使用 Rx,我將使用 Rx for JavaScript (RxJS)。

Rx 提供抽象的各種案例及操控使用 fluent LINQ 類似的介面,可讓您撰寫更簡單的建置組塊的應用程式的標準方法。Rx 可讓您同時整合 UI 事件進行後端處理程序時,在此同時,將它們分開 — 您可以使用 Rx 改寫 UI 而不需要進行對應的變更,您的後端 (反之亦然)。

RxJS 也支援您的 HTML 和程式碼中,清楚地分隔有效地讓資料繫結,而不需要特殊的 HTML 標記。RxJS 也是根據現有的用戶端技術 (例如 jQuery)。沒有其他的好處之一,來接收︰ 所有接收實作起來非常類似都傾向 — RxJS 中的程式碼這篇文章,非常類似我在前一篇文章中所撰寫的 Microsoft.NET Framework 程式碼。您可以利用您在任何 Rx 環境中的一個 Rx 環境中挑選的技術。

開始使用 RxJS

RxJS 會分成兩個群組的應用程式的部分,藉以達成其目標。第一個群組的成員都是可預見值︰ 基本上,任何引發的事件。RxJS 提供一組豐富的運算子來建立可預見值 — 可預見值包含任何項目可顯示在引發的事件 (Rx 可以轉換陣列事件來源,例如)。Rx 運算子也可讓您篩選和轉換的輸出事件。可能值回票價視為可觀察事件來源的處理管線。

第二個群組的成員是觀察者接受可預見值的結果,並且提供可觀察的三個通知的處理 — 新的事件 (與它相關聯的資料)、 錯誤或的事件結束的順序。在 RxJS,觀察者可以是具有函式來處理一或多個三個通知或只是函式,一個用於每個通知的集合的物件。

若要將結合這兩個群組在一起,可觀察訂閱一或多個觀察者的管線。

您可以加入您的專案透過 NuGet RxJS (NuGet 程式庫中尋找 RxJS All)。其中一個警告,不過︰ 當我第一次加入專案,設定 TypeScript RxJS 時,NuGet 管理員要求您是否我也希望相關的 TypeScript 定義檔案。按一下 [是] 會新增的檔案,並提供大約 400 個 「 重複定義 」 錯誤。我已經停止接受該選項。

此外,有數 Rx 支援程式庫為 RxJS 提供有用的外掛程式。比方說,RxJS DOM (可透過 NuGet 以 html 橋接器 RxJS 格式) 可提供與用戶端 DOM 中的事件和與 jQuery 的整合。該程式庫是不可或缺的使用 RxJS 建立回應靈敏的 Web 應用程式。

大部分 JavaScript 外掛程式 Rx,像是 RxJS DOM 擱置 Rx 物件 RxJS 程式庫的核心新功能。RxJS DOM 將 Rx 物件,該物件具有多個有用的功能,包括數個 jQuery 像函式來橋接的 DOM 屬性。例如,若要讓 AJAX 呼叫使用 RxJS DOM 擷取 JSON 物件,您會使用此程式碼︰

return Rx.DOM.getJSON("...url...")

若要使用 Rx 和 RxJS DOM,您只需要是這兩個指令碼標籤︰

<script src="~/Scripts/rx.all.js"></script>
<script src="~/Scripts/rx.dom.js"></script>

使用 rx.all.js 和 rx.dom.js 是重量級解決方案,因為它們包含的所有功能,從兩個程式庫。幸運的是,這兩種 Rx 和 RxJS DOM 將數個分割的功能,您需要到您的網頁 (在 GitHub 上的文件將告訴您,任何運算子,該運算子屬於哪一個程式庫) 淡程式庫,因此您可以納入只保留功能的程式庫。

整合 RESTful 服務

JavaScript 應用程式中典型的案例是使用者按一下按鈕來從 Web 服務擷取的結果,藉由在服務的非同步呼叫。建置 RxJS 解決方案的第一個步驟是將按鈕的 click 事件轉換成可觀察,東西 RxJS/DOM/jQuery 整合簡化作業程序。這個程式碼,例如,使用 jQuery 擷取識別碼的 getButton 在表單上之項目的參考,然後從項目按一下 [建立可觀察事件︰

var getCust = $('#getButton').get(0);
var getCustObsvble = Rx.DOM.fromEvent(getCust, "click");

FromEvent 函式可讓您建立可觀察的任何項目上的重大事件。不過,RxJS 包含數個快速鍵更多的 「 熱門 」 事件,包括 click 事件。我可以比方說,也有使用此程式碼來建立可觀察從按鈕的 click 事件︰

var getCustObsvble = Rx.DOM.click(getCust);

我可以建立更豐富的處理管線,可簡化在我的應用程式中處理這個事件。比方說,我可能會想要避免處理案例中,使用者按下兩次中的快速而連續地 (太快,例如,對我來說,停用按鈕,以防止使用者執行)。與其撰寫一大堆逾時的程式碼,我可以處理,將 debounce 函式加入至管線中,指定我只想看到至少兩秒之後,按下滑鼠︰

var getCustObsvble = Rx.DOM.click(getCust).debounce(2000);

我也可以執行一些處理的事件使用,例如 flatMapLatest,這讓我併入管線 (類似 LINQ SelectMany) 的轉換函式。基底的函式 — flatMap — 執行序列中每個事件的轉換。進一步 flatMapLatest 函式,以及處理非同步處理的一般問題︰ flatMapLatest 取消先前非同步處理,如果第二次叫用轉換,而且仍然是非同步的要求擱置中。

使用 flatMapLatest,如果使用者按下按鈕兩次 (和超過兩秒分開的) 然後 flatMapLatest 仍轉換前一個事件中,如果使用者有 flatMapLatest 將會取消先前的事件。這不需要我撰寫的程式碼來處理取消非同步處理。

使用 flatMapLatest 的第一個步驟是建立我的轉換函式。若要這麼做,我只包裝 getJSON 呼叫我前面所說在函式。我的函式會繫結至按下按鈕,因為我不需要從頁面的任何資料 (事實上,此函式幾乎可視為 「 轉換 」 因為它會忽略此事件的輸入)。

以下是使用一些 jQuery 一路從網頁上的項目擷取客戶 Id 對 Web API 服務提出要求的函式︰

function getCustomer()
{
  return Rx.DOM.getJSON("api/customerorders/customerbyid/"
    + $("custId").val());
}

有了 RxJS,就不需要提供回呼,此要求。呼叫的結果都會自動通過可觀察到任何觀察者。

若要將此轉換整合到我處理鏈結,我只要呼叫 flatMapLatest 從我的觀察者,並傳遞至函式的參考︰

var getCustObsvble = Rx.DOM.click(getCust).debounce(2000).flatMapLatest(getCustomer);

現在,我必須訂閱可觀察到的處理函式。我可以將訂閱中的最多三個函式指派︰ 一個函式,來處理通知收到新的事件時,一個用來報告任何錯誤的通知,另一個處理的事件順序的結尾所傳送的通知。我按一下事件處理管線專門設計來產生一個事件的順序,因為我不需要提供 「 序列的結尾 」 函式。

若要指派兩個必要函式產生的程式碼可能如下所示︰

var getCustObsvble.subscribe(
  c   => $("#custName").val(c.FirstName),
  err => $("Message").text("Unable to retrieve Customer: "
    + err.description)
);

如果我以為我可能會發現這組函式,適用於其他地方 (或只想要簡化我訂閱的程式碼),我可以建立的觀察者物件。觀察者物件有相同的函式,我使用之前,只需指派給名為 onNext、 onError 和 onComplete 的屬性。因為我不需要處理 onComplete,我觀察者的物件會看起來像這樣︰

var custObservr = {
  onNext:  c   => $("#custName").val(c.FirstName),
  onError: err => $("#Message").text("Unable to retrieve Customer: "
     + err.description)
};

使用個別的觀察者,我可觀察使用訂閱觀察者的程式碼變得更簡單︰

var getCustObsvble.subscribe(custObservr);

將所有這些都放在一起,頁面擷取按鈕、 附加變成非常複雜的可觀察的按鈕,然後讓該管線 observer 訂閱的管線,只需要準備函式。因為 RxJS 實作 fluent 介面,我可以這麼做只兩行程式碼中︰ 一行 jQuery 程式碼,以擷取按鈕項目和另一行 RxJS 建置的管線和訂閱我觀察者的程式碼。不過,中斷 RxJS 管線建構和訂閱的程式碼在兩行可讓準備函式為讀取的下一個程式設計人員更容易。

我準備好的函式的最終版本會看起來像這樣︰

$(function () {
  var getCust = $('#getButton').get(0);
  var getCustObsvble =
    Rx.DOM.click(getCust).debounce(2000).flatMapLatest(getCustomer);
  getCustObsvble.subscribe(custObservr);
});

此外,整合 click 事件,此程序呼叫 Web 服務和資料顯示會以非同步方式執行。RxJS 摘要讓我離開該工作的瑣碎的詳細資料。

擷取事件順序

藉由抽象化程序,並提供一組豐富的運算子 Rx 進行許多項目看起來很類似。這可讓您進行重大變更您的處理序,而不需要進行重大的結構性變更您的程式碼。

比方說,因為 RxJS DOM 會以一個事件 (按下按鈕) 可觀察更像是產生連續的一連串事件 (例如 mousemove) 的可觀察,我可以對重大變更我的 UI 而不需要做什麼事,我的程式碼。例如,我可能會決定,而不是已按一下按鈕的 Web 服務要求的使用者觸發程序,我會擷取客戶資料,一旦使用者輸入客戶的識別碼。

我需要變更第一件事是要觀察其事件的項目。在此情況下,這表示從網頁上按鈕切換到包含客戶識別碼 (我也要變更保留項目變數的名稱) 頁面上的文字方塊︰

var getCustId = $('#custId').get(0);

我無法變成可觀察的任意數目的這個 textbox 的事件。比方說,我使用模糊事件文字方塊上,如果我會使我呼叫 Web 服務,當使用者結束文字方塊。不過,我想要回應速度。我改為選擇擷取客戶物件,只要使用者在文字方塊輸入 「 足夠 」 的字元。也就是說,切換到 keyup 事件產生的事件順序︰ 一個用於每個按鍵動作。

這項變更我的管線看起來像這樣︰

var getCustObsvble = Rx.DOM.keyup(getCustId)

當使用者輸入以字元為單位,可以得到多次呼叫我的轉換函式。事實上,如果使用者輸入的速度比我可以擷取客戶物件,我 gong 以便讓多個堆疊的要求。我可以指望 flatMapLatest 清除這些要求,但還有更好的解決方案︰ 我的客戶訂單管理系統,客戶 Id 都是四個字元。因此,沒有呼叫我的轉換函式,除了在文字方塊中有四個字元 (和記錄,因為我管線現在正在客戶識別碼,並傳回完整的 Customer 物件,我的轉換函式現在確實有執行 「 轉換 」) 時點。

若要實作該條件,我只需要是將 Rx filter 函數新增至我的管線。Filter 函式的作用很像 LINQ Where 子句︰ 它必須傳遞其他函式 (選取器函式),其中包含測試,並傳回布林值,根據最新的事件相關聯的資料。只有在通過測試的事件將會傳遞至已訂閱的觀察者。Filter 函式會自動要傳序列中最新的事件物件,在我的選取器函式的測試中,我可以使用事件物件的目標屬性來擷取目前文字方塊的值,然後檢查該值的長度。

一項變更我的管線︰ 由於很難看出哪些優點就讓我這個新的 UI 中,我要移除 debounce 函式。同樣的我的 UI 互動的重大變更,修改過的程式碼會建立 [我的可觀察訂閱我觀察者後仍然結構與我先前的版本︰

var getCustObsvble = Rx.DOM.keyup(getCustId)
                       .filter(e => e.target.value.length == 4)
                       .flatMapLatest(getCustomer);
getCustObsvble.subscribe(custObservr);

而當然,我沒有完全 custObservr 函式進行任何變更︰ 它們與我的 UI 變更隔離。

我可以變更一次多個選擇性,這次我的轉換函式。我的轉換函式一律已傳遞至三個參數 — 我已經剛才已忽略這些我的事件來源是一個按鈕時。傳遞至 flatMapLatest 轉換函式的第一個參數是所觀察此事件的事件物件。我可以利用該事件物件,以消除擷取客戶 Id jQuery 程式碼,相反地,將文字方塊的值擷取事件物件。這項變更可讓我更鬆散偶合,因為我的轉換函式不會再繫結至特定的項目] 頁面上的程式碼。

我新增的轉換函式看起來像這樣︰

function getCustomer(e)
{
  return Rx.DOM.getJSON("api/customerorders/customerbyid/"
    + e.target.value);
}

Rx 抽象的優點如下︰ 從單一事件產生的項目變更為順序的-事件-產生的項目只需進行程式碼組成我管線 (並為我提供一個機會增強我的轉換函式)。所有我所做的變更,則最有可能造成我的問題就重新命名保存我輸入的項目之變數。這項變更也強調,像 LINQ 一樣,成功使用 Rx 密碼熟悉可用的運算子。

存取 Web 服務呼叫抽象

Rx 抽象可讓 UI 的變更看起來幾乎完全一樣,良好的抽象概念也應該執行同樣的後端處理。例如,如果頁面被修訂,使我擷取客戶資料,不僅客戶的銷售訂單嗎? 為了達到此有趣,我假設沒有導覽屬性可以讓我擷取銷售訂單的客戶物件的一部分,而且我需要第二次呼叫。

Rx,我首先必須建立客戶 Id 轉換為客戶訂單的集合的第二個轉換函式︰

function getCustomerOrders(e) {
  return Rx.DOM.getJSON("customerorders/ordersbycustomerid/"
    + e.target.value);
}

然後,我需要建立會使用這項轉換 (此程式碼看起來很像建立我的客戶可觀察的程式碼) 的第二個機制︰

var getOrdersObsvble = Rx.DOM.keyup(getCustId)
       .filter(e => e.target.value.length == 4)
       .flatMapLatest(getCustomerOrders);

此時,您可能預期會結合與協調這些可預見值可能需要相當多的工作。但是,因為 Rx 會看起來幾乎完全一樣,您可以結合來自多個可預見值輸出成單一序列可以由單一 (且相當簡單) 的觀察者的所有可預見值。

例如,如果我有多個來源擷取訂單的數個可預見值時,我可以使用 Rx 的 merge 函式順序的所有聯結成單一序列,我無法訂閱單一觀察者。下列程式碼,例如,結合了可預見值擷取目前的訂單 (getCurrentOrdersObsvble) 和張貼的訂單 (getPostedOrdersObsvle);接著,將所產生的順序呼叫 allOrdersObservr 單一觀察者︰

Rx.Observable.merge(getCurrentOrdersObsvble, getPostedOrdersObsvble)
             .subscribe(allOrdersObservr);

在我的案例中,我想要執行更有趣︰ 結合可擷取客戶物件,以擷取該客戶的訂單可觀察的觀察。幸運的是,RxJS 已針對該操作員︰ combineLatest。CombineLatest 運算子會接受兩個可預見值與處理函式。您傳遞給 combineLatest 的處理函式會從每個序列傳遞最新的結果,並可讓您指定應該 comingled 結果的方式。在我的案例,combineLatest 提供更多的功能,因為只有一個從每個可觀察的結果所需︰ 一個可觀察的客戶物件和所有來自其他客戶的訂單。

下列程式碼中,我將 (稱為 「 訂單 」) 的新屬性加入至客戶物件,從我的第一個可觀察,然後從第二個可觀察的結果放入該屬性。最後,我訂閱稱為 CustOrdersObsrvr 處理我的新客戶訂單 + 物件的觀察者︰

Rx.Observable.combineLatest(getCustObsvble, getOrdersObsvble,
                           (c, ords) => {c.Orders = ord; return c;})
             .subscribe(CustOrdersObsrvr);

我 custOrdersObservr 現在已可使用我所建立的新物件︰

var custOrdersObservr = {
  onNext: co => {
    // ...Code to update the page with customer and orders data...               
  },
  onError: err => $("#Message").text("Unable to retrieve Customer and Orders: "
    + err.description)
};

總結

藉由抽象化只是兩種類型的物件 (可預見值和觀察者) 應用程式的元件,並提供一組豐富的運算子來管理和轉換可預見值的結果,RxJS 提供高彈性 (和可維護性) 的方式建立 Web 應用程式。

當然,總是有危險使用功能強大的程式庫,包括複雜的處理程序簡單的程式碼︰ 當您的應用程式進行非預期的項目時,偵錯問題可能會成為一個夢魘因為看不到個別的抽象概念已經排除的程式碼行。當然,您不再需要撰寫程式碼,以及 (可能) 的抽象層會有較少的 bug,您會撰寫的程式碼。它是一般與之間的取捨電源可見性。

決定用於 RxJS,net 優點是您,您可以進行大幅變更您的應用程式而不需要進行大幅變更您的程式碼。


Peter Vogel 是系統架構設計人員和 PH & V 資訊服務中的主體。 PH (& V) 提供諮詢從透過物件模型和資料庫設計的 UX 設計的完整堆疊。您可以與他連絡︰ peter.vogel@phvis.com

感謝下列 Microsoft 技術專家來檢閱這份文件︰ Stephen Cleary、 James McCaffrey 和 Dave Sexton
Stephen Cleary 使用過多執行緒和非同步 16 年的程式設計和已使用 Microsoft.NET Framework 中的非同步支援自第一次的社群技術預覽。他是 「 並行存取在 C# 操作手冊 」 (O'Reilly Media,2014年) 的作者。他的首頁,包括他的部落格位於 stephencleary.com

Dr。James McCaffrey 適用於在美國華盛頓州 Redmond 的 Microsoft Research他曾在數個 Microsoft 產品,包括 Internet Explorer 和 Bing。Dr。可以連線到 McCaffrey jammc@microsoft.com